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://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.tax_centsintegerThe sales tax charged on top of the order's total, in cents, from our own record. Present only on a live order that was taxed: while sales tax collection is on, Stripe Tax calculates it on the goods and shipping for the ship-to address. Absent on a sandbox order, which is never taxed, and on every order placed while tax collection is off.
total_with_tax_centsintegerWhat the card was charged for the order, tax included, in cents. Present only with
tax_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_centsintegerrequiredThe unit price the line was placed at, in USD cents. Where a price is a pack total that does not divide into whole cents a unit, this is the line's rate rounded to the cent, for display: it stays as placed when the line's quantity changes and is shared by the lines of one SKU, so it is not necessarily
line_total_centsoverquantity. Read the line's money fromline_total_cents, never as this timesquantity.line_items[].line_total_centsintegerrequiredThis line's goods, exact, in USD cents, at its current quantity: what the order charged for them, and what cancelling the line refunds. A quantity change moves it at the rate the line's SKU was placed at on this order, which the order's lines of one SKU share, rounded once to the cent. Equal to
unit_price_cents×quantityon every line priced in whole cents a unit.line_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": 70,
"line_total_cents": 7000
}
],
"actions": {
"cancel": true,
"update_address": true,
"update_shipping": true,
"upgrade_shipping": true,
"add_item": true,
"update_item": true,
"cancel_item": true
},
"total_cents": 7650,
"shipping_cents": 650,
"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://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