One order's API calls, events and webhook delivery attempts in one list, oldest first, as GET /order/{id}/events orders its events. Each entry has a type (request, event or delivery), its own id, the time it happened (at) and one object named after its type. A request is a call that placed this order (POST /order, a reorder, a batch) or changed it: one that named it in its path with any method but GET (a cancel, an address, shipping or item change, a simulated step). A read is not listed, this timeline included: it acts on no order. It carries the method, the operation's path template, the status, request_id, trace_id, the duration, the prefix of the key that made it (never more of the key) and the error code; no request or response body, which the request log does not keep. Its at is when it was answered, so at one instant an event comes first, then a request, then a delivery: the events a call wrote come before it. An event is the order's event exactly as GET /order/{id}/events returns it. A delivery is one attempt at sending a webhook about this order to one of your endpoints: the endpoint's id and the host the attempt was sent to (never its path or query), the event and its event_id (the X-PS-Event-Id that all attempts at one event carry), the rung of the retry ladder, whether it was delivered, the status your endpoint answered, the latency, and when the next attempt is due while one is (none while the endpoint is paused or disabled, which holds its attempts). Never a payload or a signing secret: GET /webhooks/deliveries/{id} returns an attempt's payload. What a credential reads: delivery attempts need webhooks:manage as well, the scope GET /webhooks/deliveries needs, and a connected app reads only the attempts to the endpoints it subscribed. A key created with a list of scopes, or a connected app, reads only the calls it made itself; a key created without one reads the calls of all the account's credentials. A test key lists only calls made with test keys and only sandbox deliveries. Calls and delivery attempts are kept for 90 days, as the request log and the delivery log keep them; events are kept with the order, so an older order's timeline lists its events alone. Calls answered before the request log recorded the order a call reached are listed when their path named this order; a call that placed an order before then is not. Pass type to list one kind only (only the calls, only the deliveries). Pages by cursor (see Pagination): data is a bare array and meta carries has_more and next_cursor. A walk lists what had happened when its first page was read, so it ends whatever is written meanwhile; the next walk lists what was. An order of another account, or a live order asked for with a test key, is 404 order_not_found.
- 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.limitquery · integerEntries per page: 20 by default, 100 at most. A value above 100 is clamped to 100 and one below 1 to 1, a fraction is truncated, and a missing, zero or non-numeric value falls back to 20; none is refused.
Default 20
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.typequery · stringOnly entries of this
type:request(the calls that placed or changed the order),event(its events) ordelivery(the attempts at delivering webhooks about it, which still needwebhooks:manage: without it adeliverytimeline is empty). Leave it out for every kind, merged, as before. Any other value is 400 validation_error. The cursor is bound to it: send the sametype(or none) with every page of a walk; a cursor sent with another is 400other_filters.One of
requesteventdelivery
Request
GET /api/v1/order/{id}/timeline
curl "https://portal.printedsimply.com/api/v1/order/test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk/timeline" \
-H "x-api-key: ps_test_YOUR_KEY"Response 200
The order's requests, events and delivery attempts, oldest first. data is a bare array, as on GET /order/{id}/events; has_more and next_cursor live in meta.
data is a list; each entry:
typestringrequiredAlways
requestidstringrequiredThe request log row's id.
atstring (date-time)requiredWhen the call was answered.
requestobjectrequest.methodstringrequiredThe HTTP method the call was made with.
request.pathstringrequiredThe operation's path template, such as
/v1/order/{id}/events: the ids the call named are not repeated.request.statusintegerrequiredThe HTTP status the call was answered with.
request.request_idstring | nullrequiredThe
X-Request-Id(andrequest_id) the call was answered with: what the dashboard's request log finds it by.request.trace_idstring | nullrequiredThe W3C trace id the response's
traceparentcarried; null on calls logged before traces were kept.request.duration_msintegerrequiredHow long the call took to answer, in milliseconds.
at least 0
request.key_prefixstring | nullrequiredThe prefix of the API key that made the call, as the keys page shows it. Never more of the key; null for a call a connected app made.
request.oauth_client_idstring | nullrequiredThe connected app that made the call, for a call made with its access token; null for a key.
request.modestring | nullrequiredWhether a test or a live credential made the call.
One of
testliverequest.error_codestring | nullrequiredThe error's
code, for a call answered with a status of 400 or more.eventobjectevent.actionstringrequiredevent.detailsstring | nullrequiredevent.timestampstring (date-time)requireddeliveryobjectdelivery.webhook_idstringrequiredThe endpoint the attempt was sent to.
delivery.endpoint_hoststring | nullrequiredThe host of the URL the attempt was sent to, as it was then (an endpoint's URL can change since); never its path or query. Null on attempts logged before the log kept it.
delivery.eventstringrequiredOne of
order.createdorder.paidorder.in_productionorder.shippedorder.deliveredorder.cancelledorder.failedorder.updatedorder.reroutedorder.quality_checkorder.issueorder.approval_requestedorder.approval_approvedorder.approval_declinedorder.approval_expiredcampaign.openedcampaign.order_addedcampaign.closedcampaign.batch_in_productioncampaign.completeddesign.proof_sentdesign.changes_requesteddesign.approvedreturn.requestedreturn.approvedreturn.remake_createdreturn.resolvedreturn.rejecteddrop.openeddrop.saledrop.closeddrop.sold_outcheckout_link.salegift.claimedgift.expiredproduct.updatedcatalog.syncedwebhook.disabledautopilot.run.createdautopilot.run.awaiting_approvalautopilot.run.completedautopilot.run.failedautopilot.run.cancelleddesign_run.item_readydesign_run.completedguarantee.issuedguarantee.honoredguarantee.breachedguarantee.remediedtest.pingdelivery.event_idstring | nullrequiredThe
X-PS-Event-Idthe attempt carried. Every attempt at one event shares it; null only on attempts logged before the log carried it.delivery.attemptinteger | nullrequiredThe rung of the retry ladder (
X-PS-Delivery-Attempt), 1 for the first send and at most 7; null for a redelivery you asked for, and on attempts logged before the log kept it.1 to 7
delivery.outcomestringrequireddeliveredwhen your endpoint answered 2xx.One of
deliveredfaileddelivery.status_codeinteger | nullrequiredThe status your endpoint answered; null when nothing came back.
delivery.failure_causestring | nullrequiredWhy a failed attempt failed, from the delivery log's causes; null when delivered, and on a failure logged with neither a cause nor a status.
One of
dnsunsafe_urlrefusedtimeouttlsresetnetworkredirectunauthorizedhttp_4xxhttp_5xxinbox_refusedegresssigninginternaldelivery.latency_msinteger | nullrequiredMilliseconds from sending to the answer or the failure; null when nothing was sent, and on attempts logged before the log kept it.
at least 0
delivery.next_retry_atstring (date-time) | nullrequiredWhen the next attempt at this event is due, on the latest failed attempt of a retry ladder still running to an active endpoint; null otherwise, and while the endpoint is paused or disabled (its attempts are held until it is resumed).
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": [
{
"type": "event",
"id": "c57yvwpkmchhnkd7fa6p20ued",
"at": "2026-10-15T15:00:47.000Z",
"event": {
"action": "artwork_checked",
"details": "The artwork prints well (1200×1200 px, 400 DPI at 3×3 in).",
"timestamp": "2026-10-15T15:00:47.000Z"
}
},
{
"type": "event",
"id": "cfbgb3yt0b96cqzl7lijwdpe0",
"at": "2026-10-15T15:00:47.000Z",
"event": {
"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:47.000Z"
}
},
{
"type": "request",
"id": "c64ztkjlkujdlmlc4sviofcv6",
"at": "2026-10-15T15:00:47.000Z",
"request": {
"method": "POST",
"path": "/v1/order",
"status": 200,
"request_id": "a40f913f-9b81-49ea-8d9c-c03bd20ad9b2",
"trace_id": "4e2da63b674690074ebd7d6a9629312d",
"duration_ms": 0,
"key_prefix": "mf_test_xxxxxxxx",
"oauth_client_id": null,
"mode": "test",
"error_code": null
}
},
{
"type": "delivery",
"id": "c2g0xl6j1z0c35hx7fd355s1c",
"at": "2026-10-15T15:00:47.000Z",
"delivery": {
"webhook_id": "whk_4czoxnKiV1lGoLIP_yodB3xT",
"endpoint_host": "example.com",
"event": "order.created",
"event_id": "evt_nyPRpD32bgnJc6D8T0TE6Y89",
"attempt": 1,
"outcome": "delivered",
"status_code": 200,
"failure_cause": null,
"latency_ms": 0,
"next_retry_at": null
}
}
],
"request_id": "4e3753e1-48dd-4313-bfd8-48e4af4e116a",
"meta": {
"limit": 20,
"has_more": false,
"next_cursor": null
}
}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.