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 ad click ID your site saved when the visitor landed, such as Google's gclid, Tapper stores that click ID with the order. That is how the order is tied 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 address.https://api.tapper.ai/order-feed/<id>/orders, where <id> is your order feed's number.
The secret.tof_ 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. Tapper does not read it from the address, the body or 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 tracking ID 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 its last four characters, 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
recordsList.Yes.1 to 500 records.
testtrue or false.No.true 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 adds it.

Each record is checked on its own. A record with a problem is refused with its reasons, 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 updated.Yes.
order_idText or whole number.Yes.Your 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 time.Yes.ISO 8601 with Z or an offset, for example 2026-10-15T09:30:00Z.
currencyText.Yes.Three-letter ISO 4217 code, for example AED.
valueNumber.Yes.The order's total value, 0 or more.
statusText.Yes.Your own status, for example paid. Up to 64 characters.
updated_atDate and time.No.When your store last changed the order.

About the order (all optional)

FieldTypeNotes
value_ex_tax_shippingNumber.The order value before tax and shipping.
discountNumber.The total discount.
coupon_codesList of text.Up to 20 codes, each up to 64 characters.
payment_methodText.Up to 64 characters, for example card.
sales_channelweb or app.
countryText.Two-letter ISO 3166 code, for example AE.
siteText.Which of your sites took the order. Up to 64 characters.

customer (an object, all optional)

FieldTypeNotes
customer_idText or whole number.Your own customer ID. Up to 255 characters, no spaces.
first_ordertrue or false.Whether this is the customer's first order.
email_sha256Text.The SHA-256 of the normalized email address, 64 hexadecimal characters.
phone_sha256Text.The 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 number.Yes.Your product ID. Up to 255 characters, no spaces.
categoryText.No.Up to 255 characters.
quantityWhole number.Yes.1 or more.
unit_priceNumber.Yes.The price of one unit, 0 or more.

A refund record (every field required)

FieldTypeNotes
eventrefund
order_idText or whole number.The order the refund belongs to.
refund_idText or whole number.Your refund ID. Sending the same refund ID again updates that refund.
refunded_atDate and time.ISO 8601 with Z or an offset.
amountNumber.The amount refunded, 0 or more.
currencyText.Must be the order's currency.

A cancellation record (every field required)

FieldTypeNotes
eventcancellation
order_idText or whole number.The order that was cancelled.
cancelled_atDate and time.ISO 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. Normalize 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 an order or refund Tapper did not hold, updated for one it did (sending the same batch again answers updated), and validated in a test batch. A cancellation always answers updated, because it changes an order Tapper already holds.
  • 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. When a field is wrong, the errors list names it.
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.
503Tapper could not store any record in the batch, and none was refused.Send the whole batch again. Your order and refund IDs make that safe.

In any other batch, a record Tapper could not store comes 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 gets the same checks as in a real batch, except the ones that need a stored order. 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, or that a refund's currency is the order's, because a test batch stores no order to check against.

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-21.

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.