Retrieve one order by its order_id or its portal_order_id — either id resolves the same order, and the response names it by its order_id. The read carries where the parcel is: tracking (number, carrier, carrier link and the hosted tracking page) and the flat tracking_number, tracking_carrier and tracking_url, the same values GET /v1/orders and the order.shipped webhook report, with created_at and updated_at. Alongside the fulfilment record, the response carries the fields this API is authoritative for: the reference and vat you supplied, made_by (the network shop that produced it), the guarantee in force on the order — what you were promised at quote time, whether it held, and any remedy recorded — artwork_access, the record of which independent shops were given this order's print files, how many times each fetched them, and whether they can still be fetched, and line_items, the order's lines with the id of each. That id is the {itemId} in PUT /v1/{id}/item/{itemId} and DELETE /v1/{id}/item/{itemId}/cancel, and the order_item_id a size_exchange return requires. It is deliberately NOT the items[].id in the fulfilment record beside it: that one is the facility's own line id, in a different namespace, and none of those three operations accept it. actions says which changes the order still takes (cancel, address, shipping, and adding, changing or cancelling a line), from the same checks those operations refuse on, so a client can offer an action without trying it first.
- 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.
Request
GET /api/v1/order/{id}
curl "https://portal.printedsimply.com/api/v1/order/test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk" \
-H "x-api-key: ps_test_YOUR_KEY"Response 200
Order details
data
guaranteeobject | nullThe delivery guarantee in force on this order, or null when the order carries none (guarantees are issued only on lanes whose measured on-time record clears the floor, and only while the feature is enabled). The keys mirror the
guaranteeblock returned by POST /orders/quote, so this is the object you were quoted, read back, plus what has since happened to it.guarantee.guarantee_idstringrequiredSame id the quote issued.
guarantee.deliver_bystring (date-time)requiredThe date this order is guaranteed to arrive by.
guarantee.quality_standardstringrequiredThe quality standard guaranteed ("standard" in v1).
guarantee.confidencenumberrequiredLower bound on the lane's on-time probability at issue time (Wilson), between 0 and 1.
guarantee.remedyobjectrequiredThe promise. cap_cents is the ceiling on what a breach can pay — never an amount owed or paid; read
remediesfor what was actually recorded.guarantee.remedy.typestringrequiredcredit is the only remedy issued today, and the one that settles as a real refund. remake and expedite are reserved for the ops-driven remedies; one would settle as not_applicable rather than pay.
One of
creditremakeexpediteguarantee.remedy.cap_centsintegerrequiredguarantee.statusstringrequiredissued while the promise is open, honored once the order was delivered on time, remedied once it was not. There is deliberately no
breachedvalue: a breach and its capped remedy are recorded in one transaction (so two overlapping sweeps cannot both pay out), so the terminal state of a broken promise isremediedand whether the money moved is the remedy's own status.One of
issuedhonoredremediedguarantee.issued_atstring (date-time)requiredguarantee.resolved_atstring (date-time) | nullrequiredWhen the guarantee was honored or remedied; null while still open.
guarantee.remediesobject[]requiredRemedies recorded against this guarantee. Empty unless it was breached.
guarantee.remedies[].typestringrequiredOne of
creditremakeexpediteguarantee.remedies[].amount_centsintegerrequiredWhat was recorded, capped at the order value at issue.
guarantee.remedies[].statusstringrequiredapplied is the only value that means money moved — a real refund on the order's charge. not_applicable is the ordinary sandbox outcome (a test order has no charge) and also covers a remake/expedite, which is an ops action rather than an automatic payment. failed is an owed remedy that has not been paid, kept distinct from not_applicable so it stays visible.
One of
pendingappliedfailednot_applicableguarantee.remedies[].applied_atstring (date-time) | nullrequiredguarantee.remedies[].created_atstring (date-time)requiredorder_idstringThe fulfilment network's order id: the one every order operation takes, whichever id this request was made with. Present on every order; on a live one the facility's own body supplies it when it carries one.
portal_order_idstringThis platform's own id for the order (
ord_…), the id in the dashboard's order page.GET /v1/order/{id}and every other order operation accept it in place oforder_id. Present on every order; on a live one the facility's own body supplies it when it carries one.referencestring | nullThe
referenceyou supplied at creation, echoed back.vatstring | nullThe
vatyou supplied at creation, echoed back.statusstringThe order's lifecycle status. The published vocabulary is the one
GET /v1/ordersfilters on. Always on a sandbox order; on a live one it comes from the facility's own body.pending_approvalon an order held for a person's approval (seeapproval), which is served from our own record, as a sandbox order is, until it is placed.approvalobjectOn an order placed with
approval: "required"only: whether a person has approved it, the link they decide at while it still decides, and their decision. A hold whose link has expired is closed by this read (expired, the ordercancelled).approval.statusstringrequiredpending: waiting for the person.approving: a decision is placing it now.approved: approved and placed.declined: the person declined; the order is cancelled.expired: nobody decided in time; cancelled.cancelled: you cancelled it before anyone decided.failed: an approval did not place it (seefailure); nothing was charged and it may be approved again until it expires.One of
pendingapprovingapproveddeclinedexpiredcancelledfailedapproval.urlstring | nullrequiredThe signed page the person approves or declines on, with no account. Null once it no longer decides (decided, expired, cancelled).
approval.expires_atstring (date-time) | nullrequiredWhen the link stops deciding: 72 hours after the hold.
approval.decided_atstring (date-time) | nullrequiredWhen the person approved or declined it. Null until then.
approval.decided_bystring | nullrequiredThe name the person typed with their decision, if any. No address or other identifier is recorded.
approval.failureobject | nullrequiredWhy the last approval placed nothing: the code and message POST /order answered. Null unless an approval failed.
approval.failure.codestringrequiredapproval.failure.messagestringrequiredapproval.failure.atstring (date-time)requiredissue_reasonstring | nullWhy the order is
issue_flagged, when the facility's status said: the same codeorder.issuecarries. Null when the order is not flagged, or is flagged without a reason that fits one of these. Always on a sandbox order (a simulation names it); on a live one it is filled from our record wherever the facility's own body carries no key of that name.One of
artwork_unprintableaddress_undeliverableshipment_exceptionon_holdcancellation_pendingproviderstringThe fulfilment network that holds the order. Always on a sandbox order; on a live one it comes from the facility's own body.
made_byobject | nullThe independent network shop that produced an order, or null when an in-house lane did.
GET /v1/ordersandGET /v1/order/{id}build it from the same helper and both$refthis, so they cannot describe one fact two ways — which they did, one of them calling it a string. Every order placed on this portal is made at our own plant, so it is null on them; a shop is named only on an order placed before that.made_by.namestringrequiredThe shop's display name.
made_by.slugstringrequiredThe shop's directory slug.
made_by.profile_urlstring (uri)requiredIts public profile page on this platform.
total_centsintegerWhat the order was charged, in cents. Sandbox orders only — a live response reports money the way the facility does.
shipping_centsintegerThe shipping charge, in cents. Sandbox orders only, like
total_cents.tracking_tokenstring | nullThe opaque token for this order's hosted tracking page, as
GET /v1/ordersreports it; null on an order with none.tracking_numberstring | nullThe carrier's tracking number; null until the order ships. The same value as
tracking.number, the list'stracking_numberand theorder.shippedwebhook's.tracking_carrierstring | nullThe carrier that holds the shipment; null until the order ships.
tracking_urlstring | nullThe carrier's tracking link; null until the order ships. (The create response's
tracking_urlis the hosted tracking page, which this read publishes astracking.page_url.)trackingobjectWhere the parcel is. The same block on
GET /v1/order/{id}and on everyGET /v1/ordersrow.number,carrierandurlare null until the order ships, and equal the flattracking_number,tracking_carrierandtracking_urlbeside it. On a live order whose facility reports its owntrackingobject, that object's keys are kept and these are added where it has none.tracking.numberstring | nullrequiredThe carrier's tracking number; null until the order ships.
tracking.carrierstring | nullrequiredThe carrier that holds the shipment; null until the order ships.
tracking.urlstring | nullrequiredThe carrier's tracking link; null until the order ships.
tracking.page_urlstring (uri) | nullrequiredThe order's hosted tracking page, the link to give your customer. It exists from the moment the order is placed and shows each step, including before the carrier has a number; the create response's
tracking_urlopens the same page. Null only on an order with no tracking token.estimated_deliveryobject | nullWhen the order should arrive, as an estimate counted from placement; null where no honest date exists (see OrderEstimatedDelivery). Ours on a live order too: a key of that name in the facility's body is replaced.
estimated_delivery.earlieststring (date)requiredYYYY-MM-DD, UTC: the soonest the order should arrive.
estimated_delivery.lateststring (date)requiredYYYY-MM-DD, UTC: the latest it should, before anything goes wrong.
estimated_delivery.ships_bystring (date)requiredYYYY-MM-DD, UTC: the day it should leave the facility, when its production estimate ends, counted the same way.
estimated_delivery.is_estimatebooleanrequiredAlways true: say it as an estimate wherever it is shown.
Always
truereordered_fromstring | nullOn an order POST /order/{id}/reorder placed: the source order's
portal_order_id(this operation takes it), as the order's row records it and the reorder's own answer stated it. Null on any other order. Ours on a live order too: a key of that name in the facility's body is replaced.created_atstring (date-time)When the order was placed. On a live order the facility's own body supplies it when it carries one.
updated_atstring (date-time)When the order last changed. On a live order the facility's own body supplies it when it carries one.
test_modebooleanPresent, and true, only on a sandbox order. Its absence is how a live order is told apart.
artwork_accessobject[]Every print-file grant ever issued on this order, oldest first. Empty when no independent shop has been given this order's artwork — which is every order produced in-house, and every sandbox order.
artwork_access[].asset_idstringrequiredStable handle for the file this grant covers — a digest of the artwork URL, so two order lines printing the same design share one grant and one count. The artwork URL itself is deliberately not returned: it is held only so the shop never receives it.
artwork_access[].locationstring | nullrequiredPrint area ("front", "back", …) or "mockup"; null when the order line named none.
artwork_access[].shopobject | anyrequiredThe shop the file was released to. Null only if that shop's record has since been deleted — a suspended or de-listed shop is still named here, because an audit entry that disappeared with the shop's directory listing would hide exactly the disclosure worth reading.
artwork_access[].shop.namestringartwork_access[].shop.slugstringartwork_access[].statusstringrequiredlive while the signed window is open, expired once it closed without being re-issued (the ordinary end state — we simply stop re-issuing), revoked if we withdrew it first. live is necessary but not sufficient for a fetch to succeed: access also requires the shop to still be live on the network and the order not to be cancelled, both of which this response already reports via made_by and status.
One of
liveexpiredrevokedartwork_access[].granted_atstring (date-time)requiredWhen the file was first released to this shop.
artwork_access[].expires_atstring (date-time)requiredEnd of the current signed window. In the past on an expired grant.
artwork_access[].access_countintegerrequiredFetches recorded against this grant. 0 means the file was granted and never downloaded.
artwork_access[].last_accessed_atstring (date-time) | nullrequiredMost recent fetch, or null if there has never been one.
artwork_access[].revoked_atstring (date-time) | nullrequiredartwork_access[].revoked_reasonstring | nullrequiredWhy the grant was withdrawn; null unless status is revoked. A closed set: provider_declined (the shop refused the order), submission_failed (submission failed after the URLs had gone out), order_cancelled, order_rerouted (the order moved to another maker), reroute_submit_failed (the replacement refused it too).
One of
provider_declinedsubmission_failedorder_cancelledorder_reroutedreroute_submit_failedaccount_purgedlanded_costobject | nullWhat the border cost on this order, as recorded for the parcel that ships. Null on a domestic shipment — there is no border, so there is nothing to state, and a row of zeros would read like a customs assessment that came to nothing. When a cross-border order has no stored figures,
recordedis false and onlydestination_countryandnot_recorded_reasonare present: the duty, tax and FX rates that applied on the day are not recoverable, and backfilling them with today's rates would be a fabricated record rather than a missing one. That is what an order placed before these figures were stored looks like, and also a free replacement order, which carries no charged goods value to assess. The figures are a snapshot, never recomputed on read, so a stale figure cannot be mistaken for a current one — but they ARE re-recorded when the parcel itself changes, because the border is costed against the destination, the goods and the shipping charge. Change the address, add or cancel a line, change a quantity or change the shipping method and the order is costed again at that moment;recorded_atmoves with it, and it is the order's creation time until something does. An order re-routed to a domestic address goes back to null.estimatedstays true throughout: on DAP the courier bills the recipient directly, so we never learn what customs actually assessed.landed_cost.recordedbooleanrequiredFalse when no landed cost was stored for this order.
landed_cost.destination_countrystringISO 3166-1 alpha-2.
landed_cost.not_recorded_reasonstringPresent only when
recordedis false.landed_cost.recorded_atstring (date-time)When the figures were computed — never the moment of the read. The order's creation time, or the moment of the last change to its destination, lines or shipping charge.
landed_cost.duty_basisstringDAP today: goods and shipping are collected at checkout, and the courier bills the recipient for duties and import tax on delivery.
One of
DDPDAPlanded_cost.duty_ratenumberlanded_cost.tax_ratenumberlanded_cost.breakdownobjectlanded_cost.breakdown.goods_centsintegerlanded_cost.breakdown.shipping_centsintegerlanded_cost.breakdown.duties_centsintegerlanded_cost.breakdown.tax_centsintegerlanded_cost.total_landed_centsintegerWhat the buyer ends up paying in total.
landed_cost.pre_collected_centsintegerCollected at checkout. Equals goods + shipping — never more than was charged.
landed_cost.due_on_delivery_centsintegerBilled by the courier on delivery. Sums with
pre_collected_centsto the total.landed_cost.presentmentobjectlanded_cost.presentment.currencystringlanded_cost.presentment.amount_centsintegerlanded_cost.presentment.minor_unitsintegerlanded_cost.presentment.fx_ratenumberlanded_cost.estimatedbooleanlanded_cost.notestringline_itemsobject[]The order's lines, each with the id our own item and return operations resolve. Present on live and sandbox orders alike.
line_items[].idstringrequiredThis line's id. Use it as
{itemId}on PUT /v1/{id}/item/{itemId} and DELETE /v1/{id}/item/{itemId}/cancel.line_items[].skustringrequiredline_items[].quantityintegerrequiredat least 1
line_items[].unit_price_centsintegerrequiredline_items[].artworkobjectThe line's artwork declaration, as the order accepted it: how the artwork was made, and who approved it and when. Absent on a line that declared none.
line_items[].artwork.provenancestringHow the artwork was made:
human(made by a person),ai_assisted(a person made it with a model's help) orai_generated(a model made it).One of
humanai_assistedai_generatedline_items[].artwork.approved_bystringWho approved the artwork for print: a name or an email address, at most 200 characters. Control and invisible formatting characters are removed and whitespace collapsed before it is stored. Send it with approved_at.
non-empty · at most 200 characters
line_items[].artwork.approved_atstring (date-time)When the artwork was approved: an ISO 8601 date-time with a time zone, not in the future. Stored and echoed in UTC. Send it with approved_by.
actionsobjectWhich changes this order still takes, read from the same checks the operations refuse on, so a client can show or offer an action without trying it first.
truemeans the order's own state does not refuse it; the request is still checked as usual (the key's scope; the line an item action names, which must exist and, on a live order at our own plant, carry its artwork; and on a live order the facility, which can refuse a change with 502 fulfillment_error).falsenames the refusal the operation would give. Live PrintedSimply orders have add_item, update_item and upgrade_shipping false; only supported no-cost or downward shipping requests remain requestable. On other orders a shopper paid for (a drop, a checkout link or an agent checkout),update_itemandupdate_shippingstay true because a decrease or a downgrade is allowed, while an increase or an upgrade is refused 409 shopper_paid_order, asadd_itemis.actions.cancelbooleanrequiredDELETE /order/{id}/cancel. True only before production (pending_payment, paid or accepted); false once the order is cancelled (400 already_cancelled) or past that (409 not_cancellable).
actions.update_addressbooleanrequiredPUT /order/{id}/address. False on an order an independent shop made (501 not_supported_by_provider) and once the order is past the point of change (409 order_not_modifiable).
actions.update_shippingbooleanrequiredPUT /order/{id}/shipping. False for the reasons
update_addressis.actions.upgrade_shippingbooleanrequiredPositive-cost PUT /order/{id}/shipping upgrades. False for live PrintedSimply orders (501 not_supported_by_provider), shopper-paid orders (409 shopper_paid_order), and when update_shipping is false. A downgrade or unchanged method can still be allowed when this is false.
actions.add_itembooleanrequiredPOST /{id}/item. False for the reasons
update_addressis, on live PrintedSimply orders (501 not_supported_by_provider), and on an order a shopper paid for (409 shopper_paid_order).actions.update_itembooleanrequiredPUT /{id}/item/{itemId}. False for the reasons
update_addressis and on live PrintedSimply orders (501 not_supported_by_provider). An unchanged quantity is still an allowed no-op.actions.cancel_itembooleanrequiredDELETE /{id}/item/{itemId}/cancel. False for the reasons
update_addressis.
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",
"reference": null,
"vat": null,
"status": "paid",
"issue_reason": null,
"provider": "merchfarm",
"made_by": null,
"guarantee": null,
"artwork_access": [],
"line_items": [
{
"id": "itm_fowT5-1iZwSnQcGX0ByoA64l",
"sku": "sticker-die-cut-3x3",
"quantity": 100,
"unit_price_cents": 64
}
],
"actions": {
"cancel": true,
"update_address": true,
"update_shipping": true,
"upgrade_shipping": true,
"add_item": true,
"update_item": true,
"cancel_item": true
},
"total_cents": 7299,
"shipping_cents": 899,
"landed_cost": null,
"tracking_token": "trk_AquJFqDLDkisK6Jh-HABHz9L",
"tracking_number": null,
"tracking_carrier": null,
"tracking_url": null,
"tracking": {
"number": null,
"carrier": null,
"url": null,
"page_url": "https://portal.printedsimply.com/track/eyJ2IjoxLCJ0IjoidHJhY2tpbmciLCJ0b2siOiJ0cmtfQXF1SkZxRExEa2lzSzZKaC1IQUJIejlMIn0.199c0c58721512fcccd784902c422ca4e827c6d09b7e495bbb4db9b06ae62003"
},
"estimated_delivery": {
"earliest": "2026-10-20",
"latest": "2026-10-26",
"ships_by": "2026-10-14",
"is_estimate": true
},
"reordered_from": null,
"created_at": "2026-10-10T08:09:14.588Z",
"updated_at": "2026-10-10T08:09:14.588Z",
"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).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.502Production system error