CSV export

GET /api/export/conversions — the eleven columns, what scopes a download, and why refunds appear as their own rows.

GET /api/export/conversions returns every conversion for the client you are currently working in, newest first, as a CSV download.

Authentication and scope#

Warning.this is not an API-key endpoint. It authenticates with your browser session cookie, so it works from a logged-in browser and not from a script or a cron job. There is currently no token-based way to pull this data programmatically.

Two things scope a download, and the second matters if you run more than one client:

  • Your session. No session is a 401.
  • The active client. Not every business you own — the one currently selected. An agency with fifteen clients gets one client's conversions per download, which is what stops fifteen clients' revenue ending up in a file handed to one merchant. No active client is a 409.

Parameters#

ParameterTypeDescription
codestringOptional. A campaign's tracking_code, limiting the export to that campaign. Omit it for every campaign belonging to the active client. An unrecognised code is not an error — it matches no campaign and you get a file containing only the header row.

Format#

text/csv; charset=utf-8, sent as an attachment with Cache-Control: no-store. Rows are separated by CRLF, and any cell containing a comma, quote or newline is quoted with doubled inner quotes, per RFC 4180 — campaign names with commas in them are safe.

The filename is proof-conversions.csv, or proof-<code>-conversions.csv when filtered.

Note.the proof- prefix is a leftover from the product's previous name and has not been renamed. It is cosmetic, but if you have a script matching on the filename, match on the suffix.
proof-a7f3k2-conversions.csv
date_utc,campaign,creator,goal_type,kind,source,order_value,currency,external_order_id,reverses_external_order_id,tracking_code
2026-09-14T11:02:44.120Z,Autumn push,Maya Okonkwo,sale,reversal,webhook,-20.00,EUR,shopify_refund:9876543210,5123456789012,a7f3k2
2026-09-12T09:41:03.880Z,Autumn push,Maya Okonkwo,sale,sale,webhook,49.90,EUR,5123456789012,,a7f3k2
2026-09-11T18:20:15.441Z,Autumn push,Maya Okonkwo,sale,sale,pixel,32.00,GBP,pixel:order-1234,,a7f3k2

Columns#

ParameterTypeDescription
date_utcISO 8601When the conversion was recorded, in UTC — not the business's timezone, which is what the dashboard and reports display. A late evening sale can appear on the previous day here.
campaignstringCampaign name at time of export.
creatorstringCreator name at time of export.
goal_typesale | signup | leadThe campaign's goal. Determines whether order_value is meaningful.
kindsale | reversalStated explicitly so a spreadsheet reader never has to infer “refund” from a minus sign. Blank values are treated as sale.
sourcewebhook | pixel | manualHow the conversion arrived. A fourth value, code, exists in the schema but is never written by any current code path — discount-code attribution arrives through the Shopify webhook and is recorded as webhook. Do not write a parser that expects it, but do not assume it can never appear either.
order_valuenumberEmpty for signup and lead campaigns, which have no revenue. Negative on reversal rows.
currencyISO-4217Per row. Not converted — see below.
external_order_idstringThe order id, carrying its source prefix: pixel:<your-id> from the conversion endpoint, a bare numeric id from Shopify orders, and shopify_refund:<id> on reversals.
reverses_external_order_idstringOn a reversal row, the external_order_id of the sale it reverses — so the file is self-contained and you never need to join on an internal id. Empty on sale rows.
tracking_codestringThe campaign's tracking code.

Refunds are rows, not adjustments#

A refunded order appears twice: the original sale, and a reversal with a negative value. To get net revenue, sum order_value — the signs do the work. To count sales, filter to kind = sale, because a reversal is the undoing of a conversion, not a conversion of its own.

Currencies are not converted#

Each row carries the currency the shopper actually paid in. Summing order_value across mixed currencies produces a meaningless number. No exchange rate is applied anywhere in Yonto, deliberately — a rate would have to be picked for a date, and the result would look authoritative while being an estimate. Group by currency first.

Limits#

Warning.there is no pagination, date filter, or row cap. The export returns every conversion for the client in one response, built in memory. That is fine at thousands of rows and will get slow somewhere past that — use ?code= to narrow it if a download starts taking a long time.

The endpoint is not rate limited, unlike the public ones in Rate limits — it requires a session, so there is no anonymous traffic to throttle.