# Conversions API

What a conversions API is, why ad platforms need one, and how Zernio sends your sales and leads to every ad platform while tying each one to its campaign.

import { Tab, Tabs } from 'fumadocs-ui/components/tabs';

The Conversions API tells Zernio and your ad platforms which ads turned into real sales and leads. This page explains what that means and how it works, then shows your first call.

## What a conversion is

A conversion is a result you paid ads to get: a purchase, a lead, a signup, a booked call.

Ad platforms need to hear about every one. They use them to report what each campaign earned, and to show your ads to more people like your buyers.

## Why send conversions from your server

For years conversions reached ad platforms through a pixel, a script in the visitor's browser. Ad blockers, cookie limits and Apple's privacy rules now stop many of those signals before they arrive.

Many conversions never happen in a browser at all: a deal closed in your CRM, an order taken by phone, a lead qualified a week later.

So every ad platform now offers a conversions API: your server sends the result directly. The catch is that each platform has its own API, format, hashing rules and limits.

## Two ways conversions reach Zernio

Zernio does two jobs with conversions. It **counts** them per campaign, so you see what each campaign earned. And it **forwards** them to ad platforms, so they optimize your ads on real results.

How a conversion reaches Zernio decides which of the two happens.

### By connecting accounts, with no code

Connect an account and its results arrive on their own. They are counted for their campaigns, but not forwarded: the platform or the store already reports them.

| Connect | What arrives | Tied to its ad |
|---|---|---|
| <PlatformName slug="meta-ads" page="lead-forms" /> | A `Lead` for each lead form submission | Always: a lead form knows the ad it ran in |
| <PlatformName slug="google-ads" /> | A `Lead` for each lead form submission | Always |
| <PlatformName slug="linkedin-ads" page="lead-forms" /> | A `Lead` for each lead form submission | Always |
| <PlatformName slug="tiktok-ads" /> | A `Lead` for each Instant Form submission | Always |
| <PlatformName slug="shopify" page="commerce" /> | A `Purchase` for each paid order, net of refunds | Only when the order carries a UTM tag or click id |
| <PlatformName slug="wordpress" page="commerce" label="WooCommerce" /> | A `Purchase` for each paid order, net of refunds | Only when the order carries a UTM tag or click id |
| <PlatformName slug="hubspot" page="deals" /> | A `Purchase` for each won deal | Only when the deal names its campaign |

For lead forms, connecting the ad account is all it takes. A store knows what sold but usually not which ad sold it, so most orders count only in totals.

### By sending events from your server

For everything else, your server sends each conversion to `POST /v1/events`: a sale in your backend, a deal in your CRM, a signup in your app.

These events are counted **and** forwarded. You choose once which ad platforms receive them, then every event you send reaches each one, formatted and hashed the way that platform needs.

Send the click id or UTM tag from the visit along with the event. That is how Zernio ties it to its campaign.

## Set up in three steps

1. **Connect your ad accounts.** Meta, Google Ads, LinkedIn, TikTok, Pinterest, Reddit or OpenAI. Lead form leads start arriving now.
2. **Add a forwarding rule** for each ad platform that should receive your events, with `POST /v1/events/forwarding-rules`.
3. **Send events from your server** with `POST /v1/events`, each with the click id or UTM tag of the visit.

## What happens to a conversion

<ConversionsFlow />

Each conversion arrives as an **event**: one JSON object saying what happened, when, and who did it.

## Words you will see

| Word | Meaning |
|---|---|
| Event | One thing a customer did, such as a `Purchase` or a `Lead` |
| `eventId` | Your id for that event. Sending it twice keeps one copy |
| Destination | Where a platform records conversions: a Meta pixel, a Google conversion action |
| Forwarding rule | Your choice of which events go to which destination |
| Source | Who sent the event: your server, a lead form, a store, a CRM |
| Click id | The id an ad platform adds to a link when someone clicks an ad. The surest way to find the campaign |
| Profile | Who the events belong to. An agency keeps one profile per client |

## Send your first event

You need an API key with the Ads add-on and a profile id. This sends one purchase.

<Tabs items={['Node.js', 'Python', 'curl']}>
<Tab value="Node.js">
```typescript
import Zernio from '@zernio/node';

const zernio = new Zernio();

const { data } = await zernio.conversions.sendEvents({
  body: {
    profileId: '66a1f0c2a4b9d3e8f1a2b3c4',
    events: [{
      eventName: 'Purchase',
      eventTime: 1798848000,
      eventId: 'order_1042',
      value: 129.99,
      currency: 'USD',
      sourceUrl: 'https://shop.example.com/checkout?utm_campaign=fall_sale',
      user: { email: 'customer@example.com', clickIds: { gclid: 'EAIaIQobChMI...' } }
    }]
  }
});

console.log(data.stored, data.destinations);
```
</Tab>
<Tab value="Python">
```python
from zernio import Zernio

client = Zernio()

sent = client.conversions.send_events(
    profile_id="66a1f0c2a4b9d3e8f1a2b3c4",
    events=[{
        "eventName": "Purchase",
        "eventTime": 1798848000,
        "eventId": "order_1042",
        "value": 129.99,
        "currency": "USD",
        "sourceUrl": "https://shop.example.com/checkout?utm_campaign=fall_sale",
        "user": {"email": "customer@example.com", "clickIds": {"gclid": "EAIaIQobChMI..."}},
    }],
)

print(sent["stored"], sent["destinations"])
```
</Tab>
<Tab value="curl">
```bash
curl -X POST "https://zernio.com/api/v1/events" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profileId": "66a1f0c2a4b9d3e8f1a2b3c4",
    "events": [{
      "eventName": "Purchase",
      "eventTime": 1798848000,
      "eventId": "order_1042",
      "value": 129.99,
      "currency": "USD",
      "sourceUrl": "https://shop.example.com/checkout?utm_campaign=fall_sale",
      "user": { "email": "customer@example.com", "clickIds": { "gclid": "EAIaIQobChMI..." } }
    }]
  }'
```
</Tab>
</Tabs>

```json
{
  "eventsReceived": 1,
  "stored": true,
  "testCode": null,
  "destinations": [
    { "accountId": "66b2e19d8c3f5a7e9d0b1c2d", "destinationId": "1234567890123456", "platform": "metaads", "eventsSent": 1, "eventsFailed": 0, "failures": [] }
  ],
  "queuedDestinations": 0
}
```

`stored: true` means Zernio kept the event. Each entry in `destinations` is one ad platform that received it. Every field of an event is explained on [Send events](/products/conversions-api/send).

## Choose where events go

A forwarding rule sends a profile's events to one destination. Add one with `POST /v1/events/forwarding-rules`, picking the destination from `GET /v1/accounts/{accountId}/conversion-destinations`.

Set `eventNames` on the rule to forward only some events, for example only `Purchase` to Google. What each platform reads from an event is on [Platforms](/products/conversions-api/platforms).

## How an event finds its campaign

A stored `Purchase`, `Lead` or `QualifiedLead` counts for the campaign behind it, in [campaign results](/guides/campaign-results) and [ad rules](/guides/ad-rules). Zernio tries, in order:

1. The `zrn` ad id in `sourceUrl`, which Zernio adds to links of ads it creates.
2. The Google click behind `user.clickIds.gclid`.
3. `campaign.id`, the platform campaign id you send.
4. The `utm_campaign` of `sourceUrl`.
5. `campaign.name`, the campaign's name on the ad platform.

Only campaigns of the event's own profile count. An event that finds none is tried again every hour for 7 days, since click data and new campaigns arrive late.

## Correct or retract an event

Orders get refunded and deals fall through. Change an event you already sent instead of sending a new one:

- `PATCH /v1/events/{eventId}` with `profileId` and the new total `value` restates it, for a partial refund or an upgrade.
- `DELETE /v1/events/{eventId}?profileId=…` retracts it, for a full refund, a cancelled order or a lost deal.

```bash
curl -X PATCH "https://zernio.com/api/v1/events/order_1042" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "profileId": "66a1f0c2a4b9d3e8f1a2b3c4", "value": 50 }'
```

```json
{
  "eventId": "order_1042",
  "value": 50,
  "destinations": [
    { "accountId": "66b2e19d8c3f5a7e9d0b1c2d", "destinationId": "customers/1234567890/conversionActions/987654321", "platform": "googleads", "status": "corrected", "error": null },
    { "accountId": "66b2e19d8c3f5a7e9d0b1c2e", "destinationId": "1729525464415281", "platform": "metaads", "status": "not_supported", "error": null }
  ]
}
```

Zernio's own count changes at once, in campaign results and ad rules.

Each destination that received the event is told where the platform allows it: Google Ads changes its count, the others report `not_supported` and keep theirs.

## See what happened

[List events](/conversions/list-events) returns a profile's stored events, newest first. [Get one event](/conversions/get-event) shows each send to each platform: `sent`, `retrying` or `failed`, with the platform's error.

Personal data is never returned. `hasEmail`, `hasPhone` and `hasExternalId` only say what the event carried.

To hear about each event as it is stored, subscribe a webhook to [`conversion.received`](/webhooks/ads/conversion-received). It carries the event and the campaign it counts for.

## How it behaves

- **Sync by default.** The response waits for every destination. One that fails for now is retried and shows `retrying: true`.
- **Async on request.** `mode: "async"` stores the events, queues the sends and answers 202 at once.
- **One failure stays local.** A destination that fails never blocks the others.
- **Test events.** Sent with `testCode`, they are never stored. Only Meta, Pinterest and Reddit have a test mode.
- **Older integrations.** `POST /v1/ads/conversions` still works and is stored too, but sends to one destination per call. Use `POST /v1/events` for new work.

## If it fails

| Error | Cause | Fix |
|---|---|---|
| 403 `ads_addon_required` | The account has no Ads add-on | Add the Ads add-on |
| 400 on `events` | More than 1,000 events in one request | Split the batch |
| Event stored in the wrong year | `eventTime` sent in milliseconds | Send unix seconds |
| `error` on one destination | Its ad account is disconnected or lost access | Reconnect the account, then send again |
| 409 on a forwarding rule | The profile already forwards to that destination | Update or delete the existing rule |

## Related

- [Send events](/products/conversions-api/send)
- [Platforms](/products/conversions-api/platforms)
- [Send events reference](/conversions/send-events)
- [Get one event](/conversions/get-event)
- [Send your results per campaign](/guides/campaign-results)