The account's hosted test inbox and the requests it kept, newest first, paged by cursor (see Pagination). The inbox keeps its newest 100, each for at most 7 days, so a walk is a few pages at most. Each entry is exactly what an endpoint would have received for one delivery attempt — every header as signed (X-PS-Signature-V2 among them) and the raw body, byte for byte — so a signature check can be run against it with the secret POST /v1/webhooks/inbox returned. A test key only: the inbox receives test events and nothing else, and a live key is 400 test_mode_only. 404 not_found until the inbox is made.
- Credential
- An API key in
x-api-key - Scopes
webhooks:manage
Parameters
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 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/webhooks/inbox
curl "https://portal.printedsimply.com/api/v1/webhooks/inbox" \
-H "x-api-key: ps_test_YOUR_KEY"Response 200
The inbox subscription (inbox: true) and a page of its kept requests, newest first; meta carries has_more and next_cursor
data
webhookobjectrequiredwebhook.idstringrequiredwebhook.urlstringrequiredwebhook.eventsstring[]requiredwebhook.activebooleanrequiredwebhook.modestringrequiredWhich order mode this subscription accepts. Normalised on the way out, so a stored value this build does not know reads as what it is actually routed as (
all) rather than as the stored string.One of
alltestlivewebhook.created_atstring (date-time)requiredwebhook.secret_rotated_atstring (date-time) | nullwebhook.previous_secret_expires_atstring (date-time) | nullDuring a rotation both secrets verify until this moment. Sign-check against both, or accept the older one, until it passes.
webhook.rotation_pendingbooleanTrue while the previous secret is still inside its window.
webhook.inboxbooleanrequiredTrue for the account's hosted test inbox (POST /v1/webhooks/inbox): it receives test events only and keeps them for GET /v1/webhooks/inbox rather than POSTing them, its
urlis the dashboard page that shows them, and its url and mode cannot be changed.entriesobject[]requiredOne page of the requests the inbox kept, newest first. The inbox keeps its newest 100, each for at most 7 days.
at most 100 entries
entries[].idstringrequiredentries[].eventstringrequiredThe event name, as sent in
X-PS-Event.One 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.pingentries[].event_idstringrequiredThe logical event this attempt delivered: the
X-PS-Event-Idvalue, and the body'sid.entries[].received_atstring (date-time)requiredentries[].headersobjectrequiredEvery header the signer produced for this attempt, name to value: the signature headers, the event, its id, the attempt and the timestamp.
entries[].bodystringrequiredThe raw body, byte for byte as signed — a string, not parsed JSON, so a signature check runs against exactly these bytes.
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": {
"webhook": {
"id": "whk_Pxpo7Z0TfZI3H2ZY7yS2tukP",
"url": "https://portal.printedsimply.com/dashboard/webhooks/inbox",
"events": [
"order.created",
"order.paid",
"order.in_production",
"order.shipped",
"order.delivered",
"order.cancelled",
"order.failed",
"order.updated",
"order.quality_check",
"order.issue",
"order.approval_requested",
"order.approval_approved",
"order.approval_declined",
"order.approval_expired",
"product.updated",
"catalog.synced",
"webhook.disabled"
],
"active": true,
"mode": "test",
"created_at": "2026-10-15T15:00:49.000Z",
"secret_rotated_at": null,
"previous_secret_expires_at": null,
"rotation_pending": false,
"inbox": true
},
"entries": [
{
"id": "ce51qmztk5pep7spdwg9rg58q",
"event": "order.shipped",
"event_id": "evt_6GV9KxH-xHgHOcS13DqpCgDS",
"received_at": "2026-10-15T15:00:49.000Z",
"headers": {
"X-MF-Event": "order.shipped",
"X-PS-Event": "order.shipped",
"Content-Type": "application/json",
"X-MF-Event-Id": "evt_6GV9KxH-xHgHOcS13DqpCgDS",
"X-PS-Event-Id": "evt_6GV9KxH-xHgHOcS13DqpCgDS",
"X-MF-Signature": "sha256=ff059fc56400150adea84edb9f5ce87df993fa67c28c136cbfaae542c4ac05f0",
"X-MF-Timestamp": "1792076449",
"X-PS-Signature": "sha256=ff059fc56400150adea84edb9f5ce87df993fa67c28c136cbfaae542c4ac05f0",
"X-PS-Timestamp": "1792076449",
"X-MF-Signature-V2": "t=1792076449,v1=7e591194144fe43fdd5e55fbaae9a09ef3f297f62c15578829a73d4019c90ac0",
"X-PS-Signature-V2": "t=1792076449,v1=7e591194144fe43fdd5e55fbaae9a09ef3f297f62c15578829a73d4019c90ac0",
"X-MF-Delivery-Attempt": "1",
"X-PS-Delivery-Attempt": "1"
},
"body": "{\"id\":\"evt_6GV9KxH-xHgHOcS13DqpCgDS\",\"data\":{\"order_id\":\"test_ord_tVI4josqx9ZnMqHu-aBSFxsn\",\"test_mode\":true,\"tracking_number\":\"9400111899223197428490\"},\"event\":\"order.shipped\",\"timestamp\":\"2026-10-15T15:00:49.000Z\"}"
}
]
},
"request_id": "4e3753e1-48dd-4313-bfd8-48e4af4e116a",
"meta": {
"limit": 20,
"has_more": false,
"next_cursor": null
}
}Errors
A live key (test_mode_only): the inbox is read with a test key. Or a cursor this list did not issue to this account (validation_error).
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.