Conversion endpoint
POST /api/conversion — request fields, every error response, how duplicates are collapsed, and the currency rules.
POST /api/conversion is where the pixel sends a conversion, and it is a plain JSON endpoint you can call from anywhere — a server, a thank-you page, a Zapier step. p.js is a convenience wrapper around it, not a requirement.
Authentication#
Access-Control-Allow-Origin: *, and the only credential is the campaign's tracking code — which is public, because it appears in every tracked link. Anyone who has seen a link can post a conversion to that campaign.This is a deliberate trade for a pixel that has to run in a browser, and the exposure is bounded: a forged conversion can inflate a creator's numbers, but it cannot read anything, reach another business's data, or change an existing record. The Shopify webhook is HMAC-verified and is the trustworthy path where you have the option.
Request#
curl -X POST https://yontosales.com/api/conversion \
-H 'Content-Type: application/json' \
-d '{
"tracking_code": "YOUR_TRACKING_CODE",
"order_value": 49.90,
"currency": "EUR",
"click_id": "the-ref-from-the-tracked-link",
"external_order_id": "order-1234"
}'| Parameter | Type | Description |
|---|---|---|
| tracking_coderequired | string | Identifies the campaign. Trimmed before lookup. An unknown code is a 404. |
| order_value | number | string | Required for sale campaigns; must be > 0 and ≤ 1,000,000. A string is parsed with parseFloat. For signup and lead campaigns it is discarded — see below. |
| currency | string | ISO-4217, case-insensitive, whitespace trimmed. Omitted or empty falls back to the campaign's currency. Anything that is not three letters — or is three letters but not a real code — is a 400, not a silent fallback. |
| click_id | string | The ref value the tracked link appended. Optional: without it the conversion still attributes to the campaign, it just cannot be tied to a specific click. Also enables the 2-minute duplicate window. |
| external_order_id | string | Your order id. Stored prefixed with pixel: so it can never collide with a Shopify order id, so order-1234 appears in exports as pixel:order-1234. |
Value is dropped for signup and lead campaigns#
The campaign's goal_type decides, not the request. On a signup or lead campaign order_value is set to null regardless of what you sent — no error, no warning, the field is simply not stored. A signup has no revenue, and cost-per-signup is computed from the creator fee instead.
Currency is never invented#
The campaign's currency is a fallback for callers that send none, never an override. Sending "US$" or "pounds" is rejected rather than quietly replaced, because a caller doing that has a bug, and substituting the campaign's currency is how a €90 order gets recorded as £90 — a 15% overstatement invented at the point of entry that every downstream total then trusts.
Deduplication#
There are two mechanisms, and which one you get depends on what you send.
- With
external_order_id— a database uniqueness constraint. Any repeat of the same id for the same business returnsduplicate: true, forever. This is the one you want. - Without it, but with
click_id— a 120-second window. A second pixel conversion for the same campaign and click inside two minutes is treated as a double fire and dropped. Outside that window it is counted as a second conversion.
Responses#
Every response is JSON with CORS headers. Note that a duplicate is reported as 200 with ok: true — it is a success, not an error, because the conversion you are reporting is genuinely recorded.
{ "ok": true }{ "ok": true, "duplicate": true }| Parameter | Type | Description |
|---|---|---|
| 400 bad json | error | The body did not parse as JSON. |
| 400 invalid | error | tracking_code was missing or empty after trimming. |
| 400 value required | error | A sale campaign with a missing, zero, negative, non-numeric, or over-1,000,000 order_value. All five cases share one message. |
| 400 bad currency | error | A currency was supplied but is not a recognised ISO-4217 code. |
| 404 unknown campaign | error | No campaign has that tracking code. Note this confirms whether a code exists — the endpoint is public, and so are the codes. |
| 429 rate_limited | error | Over the per-IP limit. Carries a Retry-After header in seconds. See Rate limits. |
| 500 db | error | The insert failed for a reason other than a duplicate. Safe to retry. |
CORS preflight#
OPTIONS /api/conversion returns 204 with Allow-Methods: POST, OPTIONS and Allow-Headers: Content-Type. Because only Content-Type is allowed, adding any custom header to your request will fail preflight.
What it records#
One row with source: "pixel" and kind: "sale". There is no way to post a refund or reversal through this endpoint — reversals arrive only via the Shopify refund webhook. On a custom site, a refunded order stays counted.