Public API: Orders

The orders API is a read-only diagnostics view over the purchases your snippet records. Use it to confirm purchase tracking works. It gives you totals, a searchable order list, and a feed of the most recent orders, straight from the analytics store.

All orders endpoints live under a project and authenticate with a service token. The org comes from the token, so the path carries only {projectId}, not {orgId}.

Plain text
https://app.avsb.cloud/api/v1/projects/{projectId}/orders
Plain text1 line

Every response here follows the shared conventions: the { data } envelope and the standard error shape. Every /api/v1 token is rate-limited the same way everywhere: a scoped token gets 600 reads and 120 writes per minute, and an admin:* token gets 600 requests per minute, reads and writes together. Both endpoints below also need two plan features at once: Integrations and API access, and Commerce. Missing either one fails the call with 403 feature_disabled, even with a fully-permissioned token. See rate-limit headers and Refusals at 403.

Scopes

A scope is a named permission on your token. It decides exactly what that token is allowed to read or change.

OperationScope
List orders, get the orders summaryorders:read
Info

Orders are read-only on the public API: there is no orders:write. Purchases are recorded by the snippet's track calls, not written through this API.

How duplicate orders are handled

A vs B stores one row per (projectId, orderId) pair. Your store might send the same orderId twice: a retried request, a page reload, or a refund update. When that happens, the newer row wins. Whichever write carries the latest updatedAt value is what both endpoints below report. This is last-write-wins deduplication, not an average or a sum of the two writes.

See Revenue Deduplication and Refunds for how refunds and net-versus-gross revenue work on top of this.

Get the orders summary

GET /api/v1/projects/{projectId}/orders/summary: totals across the project's orders, a per-currency breakdown, and the 50 most recent orders.

Shell
curl https://app.avsb.cloud/api/v1/projects/<projectId>/orders/summary \  -H "Authorization: Bearer avsb_svc_..."
Shell2 lines

Add from and to to look at one date range instead of all time. Both are calendar days in YYYY-MM-DD format, and you must send both together:

curl "https://app.avsb.cloud/api/v1/projects/<projectId>/orders/summary?from=2026-06-01&to=2026-06-30" \  -H "Authorization: Bearer avsb_svc_..."
Shell2 lines
JSON
{  "data": {    "totals": {      "orderCount": 1284,      "grossMinor": 9817500,      "netMinor": 9512300,      "currencyMismatchCount": 3,      "currency": "USD"    },    "totalsByCurrency": [      { "currency": "USD", "orderCount": 1280, "grossMinor": 9805100, "netMinor": 9499900 },      { "currency": "EUR", "orderCount": 3, "grossMinor": 27400, "netMinor": 27400 }    ],    "unattributedCount": 42,    "testCount": 5,    "computedAt": "2026-06-30T23:59:59.000Z",    "recent": [      {        "orderId": "ord_abc",        "visitorId": "vis_1",        "source": "snippet",        "status": "placed",        "totalMinor": 5000,        "refundedMinor": 0,        "currency": "USD",        "currencyMismatch": false,        "itemCount": 2,        "timestamp": "2026-06-18T10:00:00Z"      }    ]  }}
JSON32 lines

Field notes

  • *Minor amounts are in the currency's minor unit (for example cents): 5000 means $50.00 for USD. Divide by the currency's exponent to display.
  • totals.currency is the project's own configured currency, not any one order's currency. totals covers only orders placed in that currency, with currencyMismatch rows excluded. totalsByCurrency covers every non-test order instead, grouped by whatever currency each order was actually placed in (each recent row also carries its own currency). A mismatched order still counts in its own currency's row there. So the two do not have to add up to the same total.
  • currencyMismatchCount tells you how many orders totals skipped because their currency does not match the project's, so the top-line numbers stay trustworthy.
  • unattributedCount is how many orders in the window arrived with no visitor id, so they cannot be credited to an experiment.
  • testCount is how many synthetic test orders (created from the dashboard's "Send test order" button) fell in the window. They never count toward any total.
  • computedAt is when this snapshot was taken. Two calls a second apart can return slightly different numbers if new orders arrived in between.
  • netMinor is gross minus refunds.
  • recent always holds the 50 newest orders in the window. It has no query parameter of its own to change that count.
Your retention window can narrow the date range you asked for

Order data is kept for up to two years, but you can only read part of that window: 90 days back without billing details on file, 365 days with them (see Data retention). Ask for a wider from/to range than that and the summary silently uses the narrower window instead. The response does not say which window it actually used, so if your numbers look smaller than expected, that is the first thing to check.

Error response

Requesting this endpoint without the Commerce plan feature enabled, even with a correctly-scoped token, answers 403:

403, the org's plan does not include Commerce
{  "error": {    "code": "feature_disabled",    "message": "This feature is not included in your current plan. Upgrade to unlock it.",    "details": { "kind": "plan", "feature": "commerce" }  }}
JSON7 lines

List orders

GET /api/v1/projects/{projectId}/orders: every order, newest first, cursor-paginated. The summary answers "is purchase tracking working"; the list answers "where did this order go".

curl "https://app.avsb.cloud/api/v1/projects/<projectId>/orders?limit=20" \  -H "Authorization: Bearer avsb_svc_..."
Shell2 lines
JSON
{  "data": [    {      "orderId": "ord_abc",      "visitorId": "vis_1",      "source": "snippet",      "status": "placed",      "totalMinor": 5000,      "refundedMinor": 0,      "currency": "USD",      "currencyMismatch": false,      "test": false,      "itemCount": 2,      "timestamp": "2026-06-18 10:00:00.000"    }  ],  "page": { "nextCursor": null, "hasMore": false }}
JSON18 lines

Query parameters

ParameterNotes
limit1 to 100, default 20. A bad value answers 400 pagination_invalid.
cursorThe opaque page.nextCursor from the previous response. A malformed cursor also answers 400 pagination_invalid.
from, toCalendar days as YYYY-MM-DD. Send both or neither; from must not be after to.
qMatches an order-id prefix or an exact visitor id, up to 200 characters.
attributedfalse narrows to orders with no visitor attached (they arrived without a visitor id).
includeTesttrue keeps synthetic test orders in the results. They are excluded by default.

limit and cursor are the only two parameters that fail as pagination_invalid. Everything else that is malformed, meaning from/to/q/attributed/includeTest, answers 400 validation_failed instead of being silently ignored, so a mistyped filter never quietly widens your results.

Field notes

  • test marks a synthetic test order. Test orders are excluded from every revenue total, which is why the list has to say which rows are test rows.
  • visitorId is an empty string when the order arrived without one. Those orders cannot be credited to an experiment.
  • timestamp is the analytics-store format (YYYY-MM-DD HH:MM:SS.mmm, UTC), not ISO-8601. It is the value the cursor is built from.
  • source is one of snippet, datalayer, server, or shopify, depending on how the order was recorded.
  • status is one of placed, partially_refunded, or refunded.

Error response

A limit outside 1 to 100 answers 400, distinct from the general validation error:

400, limit is out of range
{  "error": {    "code": "pagination_invalid",    "message": "limit must be an integer between 1 and 100"  }}
JSON6 lines

Next steps

Was this helpful?