Offline conversions API
Send leads, sign-ups and sales from your own servers to Tapper Relay with one POST, matched to the ad click or person and sent to your ad platforms.
Your servers send each lead, sign-up or sale to one address for a protected entity, which holds a website, mobile apps or both. Relay matches it to the ad click or the person behind it and sends it to your ad platforms, so they bid on what really happens after the click. The address and your event names are under Relay, Offline conversions, in the Tapper app.
Authenticate
Send your workspace API key in the Authorization header of every request. Create the key in Settings, API keys. See Settings.
POST /relay/push/<entity id>/events HTTP/1.1
Host: api.tapper.ai
Content-Type: application/json
Authorization: <your API key>- Call it from your servers only. The key is a password for your workspace and must never reach a browser or an app.
- A missing or wrong key gets
401. - Owners and admins create API keys.
- Give the key the scope Send conversions to Relay (
RELAY_EVENTS), and set Entity access to All or Selected, naming the entity you send for.
Before the first request
In the Tapper app, open Relay, then Offline conversions. Copy the address of your entity, https://, where <entity id> is the entity's ID.
Under Event names, list each event name your servers send and the name Relay sends it to your ad platforms as. An event with a name that is not listed is refused, so nothing goes out by accident. You can list up to 100 names.
While Receiving is off, every request is refused with 403.
What you send
Send a POST with a JSON body:
| Field | Type | Required | What it is |
|---|---|---|---|
events | List. | Yes. | 1 to 500 events. |
test | true or false. | No. | true checks the events and stores nothing. |
Each event is checked on its own. An event with a problem is refused with its reasons, and the other events in the batch are still accepted. A field this table does not list is refused, not ignored.
| Field | Type | Required | Notes |
|---|---|---|---|
event_id | Text. | Yes. | Your own ID for the event. Up to 120 characters, no spaces. Send the same ID on every retry. |
event_name | Text. | Yes. | One of the names you listed under Event names. Up to 100 characters. |
occurred_at | Date and time. | Yes. | ISO 8601 with Z or an offset, for example 2026-10-31T09:30:00Z. Not in the future, and not more than 30 days old. |
value | Number or text. | No. | A positive decimal with at most 6 decimals. Needs currency. |
currency | Text. | With value. | Three-letter code, for example USD. |
email_sha256 | Text. | No. | SHA-256 of the email, 64 hexadecimal characters. |
phone_sha256 | Text. | No. | SHA-256 of the phone number, 64 hexadecimal characters. |
gclid | Text. | No. | Google's click ID. |
fbclid | Text. | No. | Meta's click ID. |
ttclid | Text. | No. | TikTok's click ID. |
lead_id | Text. | No. | The Meta lead ID of a Lead Ad lead, 15 to 17 digits, as text. Relay sends the event to Meta as a stage of that lead. |
valueless | true or false. | No. | true for an event that has no value by design, such as a lead stage or a sign-up. It then carries no value. |
consent | GRANTED, DENIED or UNKNOWN. | No. | Whether the person agreed to ad measurement. Without it, UNKNOWN. |
Click IDs are 1 to 255 letters, digits and - _ . ~. Send them exactly as the ad platform set them.
How a conversion is matched
Send a click ID if you have one: gclid, fbclid or ttclid. It is the surest match. Without one, send the hashed email, email_sha256, or the hashed phone number, phone_sha256. Tapper links it to the visit and its click IDs, and passes it to the platforms for their own matching. For Meta Lead Ads, the lead_id matches on its own.
An event with nothing a platform can match on is recorded and not sent. Each platform credits the conversion only when it recognises the person.
Hash email and phone
Hash on your servers, before the request leaves them. Never send a raw email or phone number: an event with a field named like one, for example email or phone, is refused and the reason says to send the hash.
- Email: trim spaces, convert to lowercase, then SHA-256, as 64 hexadecimal characters.
- Phone: write it in E.164 form, a
+, the country code, then the digits, then SHA-256.
For example, jane.doe@example.com hashes to 86e0b9e56c17cc4d12387e1949b85053fbe73bc3ce5a1188713a9d300cc6133d.
Value and currency
Send the value net of tax and refunds, with shipping left out, the same as Relay counts a store order. A value always comes with its currency. Relay sends money in the ad account's own currency and never converts it, so an event in another currency is not sent to that ad account. Leave value out for a lead stage or a sign-up and set valueless to true.
No double counting
event_id is the key. Send the same event_id again, for example after a timeout, and Tapper answers duplicate and sends nothing twice. Retrying a whole batch is always safe.
Where events go
Events are sent to your Meta Ads and TikTok Ads accounts that have offline conversions switched on. Each reaches the platform under its Relay name, TapperRelay followed by the name you chose under Event names, so it shows apart from your pixel events. Which ad accounts receive them is under Routing.
Example
curl -sS -X POST "https://api.tapper.ai/relay/push/<entity id>/events" \
-H "Content-Type: application/json" \
-H "Authorization: $TAPPER_API_KEY" \
-d @batch.jsonbatch.json:
{
"events": [
{
"event_id": "lead-48213",
"event_name": "qualified_lead",
"occurred_at": "2026-10-31T09:30:00Z",
"email_sha256": "86e0b9e56c17cc4d12387e1949b85053fbe73bc3ce5a1188713a9d300cc6133d",
"fbclid": "IwAR2xK9",
"valueless": true
},
{
"event_id": "sale-90017",
"event_name": "sale",
"occurred_at": "2026-10-31T10:12:00Z",
"value": 349.5,
"currency": "USD",
"gclid": "EAIaIQobChMIq5bW4o2ziQMV",
"consent": "GRANTED"
}
]
}import { createHash } from "node:crypto";
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
const res = await fetch("https://api.tapper.ai/relay/push/<entity id>/events", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: process.env.TAPPER_API_KEY,
},
body: JSON.stringify({
events: [
{
event_id: "lead-48213",
event_name: "qualified_lead",
occurred_at: new Date().toISOString(),
email_sha256: sha256("jane.doe@example.com".trim().toLowerCase()),
valueless: true,
},
],
}),
});
console.log(res.status, await res.json());The response
A 200 lists what happened to each event. index is its position in events, counting from 0.
{
"data": {
"test": false,
"received": 2,
"accepted": [
{ "index": 0, "event_id": "lead-48213", "outcome": "stored" }
],
"rejected": [
{
"index": 1,
"event_id": "sale-90017",
"reasons": ["event_name is not mapped for this endpoint: map it to a Relay event name first."]
}
]
}
}outcome | Meaning |
|---|---|
stored | Relay has the event and will send it. |
duplicate | You sent this event_id before. Nothing was added. |
off | No ad account has offline conversions switched on yet, so the event was not kept. |
validated | A test request. The event passed every check and nothing was stored. |
A refusal reason names the field and never repeats what you sent.
Test first
Add "test": true to check a batch without storing anything. Each good event answers validated.
Other answers
| Status | Code | When | What to do |
|---|---|---|---|
400 | The body is wrong: not JSON, events missing, empty or longer than 500. | Fix the body. The errors list names the field. | |
401 | The API key is missing or wrong. | Check the key. | |
403 | API_KEY_SCOPE_MISSING | The key does not have the scope Send conversions to Relay. | Add the scope to the key under Settings, API keys. |
403 | API_KEY_ENTITY_NOT_ALLOWED | The key's Entity access does not include this entity. | Set Entity access to All, or select this entity. |
403 | RELAY_NOT_ON_WORKSPACE | Relay is not switched on for your workspace. | Ask your Tapper contact. |
403 | RELAY_PUSH_OFF | Receiving is switched off. | Switch it on under Relay, Offline conversions. |
404 | ENTITY_NOT_FOUND | The address names an entity that does not exist. | Check the entity ID in the address. |
413 | The body is larger than 1 MiB. | Send fewer events in each batch. | |
429 | More than 300 requests in a minute from one IP address. | Wait a minute, then carry on. | |
503 | Tapper could not store the batch. | Send the whole batch again. Your event IDs make that safe. |