For an order that shipped by letter — untracked letter mail, which carries no carrier scans. Once its expected window has passed (the plant's production estimate plus the slowest letter transit, stated on the order's letter_mail event and its tracking page), reporting it missing creates ONE free replacement: the same lines at $0, to the same address, by letter, produced where the original was. Nothing is charged. The original fires order.updated with changes.action: "letter_reshipped" and the replacement's ids.
Once per letter: a second report — on the original or on the replacement — is refused 409 letter_report_refused, and two racing reports produce one replacement. details.reason is not_a_letter (it shipped as a parcel; read its tracking), not_shipped (it has not left the plant), not_yet_due (with details.expected_by), already_reshipped (with details.replacement_order_id) or is_a_reship. A sandbox letter may be reported as soon as it has shipped — the sandbox compresses time — and its replacement runs the sandbox lifecycle. Letter mail exists only where the platform prices it; on a deployment that does not, no order is a letter and every report is not_a_letter.
- 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
POST /api/v1/order/{id}/missing-letter
curl -X POST "https://portal.printedsimply.com/api/v1/order/test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk/missing-letter" \
-H "x-api-key: ps_test_YOUR_KEY" \
-H "Idempotency-Key: report-missing-letter-$(date +%s)"Response 200
The letter was reported missing and its one free replacement created
data
order_idstringrequiredThe letter that was reported missing.
portal_order_idstringrequiredreplacementobjectrequiredThe one free replacement. An order in its own right: GET /v1/order/{id} reads it.
replacement.order_idstringrequiredIts public id. A placeholder until the plant accepts it, then the plant's own id; the portal id never changes.
replacement.portal_order_idstringrequiredreplacement.statusstringrequiredpaidwhen it is written; it moves through the lifecycle like any order.One of
pending_approvalpending_paymentpaidacceptedin_productionquality_checkshippeddeliveredcancelledissue_flaggedreroutingreplacement.shipping_methodstringrequiredA replacement ships the way the original did.
Always
letterreplacement.total_centsintegerrequiredNothing is charged for a replacement.
Always
0replacement.tracking_urlstring (uri)requiredIts tracking page, which states the letter's expected window — there are no carrier scans.
test_modebooleanrequired
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_rzbohZBiqt6XnFMC5RazH_R5",
"portal_order_id": "ord_rzbohZBiqt6XnFMC5RazH_R5",
"replacement": {
"order_id": "rmk_KEFW7K8h6mUCGRWlDC",
"portal_order_id": "ord_FvjgWGKEFW7K8h6mUCGRWlDC",
"status": "paid",
"shipping_method": "letter",
"total_cents": 0,
"tracking_url": "https://portal.printedsimply.com/track/eyJ2IjoxLCJ0IjoidHJhY2tpbmciLCJ0b2siOiJ0cmtfMkJMeUh2TVd1RS1MbDc5QlFEU050dHdFIiwiZXhwIjoiMjAyNy0wMS0xM1QxNTowMDo0MS4wMDBaIn0.d6738ba42dc0cc76aa05ceb30c4b0f02fae1f278f1dbd4b69434d65549551044"
},
"test_mode": true
},
"request_id": "4e3753e1-48dd-4313-bfd8-48e4af4e116a"
}Errors
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).403sandbox_onlysandbox_only — the account is restricted to sandbox mode and the request used a live key.
404Resource not found
letter_report_refused —
details.reasonsays why: not_a_letter, not_shipped, not_yet_due (with expected_by), already_reshipped (with replacement_order_id) or is_a_reship.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.