# Send purchases to your ad platforms

> Send purchases, renewals, sign-ups and refunds from our servers to Google Analytics 4, Google Ads, Meta, TikTok, Microsoft Ads, Reddit and X.

Source: https://www.coritan.com/docs/organizations/conversions/

In the dashboard:

- /dashboard/organizations/…/settings/conversions: https://www.coritan.com/dashboard/organizations

A *server-side conversion* is a sale that we report to an ad platform from our servers, after the invoice is paid. The [browser tags](/docs/organizations/tags/) report a purchase only when the buyer's browser stays open, allows the tag and is not blocked. Reporting from the server as well counts the sales that the browser missed, and it counts refunds, which no browser sees.

You give each platform the ID and secret that let us report to your account. We then send what happens on your storefront: each paid invoice, each new account and each refund. We send nothing for a platform you leave off.

## Before you begin

- You need the Owner or Admin role in the organization. Members with any other role cannot read or change these settings, because they hold platform secrets.
- You need an account on each platform you want to use, with its pixel or data source already made, and an access token or API secret for it. [Set up each platform](#set-up-each-platform) says where to find them.
- Your storefront must be one that Coritan serves, so the visitor's tracking cookies reach us with the order. A storefront you build yourself on the [Storefront API](/docs/organizations/storefront/) needs to forward them, as [How customers and orders are attributed](/docs/organizations/attribution/) describes.

## What is sent

One event goes to each platform that is on, for each of these things that happen:

| Event | When it is sent | Sent as |
| --- | --- | --- |
| `purchase` | An invoice is paid and a service on it was still waiting for that payment: the buyer's first purchase of that service. | Google Analytics 4 `purchase`, Google Ads (the `purchase` label), Meta `Purchase`, TikTok `CompletePayment`, Reddit `Purchase`, X (the `purchase` event). |
| `upgrade` | A paid invoice charges for a plan change, an add-on or a one-time line on a service that is already active. | The same as a purchase. |
| `renewal` | A paid invoice bills a service that is already active for another period. Off until you turn on `send_renewals` for the platform. | Google Analytics 4 `renewal`, Google Ads (the `renewal` label), Meta, TikTok and Reddit as a custom event named `Renewal`, X (the `renewal` event). |
| `free_server` | An order with nothing to pay, such as a free server, is settled. It is a lead and never a purchase. | Google Analytics 4 `generate_lead`, Google Ads (the `lead` label), Meta `Lead`, TikTok `CompleteRegistration`, Reddit `Lead`, X (the `lead` event). |
| `sign_up` | A customer account is made on your storefront. | Google Analytics 4 `sign_up`, Google Ads (the `sign_up` label), Meta and TikTok `CompleteRegistration`, Reddit `SignUp`, X (the `sign_up` event). |
| `refund` | A refund of a sale is booked on the invoice, for the amount that went back. | Google Analytics 4 `refund` only. |
| `chargeback` | A chargeback on a sale is lost and its money taken back. | Google Analytics 4 `refund` only. |

Nothing is sent for an invoice that reactivates a suspended service, for an invoice with nothing to pay on a service that is already active, or for an invoice with no service on it. An invoice paid before you turned a platform on is never sent to it. A refund or a lost chargeback is sent only for an invoice that was a purchase, an upgrade or a renewal when it was paid. A refund that you give as account credit is not sent, because no money went back to the buyer.

Every event for a purchase carries the same ID, `purchase:` followed by the invoice number, such as `purchase:INV-000042`. The browser tag sends the same ID, so a platform that receives the sale from both counts it once. Google Analytics 4 and Google Ads get the invoice number itself as the transaction ID. A renewal's ID starts with `renewal:`, a free server's with `lead:` and a sign-up's with `sign_up:`.

Meta, TikTok, Reddit and X have no event for a refund, and Google Ads can take one back only through its own API, which we do not use. Each of those platforms gets a delivery marked `skipped` that says so, so you can see why nothing went.

## What the buyer allowed

We copy the visitor's cookie choice onto the order when it is placed, and what we send depends on it. See [Consent and privacy](/docs/organizations/attribution/#consent-and-privacy) for how the choice is made.

The buyer allowed marketing
: We send what the platform uses to match the sale to the ad: the SHA-256 hash of the email address, the phone number, the name and the address, the SHA-256 hash of the customer ID, the click ID the visit arrived with, the Meta browser IDs (`fbp` and `fbc`), and the IP address and browser name of the order. Reddit gets the IP address hashed.

The buyer denied marketing, or we cannot tell what they chose
: We send nothing that names the buyer: no email, phone, name, address, customer ID, click ID, cookie, IP address or browser name. Meta, TikTok, Reddit and X are not sent the sale at all. Google Analytics 4 and Google Ads receive the count, the value and the invoice number only, and Google Analytics 4 sends no user ID.

The buyer denied analytics
: Google Analytics 4 is not sent anything for that sale, and its client and session IDs are left out.

An invoice that did not begin as a storefront order has no choice on file: an invoice your team made, or a renewal. It uses the choice the customer made when they signed up, and when there is none, everything is denied. Nothing is ever sent as a plain email address or phone number. Meta, TikTok and X are sent the hashes they ask for, and Google Ads receives the hashed email only with marketing allowed.

## Set it up in your dashboard

Owners and admins turn each platform on in the dashboard. Your team can do the same in the staff console, as [Team and settings](/docs/organizations/staff-console/team-and-settings/) describes.

You need the Owner or Admin role, because the page holds platform secrets. A member with another role does not see the page, and its address opens **Overview**. You also need, from each platform's own site, the ID and the secret that [Set up each platform](#set-up-each-platform) names.

1. In the [dashboard](https://www.coritan.com/dashboard/organizations), open the organization, then **Settings**, then **Conversions**.
2. Find the platform's card. It names the platform and shows a badge: **On**, **Test mode**, **Off**, or **Failing** when the last event we sent it was refused. Under the name, **Last event sent** says what we last sent and when, and **Last 30 days** says how many of your first purchases from the last 30 days we sent it.
3. Fill in the platform's settings, such as "Measurement ID" for Google Analytics 4 or "Pixel ID" for Meta. Each hint says where to find the value. Google Ads asks for "Conversion labels" and X for "Event IDs", one for each kind of event. Set at least "Purchase".
4. Type the secret into its field, such as "API secret" or "Access token". X takes four values: "Consumer key", "Consumer secret", "Access token" and "Access token secret". Google Ads and Microsoft Ads take no secret. After you save, the card shows **Saved** where the field was, because we never show a secret again. Select **Replace** to type a new one, or **Clear** to remove it.
5. Turn on **Send events**. Turn on "Send renewals" to send each paid renewal as its own conversion action. Turn on **Test mode** to mark every event as a test while you check the setup. Google Ads, X and Microsoft Ads have no test mode, so their cards have no such switch.
6. Select **Save changes**. Each card saves alone, so what you typed in another card stays unsaved until you save that card. A setting that does not fit, or a platform turned on without what it needs, shows the API's sentence under the input it belongs to, and focus moves there.
7. For a platform with a test mode, select **Send test event**. It sends one made-up purchase with the settings you saved, so it is off while a card has unsaved changes. Meta and TikTok need their "Test event code" first. The card says whether the platform accepted the event. A test is not counted in **Last 30 days**.

When it works, the page says "Meta Conversions API turned on." or "Meta Conversions API saved.", the badge reads **On** and the secret reads **Saved**. After the next paid invoice, **Last event sent** shows the purchase as **Sent**. A **Skipped** event carries the reason, such as a buyer who did not allow marketing.

For Microsoft Ads, turn on **Send events** and select **Save changes**. The page then shows **Copy the file address now** with the whole address of your file. Copy it into a schedule in Microsoft Advertising, as [Send purchases to Microsoft Advertising](#send-purchases-to-microsoft-advertising) describes. We show the address whole only that once. Later it shows with the token hidden, next to **Last fetch**, which says when Microsoft Advertising last read the file. If the address leaks, select **Replace address…**, type `replace` and select **Replace address**. The old address stops working at once, and the page shows the new one whole.

## Set up each platform

Each platform has a `kind`, settings that are not secret (`config`) and, except for Google Ads and Microsoft Ads, one secret. The secret is write only: we seal it, never show it again, and the API answers only `secret_set`.

| Platform | `kind` | `config` | Secret |
| --- | --- | --- | --- |
| Google Analytics 4 | `ga4` | `measurement_id` | The Measurement Protocol API secret: in Google Analytics, open Admin, Data streams, your web stream, then Measurement Protocol API secrets. |
| Google Ads | `google_ads` | `conversion_id` and `labels` | None. In Google Ads, open Goals, Conversions, then each conversion action and its tag setup: the ID starts with `AW-` and the label follows the slash. Make one conversion action for each of `purchase`, `renewal`, `sign_up` and `lead` that you want. |
| Meta | `meta_capi` | `pixel_id`, `test_event_code` | The access token: in Meta Events Manager, open your data source, then Settings and Conversions API. The test event code is on its Test events tab. |
| TikTok | `tiktok_events` | `pixel_code`, `test_event_code` | The access token: in TikTok Events Manager, open your web events, then Settings and Events API. The test event code is on its Test events view. |
| Reddit | `reddit_ads` | `pixel_id` | The conversion access token: in Reddit Ads, open Events Manager and your data source, then Conversions API. `pixel_id` is the Pixel ID there, not the ID of the ad account. |
| X | `x_ads` | `pixel_id` and `events` | Four OAuth values as one object: `consumer_key`, `consumer_secret`, `access_token` and `access_token_secret`, from an app in the X developer portal with Ads API access. `events` names the X event ID, which starts with `tw-`, for each of `purchase`, `renewal`, `sign_up` and `lead`. |
| Microsoft Ads | `microsoft_ads` | `conversion_name`, `lookback_days`, `browser_grace_minutes`, `email_only_rows` | None. We make an address for the file Microsoft Advertising imports. See [Send purchases to Microsoft Advertising](#send-purchases-to-microsoft-advertising). |

Every platform also takes `send_renewals`, `true` or `false`, which is `false` until you set it.

An ID is checked as the browser tags check it, so a value that the [tags](/docs/organizations/tags/) accept is accepted here, and an ID you type in lower case where the platform uses capitals is saved in capitals. A Google Ads `labels` and an X `events` object replace the whole map each time. You turn a platform on with `enabled: true` once its settings and secret are saved.

```bash title="Turn on Google Analytics 4"
curl -X PUT "https://api.coritan.com/api/v1/orgs/acme/conversions/ga4" \
  -H "Authorization: Bearer $CORITAN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled": true, "secret": "AbCdEfGhIjKlMnOpQrStUv", "config": {"measurement_id": "G-ABC123DEF4"}}'
```

```bash title="Turn on Google Ads with a purchase and a lead label"
curl -X PUT "https://api.coritan.com/api/v1/orgs/acme/conversions/google_ads" \
  -H "Authorization: Bearer $CORITAN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled": true, "config": {"conversion_id": "AW-123456789", "labels": {"purchase": "AbCdEfGhIjKlMn", "lead": "OpQrStUvWxYz01"}}}'
```

```bash title="Turn on X with its four credentials"
curl -X PUT "https://api.coritan.com/api/v1/orgs/acme/conversions/x_ads" \
  -H "Authorization: Bearer $CORITAN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled": true, "config": {"pixel_id": "o1abc", "events": {"purchase": "tw-o1abc-o2def"}}, "secret": {"consumer_key": "...", "consumer_secret": "...", "access_token": "...", "access_token_secret": "..."}}'
```

The answer holds the saved settings and `secret_set`, and `changed` lists what this call changed. Send only the keys you want to change: a `config` key replaces the saved one, and `null` or an empty string removes it. An empty `secret` clears it. Saving what is already saved changes nothing and writes no audit entry. A value that does not fit answers `422` with `detail`, the sentence to show, and `field`, the input it belongs to:

```json
{ "detail": "Meta pixel id must be 8 to 20 digits", "field": "config.pixel_id" }
```

## Check that it works

1. Send a test event with `POST /conversions/{kind}/test`. It sends one made-up purchase of one currency unit for a product called `Test product`, with nothing about a person in it. Meta and TikTok send their test event code, Google Analytics 4 sends to its validation endpoint and Reddit sends its test flag, so nothing reaches live reporting. Set `test_event_code` for Meta and TikTok first.
2. Read the answer: `ok` is `true` when the platform accepted the event, and `http_status` and `error` carry what it said when it did not. A test is not logged as a delivery and does not count toward coverage.
3. Look for the event in the platform: Meta and TikTok in their test events, Google Analytics 4 in DebugView.
4. After the next real purchase, read `GET /conversions/deliveries`. A purchase that went out has a delivery with `status` `sent`.

You can turn on `test_mode` for a platform to send every real event as a test. Google Ads and X have no test mode, so a test would count as a real sale: their test call answers `422`, and while either is in test mode nothing is sent to it. Microsoft Ads takes no events, so its test call answers `422` too.

You can send at most ten test events an hour for each platform, and the eleventh answers `429`.

## Read what was sent

`GET /conversions` lists the seven platforms, whether or not you set them up. Each entry has these parts besides its settings:

`last_delivery`
: The newest event the platform was sent, with its `event`, `status`, `http_status`, `error` and the time `at`, or `null`.

`coverage`
: How many of your first purchases the platform was sent. `orders` is the invoices paid in the last 30 days that were a first purchase, `sent` is how many of them the platform's `purchase` was sent for, and `rate` is `sent` over `orders`, or `null` with no orders. A free server, a renewal and an upgrade are in neither number.

`GET /conversions/deliveries` lists every event, newest first. Filter it with `kind`, `status` (`sent`, `failed` or `skipped`) and `invoice_number`, which also finds the refunds and chargebacks booked on that invoice. `limit` takes 1 to 200 (50 by default) and `offset` pages through. Each row names its `invoice`, the `event` and the `status`:

`sent`
: The platform answered with a success.

`skipped`
: We chose not to send, and `error` says why. Common reasons: `Renewals are off for this integration.`, `The buyer did not allow marketing, so nothing is sent to Meta Conversions API.`, `The buyer did not allow analytics, so nothing is sent to Google Analytics.`, `No Google Ads conversion label is set for lead.`, `No X event ID is set for lead.`, `X needs a click ID or an email address to match a conversion, and this buyer gave none.`, and `Meta Conversions API has no refund event.`

`failed`
: The platform refused the event, or did not answer. `http_status` and `error` hold what it said, with your secret taken out. A timeout, a connection failure, `408`, `429` and any `5xx` are tried again, three attempts in all, and the last row ends `Gave up after 3 attempts.` Any other answer is final at once.

A delivery that is `sent` or `skipped` is final, so a sale is never reported to a platform twice by us.

## Send purchases to Microsoft Advertising

Microsoft Advertising does not take events. It fetches a file of offline conversions on a schedule, and we serve the file.

1. In Microsoft Advertising, make an offline conversion goal and note its name.
2. Turn Microsoft Ads on with `PUT /conversions/microsoft_ads` and `{"enabled": true, "config": {"conversion_name": "Offline Purchase"}}`. The first time, the answer's `feed_url` holds the whole address of your file, ending in a `token`. Copy it now: later answers show it with the token hidden in `feed_url_masked`.
3. In Microsoft Advertising, open Conversions, Offline conversions, then Schedules, and add that address as the file to import.

The file lists a purchase when its invoice was paid in the last `lookback_days` (30 unless set), at least `browser_grace_minutes` ago (120 unless set) and after you turned Microsoft Ads on, and when the buyer allowed marketing. It lists first purchases and upgrades, and renewals when `send_renewals` is on. Each row holds the `msclkid` the buyer arrived with, your `conversion_name`, the time paid, the total, the currency and the hashed email. A buyer with no click ID is listed by the email alone when `email_only_rows` is on (the default) and the visit came from Microsoft. In test mode the file lists nothing.

The grace period gives the buyer's browser the first chance to report the sale with the UET tag from your [tags](/docs/organizations/tags/). When the invoice shows paid, your storefront calls `POST /storefront/conversions/microsoft/claim` with the customer's token and `{"invoice_id": 42}`. The answer `{"fire": true}` comes once for each invoice, and the browser then fires the UET event and the file leaves that invoice out. Every other answer is `{"fire": false}`: the file already listed it, the invoice is unpaid or not a sale, or Microsoft Ads is off or in test mode. At most 10 claims an hour for one invoice and 600 an hour from one IP address.

Microsoft Advertising fetches the file with no sign-in: the token in the address is the credential. A wrong token, an organization with the integration off and an unknown organization all answer `404`. Replace a lost or leaked address with `POST /conversions/microsoft_ads/rotate-feed-token`. The answer holds the new address, and the old one answers `404` from then on. Purchases the file already listed stay listed. Rotating before you first turned Microsoft Ads on answers `409`.

## Result

After a customer pays, we queue the purchase in the same transaction that records the payment and send it from a background job. `GET /conversions/deliveries` then lists one row for each platform you turned on, with its status. The invoice number is the same in your platform's reports and in your invoices.

## Troubleshooting

A delivery is `skipped` with `The buyer did not allow marketing`
: Meta, TikTok, Reddit and X are sent a sale only when the buyer allowed marketing. Google Analytics 4 and Google Ads still receive the count. The browser tags follow the same choice.

A delivery is `failed` with `HTTP 400` or `HTTP 401`
: The platform refused the ID or the secret. Check the ID against the platform, make a new access token and save it again. The failure is final for that sale.

Meta or TikTok answers a test with `422` and `Test mode sends a test event code`
: Set `config.test_event_code` from the platform's test events page, then test again.

A Google Ads purchase shows no conversion
: We send Google Ads the click ID only when the buyer allowed marketing and the visit had one. Without it the request carries the invoice number, the value and the currency alone. Check that the `purchase` label belongs to the conversion action you read, and read the delivery's `status` first.

The same sale counts twice
: The browser tag and the server send the same event ID, so Meta, TikTok, Reddit and X count it once. For Google Analytics 4 and Google Ads, make sure the tag sends the invoice number as the transaction ID, which the storefront's tags do.

Microsoft Advertising says the file is not found
: The address needs its token, and a rotated address replaces the old one. Turn Microsoft Ads on, copy `feed_url` from that answer, or rotate to get a new one.

`coverage.rate` is lower than you expect
: Count only first purchases paid in the last 30 days. A purchase whose buyer denied marketing is `skipped` for the platforms that need it, and a platform you turned on after the sale never receives it.

## With the API

The staff console's routes under `/api/v1/orgs/{org_slug}/staff/marketing/conversions` answer the same, for the Admin and Owner roles, and platform staff have the same routes for any organization in the admin console. All of them need an access token, sent as `Authorization: Bearer $CORITAN_TOKEN`.

| Operation | What it does |
| --- | --- |
| `GET /conversions` | The seven platforms with their settings, `secret_set`, `last_delivery` and `coverage`. |
| `PUT /conversions/{kind}` | Turn a platform on or off, set `test_mode`, change its `config`, set or clear its `secret`. |
| `POST /conversions/{kind}/test` | Send one made-up purchase in test mode. |
| `GET /conversions/deliveries` | The delivery log, filtered by `kind`, `status` and `invoice_number`. |
| `POST /conversions/microsoft_ads/rotate-feed-token` | A new address for the Microsoft Ads file. |
| `GET /conversions/microsoft/feed` | The file Microsoft Advertising fetches. No sign-in, a `token` query. |
| `POST /storefront/conversions/microsoft/claim` | Whether the thank-you page should fire the UET event. The customer's token. |

Every change is recorded in the [audit log](/docs/organizations/audit-log/) as `conversions.updated` (the settings that changed, never a secret), `conversions.test_sent` or `conversions.feed_rotated`.

## Related

- [Add analytics and ad tags](/docs/organizations/tags/)
- [How customers and orders are attributed](/docs/organizations/attribution/)
- [Read the organization audit log](/docs/organizations/audit-log/)

## Next steps

- [Add the browser tags](/docs/organizations/tags/) for the same platforms, so the browser and the server count each sale once.
- [Read how customers and orders are attributed](/docs/organizations/attribution/) to see which campaigns bring buyers.
- [Follow your invoices](/docs/organizations/billing/invoices/) to match a delivery to the invoice it came from.

## API

- `GET /api/v1/orgs/{org_slug}/conversions`: List the ad platforms purchases are sent to, and how each is doing (https://www.coritan.com/docs/api/reference/organizations/organizations-members/conversions/#op-get-api-v1-orgs-org-slug-conversions)
- `GET /api/v1/orgs/{org_slug}/conversions/deliveries`: Read what each platform was sent, and why anything was not (https://www.coritan.com/docs/api/reference/organizations/organizations-members/conversions/#op-get-api-v1-orgs-org-slug-conversions-deliveries)
- `PUT /api/v1/orgs/{org_slug}/conversions/{kind}`: Turn an ad platform on or off and change its settings (https://www.coritan.com/docs/api/reference/organizations/organizations-members/conversions/#op-put-api-v1-orgs-org-slug-conversions-kind)
- `POST /api/v1/orgs/{org_slug}/conversions/{kind}/test`: Send a made-up purchase to an ad platform in test mode (https://www.coritan.com/docs/api/reference/organizations/organizations-members/conversions/#op-post-api-v1-orgs-org-slug-conversions-kind-test)
- `POST /api/v1/orgs/{org_slug}/conversions/{kind}/rotate-feed-token`: Replace the token in the Microsoft Ads file address (https://www.coritan.com/docs/api/reference/organizations/organizations-members/conversions/#op-post-api-v1-orgs-org-slug-conversions-kind-rotate-feed-token)
- `GET /api/v1/orgs/{org_slug}/conversions/microsoft/feed`: Fetch the offline conversions file Microsoft Advertising imports (https://www.coritan.com/docs/api/reference/organizations/organizations-members/conversions/#op-get-api-v1-orgs-org-slug-conversions-microsoft-feed)
- `POST /api/v1/orgs/{org_slug}/storefront/conversions/microsoft/claim`: Ask whether the thank-you page should report a purchase to Microsoft Ads (https://www.coritan.com/docs/api/reference/organizations/storefront/storefront/#op-post-api-v1-orgs-org-slug-storefront-conversions-microsoft-claim)
- `GET /api/v1/orgs/{org_slug}/staff/marketing/conversions`: List the ad platforms purchases are sent to, and how each is doing (https://www.coritan.com/docs/api/reference/organizations/org-staff-marketing/staff/#op-get-api-v1-orgs-org-slug-staff-marketing-conversions)
- `GET /api/v1/orgs/{org_slug}/staff/marketing/conversions/deliveries`: Read what each platform was sent, and why anything was not (https://www.coritan.com/docs/api/reference/organizations/org-staff-marketing/staff/#op-get-api-v1-orgs-org-slug-staff-marketing-conversions-deliveries)
- `PUT /api/v1/orgs/{org_slug}/staff/marketing/conversions/{kind}`: Turn an ad platform on or off and change its settings (https://www.coritan.com/docs/api/reference/organizations/org-staff-marketing/staff/#op-put-api-v1-orgs-org-slug-staff-marketing-conversions-kind)
- `POST /api/v1/orgs/{org_slug}/staff/marketing/conversions/{kind}/test`: Send a made-up purchase to an ad platform in test mode (https://www.coritan.com/docs/api/reference/organizations/org-staff-marketing/staff/#op-post-api-v1-orgs-org-slug-staff-marketing-conversions-kind-test)
- `POST /api/v1/orgs/{org_slug}/staff/marketing/conversions/{kind}/rotate-feed-token`: Replace the token in the Microsoft Ads file address (https://www.coritan.com/docs/api/reference/organizations/org-staff-marketing/staff/#op-post-api-v1-orgs-org-slug-staff-marketing-conversions-kind-rotate-feed-token)
