The same events your webhooks receive, pulled instead of pushed — for a client with no public URL to POST to (an agent, a notebook, a job behind NAT), and as the catch-up path when your endpoint was down longer than the retry ladder. event_id equals the X-PS-Event-Id header on a delivered copy and the id inside the webhook body, so a receiver that both subscribes and polls dedupes on it. data is the webhook body's data verbatim. Newest first, paged by cursor (see Pagination); next_cursor is also beside the rows, where this list has always carried it. A test key sees test events, a live key sees live ones, and account-level events (catalog syncs, design proofs) are returned to both.
- Credential
- An API key in
x-api-key - Scopes
events:read
Parameters
eventquery · stringOne event name, e.g.
order.shipped. An unknown name is refused rather than ignored. The list is the events this portal offers; a name outside it that an event in your history carries (one received before the switch) is still accepted.One of
order.createdorder.paidorder.in_productionorder.shippedorder.deliveredorder.cancelledorder.failedorder.updatedorder.quality_checkorder.issueorder.approval_requestedorder.approval_approvedorder.approval_declinedorder.approval_expiredproduct.updatedcatalog.syncedwebhook.disabledtest.pingfromquery · string (date-time)toquery · string (date-time)limitquery · integerEvents 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 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.
Request
GET /api/v1/events
curl "https://portal.printedsimply.com/api/v1/events" \
-H "x-api-key: ps_test_YOUR_KEY"Response 200
events (id, event, event_id, test_mode, created_at, data) + next_cursor + retention_days; meta carries has_more and next_cursor
data
eventsobject[]events[].idstringrequiredevents[].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.remediedevents[].event_idstring | nullevents[].test_modebooleanrequiredevents[].created_atstring (date-time)requiredevents[].dataanyThe event's payload, verbatim. Null when none was recorded.
next_cursorstring | nullThe same as
meta.next_cursor: send it back ascursorfor the next page. Null on the last.retention_daysintegerHow long an event stays readable here.
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": {
"events": [
{
"id": "c3sx7f4gaslmi2lt9d4gu6cca",
"event": "order.cancelled",
"event_id": "evt_F5S_u7olBRx4IBTVZNXoH-8d",
"test_mode": true,
"created_at": "2026-10-15T15:00:21.000Z",
"data": {
"status": "cancelled",
"event_id": "evt_PA21dQPJmlEc0sS9bhCLg5l_",
"order_id": "test_ord_JyZLq7sUxTZxc7qjzI76ZQYU",
"provider": "printedsimply",
"test_mode": true,
"portal_order_id": "ord_JyZLq7sUxTZxc7qjzI76ZQYU"
}
},
{
"id": "cpxetfsy9xdfbm20z1s61pn2j",
"event": "order.updated",
"event_id": "evt_T6LdoYs-r7UK7oBoe6PVn6dG",
"test_mode": true,
"created_at": "2026-10-15T15:00:17.000Z",
"data": {
"changes": {
"action": "address_changed",
"ship_to": {
"zip": "78702",
"city": "Austin",
"name": "Jane Doe",
"state": "TX",
"country": "US",
"address1": "456 Oak Ave"
},
"total_cents": 7799,
"total_delta_cents": 0
},
"event_id": "evt_ORmnOh7KCgTFDahmHVzDhdg9",
"order_id": "test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk",
"test_mode": true,
"portal_order_id": "ord_wVK9ZiG-VYiPrlSeBUBmrUVk"
}
},
{
"id": "cypad8vwky282y6c5jaqkkpgx",
"event": "order.updated",
"event_id": "evt_Ze8JBOtDDevHmgQRbq6jY6TC",
"test_mode": true,
"created_at": "2026-10-15T15:00:14.000Z",
"data": {
"changes": {
"item": {
"id": "itm_vNx36IWd8kx2dCQqkFbsTVg3",
"sku": "magnet-4x4",
"quantity": 5
},
"action": "item_updated",
"total_cents": 9034,
"total_delta_cents": -5640
},
"event_id": "evt_gwqAUgOZe7itGr-n6zvxYDi0",
"order_id": "test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk",
"test_mode": true,
"portal_order_id": "ord_wVK9ZiG-VYiPrlSeBUBmrUVk"
}
},
{
"id": "cxii3vd3gq0upj60blxhlx76a",
"event": "order.created",
"event_id": "evt_pfvUVLY9cBbI0i5Mv1kauVL-",
"test_mode": true,
"created_at": "2026-10-15T15:00:10.000Z",
"data": {
"items": [
{
"sku": "sticker-die-cut-3x3",
"quantity": 100
}
],
"status": "paid",
"order_id": "test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk",
"provider": "merchfarm",
"test_mode": true,
"created_at": "2026-10-15T15:00:10.000Z",
"total_cents": 7299,
"portal_order_id": "ord_wVK9ZiG-VYiPrlSeBUBmrUVk"
}
},
{
"id": "c956sb2eftr8jj7tu4m0dw6h5",
"event": "order.paid",
"event_id": "evt_NPH58Tcs_h19qfEXVf9xMang",
"test_mode": true,
"created_at": "2026-10-15T15:00:10.000Z",
"data": {
"status": "paid",
"order_id": "test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk",
"provider": "merchfarm",
"test_mode": true,
"amount_cents": 7299,
"portal_order_id": "ord_wVK9ZiG-VYiPrlSeBUBmrUVk"
}
}
],
"next_cursor": null,
"retention_days": 30
},
"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).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.