Send conversion events
Sends conversion events, such as purchases or leads, from your server to an ad platform so it can attribute and optimize ads.
Relay one or more conversion events to the target ad platform's native Conversions API.
Platform is inferred from the provided accountId. Requires the Ads add-on.
Supported platforms:
- Meta (
metaads) via Graph API - Google Ads (
googleads) via Data Manager APIingestEvents - LinkedIn (
linkedinads) via/rest/conversionEvents - TikTok (
tiktokads) via the Offline Events API/offline/batch/(OFFLINE conversions only) - OpenAI Ads (
openaiads) via its Conversions API (a separate host,bzr.openai.com) - Pinterest (
pinterestads) viaPOST /v5/ad_accounts/{id}/events - Reddit (
redditads) via the Reddit Conversions API (POST /api/v3/pixels/{pixelId}/conversion_events)
destinationId semantics differ per platform:
- Meta: pixel (dataset) ID, e.g.
123456789012345 - Google: conversion action resource name, e.g.
customers/1234567890/conversionActions/987654321 - LinkedIn: conversion rule ID or URN, e.g.
104012orurn:lla:llaPartnerConversion:104012 - TikTok: Offline Event Set ID, e.g.
7057103914977558530 - OpenAI Ads: pixel wire id (numeric
pixel_id, distinct from the internal pixel id), as returned byGET /v1/accounts/{accountId}/conversion-destinations - Pinterest: numeric ad account id, e.g.
549755885175(Pinterest attributes to the ad account, not to a tag) - Reddit: pixel id, e.g.
a2_abc123, as returned byGET /v1/accounts/{accountId}/conversion-destinations(any other shape returns 400)
Pinterest notes: each event needs an email, or ipAddress plus userAgent; others are
listed in failures with code INVALID_EVENT and the rest are still sent. clickIds.epik
(the _epik cookie) is sent as click_id. actionSource web stays web, offline
sources become offline, and app events need platformData.action_source app_android
or app_ios. Pinterest answers per event, so one bad event does not fail its chunk
(failures carry code PINTEREST_EVENT_FAILED). consent.adUserData: DENIED sets opt_out.
The connected user needs a Business Access role on the ad account (403 otherwise).
Reddit notes: each event needs a match key: clickIds.rdt_cid, clickIds.rdt_uuid,
email, phone, externalId, or ipAddress plus userAgent. Reddit accepts events up to
7 days old, and eventTime is unix SECONDS (a value more than an hour in the future is
refused as milliseconds). Events failing these checks are listed in failures with code
INVALID_EVENT and the rest are still sent. Email (lowercased, dots and +tags stripped
from the local part), phone (E.164 with the leading +) and externalId are SHA-256
hashed; IP and user agent go plaintext. consent.adUserData: DENIED sends no user fields.
actionSource maps to WEBSITE (web), APP (app) or OTHER, overridable with
platformData.action_source; platformData.order_id is forwarded. testCode is sent as
Reddit's test_id. The connection must hold the adsconversions permission: one
authorized without it returns 403 reconnect_required (reconnect via
GET /v1/connect/redditads/ads), and a Reddit user with no role on the ad account that owns
the pixel returns 403 insufficient_permissions. Reddit throttling and other rejections are
reported per chunk in failures with code REDDIT_REJECTED.
TikTok notes: this path sends OFFLINE conversions (in-store / CRM / call-center), not web-pixel events. Each event must carry an email or phone (TikTok requires at least one). The connected TikTok ads account must have granted the Offline Events permission; older grants must reconnect.
OpenAI Ads notes: requires a tracking tag (pixel) to already exist on the account. Returns 422
with code TRACKING_TAG_REQUIRED if POST /v1/accounts/{accountId}/tracking-tags hasn't been
called yet.
Callers can list valid destinations via GET /v1/accounts/{accountId}/conversion-destinations.
All PII (email, phone, names, external IDs) is hashed with SHA-256 server-side per each
platform's normalization spec, including Google's Gmail-specific dot/plus-suffix stripping.
Send plaintext. LinkedIn externalIds are passed through as plaintext per LinkedIn's spec;
only emails and phones are hashed.
For LinkedIn, the connected account must have been authorized after the Conversions API
rollout (i.e. the OAuth grant must include rw_conversions). Older accounts must reconnect.
Batching is handled automatically. Meta caps at 1000 events per request and rejects the entire batch if any event is malformed. Google caps at 2000. LinkedIn caps at 5000 and is also all-or-nothing per chunk. OpenAI Ads caps at 1000 per request; larger submissions are split into 1000-event chunks, each all-or-nothing (a malformed event fails every event in that chunk, not the whole request). Reddit works the same way, in chunks of 1000.
Dedup: pass a stable eventId on every event. Meta and LinkedIn use it to dedupe against
browser-side pixel/Insight Tag events; Google maps it to transactionId; Reddit receives it as
conversion_id, its dedup key against Reddit Pixel events.
Per-platform eventName semantics:
- Meta: free-form. Standard names (Purchase, Lead, ...) match Meta's built-in events; custom strings are accepted.
- Google: ignored. The conversion action's category determines the event type. Send the standard name closest to your action for documentation, but the platform will not branch on it.
- LinkedIn: ignored. The conversion rule's
type(LEAD, PURCHASE, etc.) is locked to the destination at rule-creation time. Send the standard name for documentation; LinkedIn does not branch on it. - OpenAI Ads: a fixed subset of standard names (Purchase, Lead, AddToCart, ViewContent, InitiateCheckout, CompleteRegistration, Subscribe, StartTrial, Schedule) maps 1:1 onto OpenAI's own event-type enum; any other standard name or custom string is sent as
type: customwith the name preserved. - Pinterest: standard names map onto Pinterest's (Purchase =
checkout, AddToCart =add_to_cart, InitiateCheckout =initiate_checkout, AddPaymentInfo =add_payment_info, Lead =lead, CompleteRegistration =signup, Subscribe =subscribe, ViewContent =view_content, Search =search, PageView =page_visit); any other string is sent as is, so it matches an advertiser defined event of that name.
Authorization
bearerAuth API key authentication: send your Zernio API key in the Authorization header, prefixed with Bearer.
In: header
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
import Zernio from '@zernio/node';const zernio = new Zernio({ apiKey: process.env.ZERNIO_API_KEY });const { data } = await zernio.conversions.sendConversions({ body: { accountId: 'account_abc123', destinationId: 'destination_abc123', events: [ { eventName: 'Purchase', eventTime: 1744732800, eventId: 'order_abc_123', user: { email: 'user@example.com', phone: 'string', firstName: 'Example', }, }, ], },});console.log(data);{ "platform": "metaads", "eventsReceived": 0, "eventsFailed": 0, "failures": [ { "eventIndex": 0, "eventId": "string", "message": "string", "code": "string", "platformError": {} } ], "traceId": "string"}Remove a custom conversion goal
Deletes a Google Ads custom conversion goal; switch any campaigns using it to another goal first. Google refuses (400) while any campaign still uses it: switch those campaigns to another goal first.
Create Click-to-Call ad
Creates a Meta ad with a Call Now button that dials your phone number, optimized for quality calls. Same shape and flow as POST /v1/ads/ctwa, but the CTA is CALL_NOW dialing `phoneNumber` via a tel: link. The ad set is destination_type PHONE_CALL optimizing QUALITY_CALL and the campaign objective defaults to OUTCOME_LEADS. Supports the same single-creative and multi-creative shapes as CTWA. **Idempotency:** this endpoint is not idempotent at the platform level (a blind retry creates a second campaign/ad set/ad). Send an `Idempotency-Key` header to make retries safe: the first request with a given key creates the ad and we store the response; a retry with the same key replays that exact response (with `Idempotent-Replayed: true`) instead of creating duplicates. Reusing a key with a different body returns 422; a key whose first request is still in flight returns 409 (retry after a short backoff). If the first request is cut off mid-create (for example by a function timeout), its key can stay in progress and keep returning 409 until it expires; use a new key to retry sooner, after checking whether the ad was created. Keys are scoped to your credential and expire after 24h.