Ads
Send purchases, leads and offline conversions from your server to Meta, Google, LinkedIn, TikTok, Pinterest, Reddit and ChatGPT with one event shape.
A browser pixel misses a lot: blocked scripts, purchases that close on the phone, deals that sign weeks later in your CRM. Server-side conversions fill the gap. You send the event from your backend once, in one shape, and Pozy delivers it to the ad network's own conversions API, hashed and formatted the way that network expects. Networks then attribute and optimize on it next to what the pixel saw.
| Network | Connection | Send events | Notes |
|---|---|---|---|
| Meta | metaads |
Yes | Web, app, offline, CRM and messaging events. Event Match Quality read-back. |
| Google Ads | googleads |
Yes | Click and enhanced conversions. Adjustments (refunds, restatements). |
linkedinads |
Yes | Offline conversions to a conversion rule. Attribution read-back. | |
| TikTok | tiktokads |
Yes | Offline events only (store, CRM, call center). |
pinterestads |
Yes | Event Quality Score read-back. | |
redditads |
Yes | To a pixel created in Reddit Ads Manager. | |
| ChatGPT Ads | openaiads |
Yes | Needs the pixel created first. |
| X | xads |
No | Measure X with the X Pixel. |
#Send your first event
#Find the destination
Every network calls the receiving object something different. GET /v1/accounts/{accountId}/conversion-destinations lists the ones the ads connection can reach, so you never have to guess the format.
curl https://api.pozy.ai/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/conversion-destinations \
-H "Authorization: Bearer $POZY_API_KEY"{
"platform": "metaads",
"destinations": [
{ "id": "1729525464415281", "name": "Acme website pixel", "status": "active", "adAccountId": "act_1234567890" }
]
}| Network | destinationId is |
Example |
|---|---|---|
| Meta | The pixel (dataset) id | 1729525464415281 |
| Google Ads | The conversion action resource name | customers/1234567890/conversionActions/987654321 |
| The conversion rule id or URN | 104012 |
|
| TikTok | The Offline Event Set id | 7057103914977558530 |
| The numeric ad account id | 549755885175 |
|
| The pixel id | a2_gu5tcd9ah5se |
|
| ChatGPT Ads | The pixel's site id | 4Kx9TqLm2VbN7wRz1PsYcD |
#Send the event
POST /v1/ads/conversions takes the ads connection, the destination and up to thousands of events. The connection's platform picks the network.
curl -X POST https://api.pozy.ai/v1/ads/conversions \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"destinationId": "1729525464415281",
"events": [{
"eventName": "Purchase",
"eventTime": 1804064400,
"eventId": "order_1042",
"value": 129.99,
"currency": "USD",
"actionSource": "web",
"sourceUrl": "https://shop.example.com/checkout/thanks",
"user": {
"email": "jane@example.com",
"phone": "+14155551234",
"ipAddress": "203.0.113.7",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5)",
"clickIds": { "fbc": "fb.1.1804060000.IwAR2...", "fbp": "fb.1.1803990000.1234567890" }
},
"items": [{ "id": "sku-981", "name": "Linen throw", "price": 64.99, "quantity": 2 }]
}]
}'const res = await fetch("https://api.pozy.ai/v1/ads/conversions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
accountId: "66b2e19d8c3f5a7e9d0b1c2d",
destinationId: "1729525464415281",
events: [
{
eventName: "Purchase",
eventTime: Math.floor(Date.now() / 1000),
eventId: "order_1042",
value: 129.99,
currency: "USD",
actionSource: "web",
sourceUrl: "https://shop.example.com/checkout/thanks",
user: {
email: "jane@example.com",
phone: "+14155551234",
ipAddress: "203.0.113.7",
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5)",
clickIds: { fbc: "fb.1.1804060000.IwAR2...", fbp: "fb.1.1803990000.1234567890" },
},
items: [{ id: "sku-981", name: "Linen throw", price: 64.99, quantity: 2 }],
},
],
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
if (data.eventsFailed) console.warn(data.failures);import os, time, requests
resp = requests.post(
"https://api.pozy.ai/v1/ads/conversions",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
json={
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"destinationId": "1729525464415281",
"events": [{
"eventName": "Purchase",
"eventTime": int(time.time()),
"eventId": "order_1042",
"value": 129.99,
"currency": "USD",
"actionSource": "web",
"sourceUrl": "https://shop.example.com/checkout/thanks",
"user": {
"email": "jane@example.com",
"phone": "+14155551234",
"ipAddress": "203.0.113.7",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5)",
"clickIds": {"fbc": "fb.1.1804060000.IwAR2...", "fbp": "fb.1.1803990000.1234567890"},
},
"items": [{"id": "sku-981", "name": "Linen throw", "price": 64.99, "quantity": 2}],
}],
},
)
body = resp.json()
print(body["eventsReceived"], body["failures"])#Check the result
{
"platform": "metaads",
"eventsReceived": 1,
"eventsFailed": 0,
"failures": [],
"traceId": "AbCdEf123456"
}A rejected event is not an HTTP error. The call returns 200 and lists what did not land in failures[], each with eventIndex, eventId, message and code. Always read eventsFailed. traceId is the network's own request id, useful when you contact that network's support.
#The event shape
| Field | Required | What to send |
|---|---|---|
eventName |
Yes | A standard name (Purchase, Lead, CompleteRegistration, AddToCart, InitiateCheckout, AddPaymentInfo, Subscribe, StartTrial, ViewContent, Search, Contact, SubmitApplication, Schedule, PageView) or your own name. See event names. |
eventTime |
Yes | Unix time in seconds, UTC. A millisecond value lands thousands of years in the future and is dropped. |
eventId |
Yes | Your stable id for this conversion, such as the order id. It is the dedupe key. |
value, currency |
No | The conversion value in whole units, and an ISO currency code. |
user |
Yes | Who converted. See match keys. |
items |
No | Line items: id, name, price, quantity, category. |
sourceUrl |
No | The page where it happened. |
actionSource |
No | web (default), app, offline, crm, phone_call, system_generated or business_messaging. |
messaging |
No | For conversations started from an ad: channel (whatsapp, messenger, instagram_direct) plus ids such as ctwaClid or pageScopedUserId. Meta only. |
platformData |
No | Extra network-specific fields Pozy does not model. On Meta they merge into custom_data. |
At the request root, testCode sends the batch to the network's test tools, and consent carries the person's choices. Both are explained below.
#Match keys
The more identifiers you send, the more conversions a network can tie back to an ad. Send them in plain text. Pozy normalizes and hashes personal data with SHA-256 to each network's spec before anything leaves Pozy, including Google's rule that strips dots and +suffix from Gmail addresses.
| Group | Fields under user |
|---|---|
| Contact | email, phone (E.164, with +), firstName, lastName |
| Location and demographics | country, city, state, zip, dob (YYYYMMDD), gender (f or m) |
| Identifiers | externalId (your own user id), ipAddress, userAgent, leadId |
Click ids, under clickIds |
fbc, fbp (Meta), gclid, gbraid, wbraid (Google), li_fat_id (LinkedIn), epik (Pinterest), rdt_cid, rdt_uuid (Reddit) |
ipAddress, userAgent and click ids are sent unhashed, as the networks require. LinkedIn takes externalId in plain text too.
Click ids arrive on the landing page URL (fbclid, gclid, li_fat_id, rdt_cid) or in first-party cookies (_fbp, _epik, _rdt_uuid). Store them with the visitor when they land, then attach them to every later conversion. They are the strongest match signal each network has.
Minimum identity per network:
| Network | Each event needs at least |
|---|---|
| Meta | Any match key; more is better |
| Google Ads | A click id, or an email or phone (enhanced conversions for leads) |
An email, li_fat_id, or both first and last name |
|
| TikTok | An email or a phone |
| An email, or the IP address with the user agent | |
A click id, rdt_uuid, email, phone, externalId, or IP with user agent |
|
| ChatGPT Ads | An email (the identifier it matches on) |
Events missing the minimum are reported in failures[] (for example INVALID_EVENT or MISSING_IDENTITY) and the rest of the batch still goes.
#Deduplicate against the pixel
Most sites send the same purchase from the browser pixel and from the server. Use the same eventId in both places and the network counts it once.
| Network | How the dedupe works |
|---|---|
| Meta | Pixel and server events with the same id merge within 48 hours. |
| Google Ads | eventId becomes the transaction id, so repeated uploads of one conversion are ignored. |
| When the Insight Tag fires the same id, LinkedIn keeps the tag's event and drops the server copy. | |
| Matched against the Pinterest tag by event id. | |
| Sent as the conversion id and matched against the Reddit pixel. | |
| ChatGPT Ads | Matched against the pixel by event id. |
A missing eventId, or a different id on the pixel and the server, counts the conversion twice. Pozy's store installs (see Pixels) set an event id on browser events so they can be matched.
#Event names per network
| Network | How eventName is used |
|---|---|
| Meta | Any name. Standard names match Meta's built-in events; anything else is a custom event. |
| Google Ads | Ignored. The conversion action decides the category. |
| Ignored. The conversion rule's type decides the category. | |
| TikTok | Sent as is. |
Standard names map to Pinterest's (Purchase is checkout, CompleteRegistration is signup, PageView is page_visit). Other names match a custom event of that name. |
|
Standard names map to Reddit's (CompleteRegistration is SIGN_UP, PageView is PAGE_VISIT). Other names become custom events. |
|
| ChatGPT Ads | Nine names map to its own types (Purchase is order_created, Lead is lead_created, AddToCart is items_added). Other names become custom events. |
#Offline and CRM conversions
Set actionSource to where it happened and send the identifiers you have from the CRM.
- Meta:
offline,crm,phone_callandsystem_generatedare all accepted on the same pixel. - Google Ads: create a conversion action for uploads (below), then send events with a
gclidwhen you kept one, or a hashed email or phone when you did not. - LinkedIn: send to a conversion rule.
eventNamedoes not matter; the rule's type does. - TikTok: the only path through this endpoint.
destinationIdis an Offline Event Set and each event needs an email or phone.
#Create a destination for uploads
POST /v1/accounts/{accountId}/conversion-destinations creates one on Google Ads and LinkedIn.
curl -X POST https://api.pozy.ai/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/conversion-destinations \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "adAccountId": "1234567890", "name": "Offline purchase", "type": "Purchase" }'- Google:
typeis a standard name or a Google category (PURCHASE,SUBSCRIBE_PAID,SIGNUP,IMPORTED_LEAD,BOOK_APPOINTMENT). The same name again returns the existing action; the same name with a different category returns409 idempotency_conflict.countingType(MANY_PER_CLICKby default) andprimaryForGoal(defaultfalse, so it records without driving bids) are Google-only. - LinkedIn: a rule binds a
typeto the destination. By default it attaches to every campaign in the ad account (autoAssociationType: "ALL_CAMPAIGNS");OBJECTIVE_BASEDattaches only matching campaigns andNONElets you manage it with the associations endpoints. Click and view windows default to 30 and 7 days and take 1, 7, 30, 90 or 365. Creating a rule is not idempotent.
On LinkedIn, GET .../conversion-destinations/{destinationId}/metrics reads post-click and view-through conversions and value back per rule, with granularity of ALL, DAILY, MONTHLY or YEARLY.
#Correct a conversion on Google Ads
POST /v1/ads/conversions/adjustments changes conversions you already uploaded. Google Ads is the only network with this.
adjustmentType |
Use it for |
|---|---|
RETRACTION |
Refunds, chargebacks, cancelled orders |
RESTATEMENT |
A changed value. Send the new total in restatementValue, not the difference. |
ENHANCEMENT |
Adding an email or phone to a conversion after the fact |
Identify the original with orderId, or gclid plus conversionTime. adjustmentTime is Unix seconds.
#Send once, deliver everywhere
When the same purchase should reach every network a customer advertises on, use the profile-level event stream instead of calling each destination.
#Save the destinations
POST /v1/events/destinations links an ads connection and destination to a profile. eventNames limits what it receives, for example only Purchase. Leave it out to forward everything.
curl -X POST https://api.pozy.ai/v1/events/destinations \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "profileId": "66b2e19d8c3f5a7e9d0b1c2a", "accountId": "66b2e19d8c3f5a7e9d0b1c2d", "destinationId": "1729525464415281", "eventNames": ["Purchase", "Lead"] }'#Send events to the profile
POST /v1/events takes profileId and the same events array. Pozy forwards each event to every saved destination, exactly as POST /v1/ads/conversions would. Pass destinations in the body to override the saved list for one request.
#Read the stream
GET /v1/events?profileId=... lists stored events, newest first. Personal data is never returned: hasEmail, hasPhone and hasExternalId only say whether an event carried them.
- One destination failing never blocks the others. Each reports its own result.
- Pozy stores email, phone and
externalIdonly as SHA-256 hashes, and keeps only the origin, path and attribution parameters ofsourceUrl. - Events sent with
testCodeare forwarded but not stored.
#Test mode
testCode at the request root lets you check an integration without polluting live data.
| Network | What testCode does |
|---|---|
| Meta | The batch shows in the Test Events tab of Events Manager. |
| Pinterest validates the batch and records nothing. | |
| Sent as Reddit's test id, visible in its Events Manager. |
Remove testCode for production traffic. An event sent with it is not counted on Pinterest.
#Consent
The root consent object takes adUserData and adPersonalization, each GRANTED or DENIED. Split batches by consent state rather than by network.
| Network | Effect of DENIED |
|---|---|
| Meta | Events are flagged for Limited Data Use, with the location derived from the event. |
| Google Ads | Sent as Google's EEA and UK consent signals. Omit consent outside those regions if you have no data. |
| Each event is marked opt-out. | |
| User identifiers are dropped; only the click id is sent. |
#Match quality
GET /v1/ads/conversions/quality?accountId=...&destinationId=... reads the network's own scoring of your server events, without opening its dashboard.
- Meta: Event Match Quality per event name,
compositeScorefrom 0 to 10, coverage per identifier inmatchKeys, and how well server events overlap the pixel. Web events only. - Pinterest: Event Quality Score over the last 14 days, per event name, with
matchKeysand pixel overlap. There is no composite score.
#Batching and limits
Pozy splits large batches for you. What happens when one event is bad depends on the network:
| Network | Chunk size | One bad event |
|---|---|---|
| Meta | 1,000 | Fails its chunk |
| Google Ads | 2,000 | Listed in failures[] |
| 5,000 | Fails its chunk | |
| TikTok | 1,000 | The network accepts or rejects a batch whole |
| Per event | Reported on its own | |
| 1,000 | Reported on its own; rejected chunks show REDDIT_REJECTED |
|
| ChatGPT Ads | 1,000 | Fails its chunk |
Google can take up to 3 hours to show an uploaded conversion. Reddit accepts events up to 7 days old.
#Permissions and reconnects
Some networks grant the conversions permission separately. A connection made before that permission existed has to go through the connect flow again.
| Network | Symptom | Fix |
|---|---|---|
403 linkedin_reconnect_required |
Reconnect LinkedIn Ads so the grant includes conversions. | |
| TikTok | Calls refused for the Offline Events permission | Reconnect TikTok Ads. |
403 reconnect_required |
Reconnect Reddit Ads to grant adsconversions. |
|
403 |
Give the connected user a Business Access role on the ad account. | |
| ChatGPT Ads | 422 tracking_tag_required |
Create the pixel first. |
#Endpoints
Meta custom conversions (rules over pixel events) live on the ad account: GET and POST /v1/accounts/{accountId}/custom-conversions.