Test keys only. Deterministically advances a sandbox order to in_production, shipped, delivered, or cancelled, firing the same signed webhooks the timed sandbox sweep would — so integration tests can drive the full lifecycle without waiting on the clock. issue_flagged rehearses the unhappy path: the order takes the status and the issue_reason you name, as a live order does when the facility flags it, and order.issue is enqueued carrying the same issue_reason. Omit issue_reason to rehearse a flag the facility gave no reason for. A flag stops the sandbox clock; simulate a later state to move the order on, which clears it.
- Credential
- An API key in
x-api-key - Scopes
orders:write
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.Idempotency-Keyheader · string (uuid)Unique key for idempotent requests. If you retry a request with the same key within 24 hours, you receive the original response. The key is bound to the request payload: reusing it for a different payload returns 422
idempotency_key_reuserather than the original response, so a changed request is never silently dropped. The binding is over the JSON's content, not its bytes — keys in another order or other whitespace are the same request. Generate a new key for each new request. On an operation that moves money (an order, a batch, an item, a shipping or address change, a cancellation's refund), a key that cannot be checked because the idempotency ledger is unreachable is answered 503idempotency_unavailablewithRetry-After, and nothing runs or is charged: retry with the same key.
Request body application/json, required
target_statestringrequiredOne of
in_productionshippeddeliveredcancelledissue_flaggedissue_reasonstringOnly with
target_state: issue_flagged: why the rehearsed flag was raised — the same codes a live order'sissue_reasoncarries on its reads and onorder.issue. Sent with any other target_state, it is a 400 validation_error.One of
artwork_unprintableaddress_undeliverableshipment_exceptionon_holdcancellation_pending
Request
POST /api/v1/order/{id}/simulate
curl -X POST "https://portal.printedsimply.com/api/v1/order/test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk/simulate" \
-H "x-api-key: ps_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: simulate-order-$(date +%s)" \
-d '{
"target_state": "shipped"
}'Response 200
Order advanced; webhooks fired
data
order_idstringportal_order_idstringstatusstringThe status the order was moved to. A simulation only ever moves an order forward through the statuses the platform writes, never into a legacy one.
One of
pending_approvalpending_paymentpaidacceptedin_productionquality_checkshippeddeliveredcancelledissue_flaggedreroutingissue_reasonstring | nullPresent only when
statusisissue_flagged: theissue_reasonthe flag was raised with, or null when none was named.One of
artwork_unprintableaddress_undeliverableshipment_exceptionon_holdcancellation_pendingsimulatedbooleanAlways
true
meta
pageintegertotalintegerlimitintegerThe page size actually applied, which is not necessarily the one asked for — the operation's own ceiling wins.
pagesintegertotaloverlimit, rounded up.next_cursorstring | nullOn a cursor-paged operation: send this back as
cursorfor the next page. Null on the last page. Absent on operations that do not page by cursor.has_morebooleanOn a list operation whose
metaisListMeta: whether another page follows this one.
{
"status": "success",
"data": {
"order_id": "test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk",
"portal_order_id": "ord_wVK9ZiG-VYiPrlSeBUBmrUVk",
"status": "shipped",
"simulated": true
},
"request_id": "4e3753e1-48dd-4313-bfd8-48e4af4e116a"
}Errors
Live key (test_mode_only), invalid target_state or issue_reason (validation_error), or order already terminal (already_terminal)
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
payload_too_large — the request body exceeded this route's byte cap (256 KB unless the route raises it with a written reason). It is refused before it is parsed, so nothing was created. Send fewer rows, or use the endpoint's own asynchronous door where it has one.
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.