Retrieve the event history for an order including status changes and tracking updates, oldest first. Pages by cursor (see Pagination): data stays a bare array, and meta carries has_more, next_cursor and the full event count (total). A request without cursor answers up to 500 events (1,000 with limit), which is nearly every order's whole history in one answer. To follow an order without a webhook address: keep meta.since from each answer and send it back as since with wait_seconds (at most 25). The answer comes as soon as the order has an event after since; when it has none yet, the request waits for one, up to wait_seconds, and then answers with an empty data and the same meta.since, so the next request asks from the same place. One request, counted once by the rate limit.
- Credential
- An API key in
x-api-key - Scopes
orders:read
Parameters
idpath · stringrequiredThe order's
order_id, or itsportal_order_id(ord_…): either resolves the same order. An id the order answered to before it was re-routed resolves too.cursorquery · stringOpaque cursor: send back
meta.next_cursorfrom the previous page for the page after it; omit it for the first page. Good only on this list, with a key of the same account and mode (test or live), and only for this order, and with the same filters (onlylimitmay change); an altered or foreign cursor, or one sent with other filters, is refused with 400 validation_error. Wins overpagewhen both are sent.limitquery · integerEvents per page. Without
cursor: 500 by default and 1000 at most, which is nearly every order's whole history in one answer — the size this list's page walk always had, so a first answer andpageagree on it. Withcursor: 20 by default and 100 at most, as on every list. Either way a value above the ceiling is clamped to it and one below 1 to 1, a fraction is truncated, and a missing, zero or non-numeric value takes the default; none is refused.Default 500
pagequery · integerdeprecatedDeprecated: walk with
cursor. The page number of the offset walk this operation always had, answered as before (offset, a row count, is accepted in its place), at the page size a request withoutcursortakes. Ignored whencursororsinceis sent.Default 1
sincequery · stringThe position to read on from:
meta.sincefrom an earlier answer for this order (ameta.next_cursoris accepted too), sent back unchanged. Only events after it are returned, oldest first, at a cursor page's size (limit20 by default, 100 at most). Good only for this order, with a key of the same account and mode; an altered or foreign value is refused with 400 validation_error anddetails.parameter: "since", and so is a request that sendscursoras well. Omit it to read from the first event.wait_secondsquery · integerHow long to wait for an event when there is none to return yet (a long poll). 0, the default, answers at once. Otherwise the request answers as soon as an event is found — the order's events are re-read every 2 seconds — or after this many seconds with an empty
data. A value above 25 is clamped to 25, a fraction is truncated, and a missing, negative or non-numeric value is 0; none is refused.Default 0
Request
GET /api/v1/order/{id}/events
curl "https://portal.printedsimply.com/api/v1/order/test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk/events" \
-H "x-api-key: ps_test_YOUR_KEY"Response 200
The order's history, oldest first. data is a BARE ARRAY — the count, has_more and next_cursor live in meta, because there is nowhere in the body to hang them without breaking every existing client.
data is a list; each entry:
actionstringrequireddetailsstring | nullrequiredtimestampstring (date-time)required
meta
pageintegerdeprecatedThe page number of the deprecated offset walk, on the lists that published it (orders and an order's events, on every request without
cursor; products, on a request that sentpage). Absent on a cursor walk, where no page number would be true.totalintegerEvery row matching the request's filters, on the lists that count them (orders, an order's events, assets and products).
limitintegerThe page size applied, which is not necessarily the one asked for: a larger
limitis clamped. Absent only on a GET /products request withoutcursorthat sent nolimit, which answers every match.pagesintegerdeprecatedtotaloverlimit, rounded up, besidepage(orders and an order's events). Absent on a cursor walk.next_cursorstring | nullrequiredSend it back as
cursorfor the next page; null exactly whenhas_moreis false. Opaque: pass it back unchanged. Good only on the list that returned it, with a key of the same account and mode (test or live), and with the same filters.has_morebooleanrequiredWhether another page follows this one. Decided from one row past this page, never from a page that came back short — so a page after
has_more: truecan come back empty only if rows were deleted in between, and the walk still ends at the firsthas_more: false.sincestring | nullOn GET /order/{id}/events only: where to ask from next — the position of the last event in this answer, or, when it has none, the position the request read from. Send it back as
since(withwait_secondsto wait for the next event). Unlikenext_cursorit is not null at the end of the history; it is null only when the order has no events and the request named no position. Opaque, and bound like a cursor to this order, account and key mode.
{
"status": "success",
"data": [
{
"action": "order_submitted",
"details": "A test order: nothing is charged, printed or sent. The total is what a live order would pay.",
"timestamp": "2026-10-15T15:00:10.000Z"
},
{
"action": "artwork_checked",
"details": "The artwork prints well (1200×1200 px, 400 DPI at 3×3 in).",
"timestamp": "2026-10-15T15:00:10.000Z"
}
],
"request_id": "4e3753e1-48dd-4313-bfd8-48e4af4e116a",
"meta": {
"page": 1,
"pages": 1,
"limit": 500,
"total": 2,
"has_more": false,
"next_cursor": null,
"since": "eyJyIjoib3JkZXJfZXZlbnRzIiwiayI6WyIyMDI2LTEwLTE1VDE1OjAwOjEwLjAwMFoiLCJjdnEwNHlsNjQ5bzd0M283YWJpa3Y5NzE0Il19.9452219bf0c9044af2bbb15342e6bd3d"
}
}Errors
Invalid request body or parameters. When the request failed the operation's schema,
detailscarriesformErrors(problems with the request as a whole),fieldErrors(messages keyed by top-level field) anderrors: one{ pointer, code, message }per refused value,pointeran RFC 6901 JSON pointer into the body (or into the object of query parameters, for a query parameter) —/items/0/sku, with~written~0and/written~1, and""for the request as a whole. POST /v1/orders/bulk answerserrorsas one{ index, details }per refused order, eachdetailswith its own pointers from the request body (/orders/3/items/0/sku); POST /v1/orders/multi-ship keepserrorsas the{ formErrors, fieldErrors }it always published; POST /v1/mockups/generate and POST /v1/products/recommend keep their fields at the top ofdetails, witherrorsbeside them. A refusal of a rule the schema cannot state (an asset that does not exist, an expiry too soon) names it inmessageand carries noerrors.Missing, invalid or expired API key:
missing_api_key(none sent),invalid_api_key(unknown or revoked),key_expired(past the expiry set when it was created; create a new key).404Resource not found
Rate limit exceeded. Two windows can produce this status and
error.codesays which:rate_limit_exceededfor the per-minute sliding window,daily_quota_exceededfor the plan's daily quota. Both carryRetry-After; the daily one also carriesX-Daily-LimitandX-Daily-Remaining. The per-minute limits are plan-based — see Rate Limits above, or call GET /v1/me for this credential's own figures.