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#

Warning.there is none. The endpoint is open, CORS is 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#

bash
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"
  }'
ParameterTypeDescription
tracking_coderequiredstringIdentifies the campaign. Trimmed before lookup. An unknown code is a 404.
order_valuenumber | stringRequired 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.
currencystringISO-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_idstringThe 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_idstringYour 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 returns duplicate: 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.
Warning.with neither field, there is no deduplication at all. Every call inserts a row. A retry loop or a double-rendered thank-you page will inflate the count, and nothing in the response tells you it happened.

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.

200 — recorded
{ "ok": true }
200 — already recorded, nothing changed
{ "ok": true, "duplicate": true }
ParameterTypeDescription
400 bad jsonerrorThe body did not parse as JSON.
400 invaliderrortracking_code was missing or empty after trimming.
400 value requirederrorA sale campaign with a missing, zero, negative, non-numeric, or over-1,000,000 order_value. All five cases share one message.
400 bad currencyerrorA currency was supplied but is not a recognised ISO-4217 code.
404 unknown campaignerrorNo campaign has that tracking code. Note this confirms whether a code exists — the endpoint is public, and so are the codes.
429 rate_limitederrorOver the per-IP limit. Carries a Retry-After header in seconds. See Rate limits.
500 dberrorThe 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.