Tapper
Search docs...
⌘K
SupportDashboard

Send orders from your servers

Send your store's orders to Tapper from your own servers, with the click ID your site saved at landing, so each order can be tied to the ad click behind it.


If your store is custom-built, there is no store platform for Tapper to connect to. Instead, your servers send each order to a secure address that belongs to your protected entity. When an order carries the Google click ID your site saved when the visitor landed, that click ID is what lets Tapper tie the order to the ad click behind it, with no change in the browser.

What we never accept

Never send names, postal addresses, raw email addresses or raw phone numbers. Tapper checks every record for them, refuses a record where it finds one and says why in the response. The rest of the batch is still accepted.

Tapper looks for personal data in two ways, anywhere in a record:

  • By field name. A field named like an email, phone, name or address (for example email, phone, mobile, first_name, last_name, customer_name, address, shipping_address, street, city or postcode) is refused, whatever it holds.
  • By value. Any text that looks like an email address, a phone number or a street or building address is refused, whatever field it is in.

Every ID field refuses spaces, so a name cannot be sent as a customer ID. Country is all Tapper takes about where an order goes.

An email address or phone number may only be sent as a hash, made on your servers (see Hash email and phone). A refusal reason names the field and never repeats what you sent, for example:

customer.email is a raw email address, which Tapper does not take: send customer.email_sha256, the SHA-256 of the trimmed, lower-cased address, instead.
customer.first_name is a person's name, which Tapper does not take: send customer.customer_id, the store's own customer ID, instead.
payment_method looks like a raw phone number. Tapper does not take personal data: send customer.phone_sha256, the SHA-256 of the number in E.164 form, instead.

The address and how to authenticate

Each protected entity has one order feed. To get one, ask your Tapper contact. When it is set up you get two things:

WhatExample
The addresshttps://api.tapper.ai/order-feed/<id>/orders, where <id> is your order feed's number
The secrettof_ followed by 64 hexadecimal characters

Send every batch as a POST with a JSON body, and the secret in the X-Tapper-Order-Feed-Secret header:

POST /order-feed/<id>/orders HTTP/1.1
Host: api.tapper.ai
Content-Type: application/json
X-Tapper-Order-Feed-Secret: tof_<your secret>
  • The header is the only place the secret is read. Not the address, not the body, not an Authorization header.
  • Call it from your servers only. The secret is a password for writing orders into your Tapper data. It is not the public key in your monitoring script, and it must never reach a browser or an app.
  • The secret is shown once. Tapper keeps only a salted hash of it and cannot show it again. Store it in your secrets manager.
  • A new secret stops the old one at once. If the secret is lost or may have leaked, ask for a new one. Batches sent with the old secret get 401 from the moment the new one is issued, so switch your servers over straight away.

A missing or wrong secret, or an address that does not exist, all get the same answer: 401 with a WWW-Authenticate: X-Tapper-Order-Feed-Secret realm="tapper-order-feed" header and this body.

{
  "errors": [
    {
      "code": "unauthorized",
      "message": "Send this order feed's secret in the X-Tapper-Order-Feed-Secret request header."
    }
  ]
}

What you send

The body has two fields:

FieldTypeRequiredWhat it is
recordslistYes1 to 500 records.
testtrue or falseNotrue checks and echoes the records and stores nothing (see Test without storing anything).

Each record is one of four events, named in its event field:

eventWhen to send it
placedAn order was placed.
updatedAn order changed, for example its status or value. Send the whole order again.
refundA refund, full or partial.
cancellationAn order was cancelled.

Your order ID is the key. Sending the same order again, as placed or updated, updates the order Tapper holds instead of adding a second one. An updated record for an order Tapper does not hold yet adds it.

Each record is checked on its own. A record with a problem is refused with its reasons, up to 20, and the other records in the batch are still accepted.

Field table

Field names are exact and in snake_case. A field this table does not list is refused, not ignored.

Every placed and updated record

FieldTypeRequiredNotes
eventplaced or updatedYes
order_idtext or whole numberYesYour order ID. Up to 255 characters, no spaces. A number is kept as its digits; send IDs longer than 15 digits as text.
placed_atdate and timeYesISO 8601 with Z or an offset, for example 2026-10-15T09:30:00Z.
currencytextYesThree-letter ISO 4217 code, for example AED.
valuenumberYesThe order's total value, 0 or more.
statustextYesYour own status, for example paid. Up to 64 characters.
updated_atdate and timeNoWhen your store last changed the order.

About the order (all optional)

FieldTypeNotes
value_ex_tax_shippingnumberThe order value before tax and shipping.
discountnumberThe total discount.
coupon_codeslist of textUp to 20 codes, each up to 64 characters.
payment_methodtextUp to 64 characters, for example card.
sales_channelweb or app
countrytextTwo-letter ISO 3166 code, for example AE.
sitetextWhich of your sites took the order. Up to 64 characters.

customer (an object, all optional)

FieldTypeNotes
customer_idtext or whole numberYour own customer ID. Up to 255 characters, no spaces.
first_ordertrue or falseWhether this is the customer's first order.
email_sha256textThe SHA-256 of the normalised email address, 64 hexadecimal characters.
phone_sha256textThe SHA-256 of the phone number in E.164 form, 64 hexadecimal characters.

click (an object, all optional)

Send each click ID exactly as the ad platform set it, up to 1,024 characters with no spaces.

FieldWhat it is
gclidGoogle's click ID.
gbraidGoogle's click ID for clicks on iOS devices that lead to app conversions.
wbraidGoogle's click ID for clicks on iOS devices that lead to web conversions.
fbclidMeta's click ID.
ttclidTikTok's click ID.
visitor_idTapper's visitor ID, a UUID, if your site can read it.

lines (a list, optional, up to 200 product lines)

FieldTypeRequiredNotes
product_idtext or whole numberYesYour product ID. Up to 255 characters, no spaces.
categorytextNoUp to 255 characters.
quantitywhole numberYes1 or more.
unit_pricenumberYesThe price of one unit, 0 or more.

A refund record (every field required)

FieldTypeNotes
eventrefund
order_idtext or whole numberThe order the refund belongs to.
refund_idtext or whole numberYour refund ID. Sending the same refund ID again updates that refund.
refunded_atdate and timeISO 8601 with Z or an offset.
amountnumberThe amount refunded, 0 or more.
currencytextMust be the order's currency.

A cancellation record (every field required)

FieldTypeNotes
eventcancellation
order_idtext or whole numberThe order that was cancelled.
cancelled_atdate and timeISO 8601 with Z or an offset.

How updates work

  • An optional field you leave out of an updated record keeps the value Tapper already holds.
  • When a record carries lines, those are the order's lines, by position. A line the new list no longer has is marked removed. A record without lines leaves the lines as they were.
  • A refund or a cancellation needs its order to be stored first, earlier in the same batch or in an earlier one. Send each order before its refunds.
  • No time may be more than a day in the future.

Hash email and phone

Hash on your servers, before the request leaves them. Normalise first, then hash, the same way every time, so the same address always gives the same hash.

Email

  1. Remove spaces at the start and end.
  2. Convert to lowercase.
  3. Hash the result with SHA-256 and send it as 64 hexadecimal characters.

Phone

  1. Write the number in E.164 form: a +, the country code, then the number, digits only (for example +971501234567).
  2. Hash the result with SHA-256 and send it as 64 hexadecimal characters.

For example, jane.doe@example.com hashes to 86e0b9e56c17cc4d12387e1949b85053fbe73bc3ce5a1188713a9d300cc6133d, and +971501234567 to 5cd602a57e6a0414e38023888d6275b31208be50ab5cf4c1e7f197138da7fed1.

Example request and response

This batch sends an order, a partial refund of it, and a second order that wrongly carries a raw email address.

curl -sS -X POST "https://api.tapper.ai/order-feed/<id>/orders" \
  -H "Content-Type: application/json" \
  -H "X-Tapper-Order-Feed-Secret: $TAPPER_ORDER_FEED_SECRET" \
  -d @batch.json

batch.json:

{
  "records": [
    {
      "event": "placed",
      "order_id": "100234",
      "placed_at": "2026-10-15T09:30:00Z",
      "currency": "AED",
      "value": 349.5,
      "status": "paid",
      "value_ex_tax_shipping": 315,
      "discount": 20,
      "coupon_codes": ["WELCOME10"],
      "payment_method": "card",
      "sales_channel": "web",
      "country": "AE",
      "customer": {
        "customer_id": "C-88231",
        "first_order": true,
        "email_sha256": "86e0b9e56c17cc4d12387e1949b85053fbe73bc3ce5a1188713a9d300cc6133d"
      },
      "click": {
        "gclid": "EAIaIQobChMIq5bW4o2ziQMVvJiDBx2X7AqPEAAYASAAEgKj3_D_BwE"
      },
      "lines": [
        { "product_id": "SKU-4417", "category": "Kitchen", "quantity": 2, "unit_price": 99.75 },
        { "product_id": "SKU-0981", "quantity": 1, "unit_price": 150 }
      ]
    },
    {
      "event": "refund",
      "order_id": "100234",
      "refund_id": "R-5521",
      "refunded_at": "2026-10-16T08:05:00+04:00",
      "amount": 99.75,
      "currency": "AED"
    },
    {
      "event": "placed",
      "order_id": "100235",
      "placed_at": "2026-10-15T10:02:00Z",
      "currency": "AED",
      "value": 89,
      "status": "paid",
      "customer": { "email": "jane.doe@example.com" }
    }
  ]
}

The response, 200:

{
  "data": {
    "test": false,
    "received": 3,
    "accepted": [
      { "index": 0, "event": "placed", "order_id": "100234", "outcome": "inserted" },
      { "index": 1, "event": "refund", "order_id": "100234", "refund_id": "R-5521", "outcome": "inserted" }
    ],
    "rejected": [
      {
        "index": 2,
        "event": "placed",
        "order_id": "100235",
        "reasons": [
          "customer.email is a raw email address, which Tapper does not take: send customer.email_sha256, the SHA-256 of the trimmed, lower-cased address, instead."
        ]
      }
    ]
  }
}
  • index is the record's position in records, counting from 0.
  • outcome is inserted for a record Tapper did not hold, updated for one it did (sending the same batch again answers updated), and validated in a test batch.
  • A rejected record shows its event and order_id only when they passed their own checks; otherwise they are null.

Other answers

StatusWhenWhat to do
400The body itself is wrong: not JSON, records missing, empty or longer than 500, or test not true or false. No record is checked or stored.Fix the body. The errors list names the field.
401The secret is missing or wrong, or the address does not exist.Check the address and the header.
403The secret is right and the order feed is switched off. Nothing is stored.Ask your Tapper contact to switch it on.
413The body is larger than 8 MiB.Send fewer records in each batch.
429More than 300 batches in a minute from one IP address.Wait a minute, then carry on.
503Every record that passed its checks failed to store.Send the whole batch again. Your order and refund IDs make that safe.

If only some records fail to store, those come back in rejected with the reason Tapper could not store this record just now. Send it again.

Test without storing anything

Add "test": true to the body to check a batch against production without storing anything:

{
  "test": true,
  "records": [
    {
      "event": "placed",
      "order_id": "100234",
      "placed_at": "2026-10-15T09:30:00Z",
      "currency": "aed",
      "value": 349.5,
      "status": "paid"
    }
  ]
}

Every record is checked exactly as in a real batch. Each accepted record comes back with outcome validated and a record field holding the record as Tapper would store it, for example with the currency in capitals:

{
  "data": {
    "test": true,
    "received": 1,
    "accepted": [
      {
        "index": 0,
        "event": "placed",
        "order_id": "100234",
        "outcome": "validated",
        "record": {
          "event": "placed",
          "order_id": "100234",
          "placed_at": "2026-10-15T09:30:00Z",
          "currency": "AED",
          "value": 349.5,
          "status": "paid"
        }
      }
    ],
    "rejected": []
  }
}

A test batch checks each record on its own. It does not check that a refund's or a cancellation's order is already stored, because a test batch stores no order for it to find.

Send your order history

Send past orders through the same address, in batches of up to 500, as placed records, with their refunds and cancellations after them. Tapper takes orders placed up to 24 months ago; an older one is refused with the earliest date it takes, for example:

placed_at is more than 24 months ago. The order feed takes history back to 2024-10-16.

Send up to 300 batches a minute from one IP address. Sending a batch again is safe: an order already stored is updated, not added twice.