Prices an order and returns a signed quote_token (24h TTL). Pass the token to POST /order to charge exactly the quoted total — no re-pricing surprise. May include a non-binding delivery_risk advisory and cross-border presentment/duties blocks.
- Credential
- An API key in
x-api-key - Scopes
pricing:read
Parameters
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
itemsobject[]requiredat least 1 entry
items[].skustringrequirednon-empty
items[].quantityintegerrequiredat least 1
items[].variable_dataobjectThe same per-unit field POST /order takes, preflighted the same way, so an overflow or a count mismatch is refused here before you order. Never priced: the quote is what it is without it. The response's
variable_datacarries the variant proofs.items[].variable_data.slotsobject[]requiredThe named text slots of the line's design, each with the most characters it holds.
1 to 10 entries
items[].variable_data.slots[].namestringrequiredLower-case letters, digits and underscores, starting with a letter, e.g. name or serial. Each name once.
matches ^[a-z][a-z0-9_]{0,39}$
items[].variable_data.slots[].max_lengthintegerrequiredThe longest value this slot prints. A longer value is refused as
overflow, not shrunk or cut.1 to 200
items[].variable_data.unitsobject[]One object per unit, in print order: { slot name: value }. Its length must equal the line's quantity. Values are strings or numbers, with no line breaks. Omit it when a
sequencefills every slot.1 to 10000 entries
items[].variable_data.sequenceobjectNumbers one slot per unit: prefix + (start + (unit - 1) × step), zero-padded to
paddigits, e.g. A-0001, A-0002. A unit may not also set that slot.items[].variable_data.sequence.slotstringrequiredA declared slot.
non-empty · at most 40 characters
items[].variable_data.sequence.prefixstringPrinted before the number. Default empty.
at most 40 characters
items[].variable_data.sequence.startintegerThe first unit's number. Default 1.
0 to 1000000000
items[].variable_data.sequence.stepintegerAdded per unit. Default 1.
1 to 1000000
items[].variable_data.sequence.padintegerZero-pad the number to this many digits. Default 0 (no padding).
0 to 12
items[].variable_data.qrobjectEach unit's QR payload, built from its values.
items[].variable_data.qr.templatestringrequiredText with {slot} placeholders, e.g. https://example.com/t/{serial}. Every placeholder must name a declared slot.
non-empty · at most 1000 characters
items[].variable_data.qr.max_lengthintegerThe longest payload accepted, for a small sticker whose code must stay scannable. Default 1000.
1 to 1000
shippingobjectshipping.methodstringstandard (default); priority or express once they are priced — until then a request for either is 400 shipping_method_unavailable.
currencystringOptional presentment currency (ISO 4217); adds a presentment block when non-USD. The block's amount_cents is in that currency's own minor units and minor_units carries the exponent (0 for JPY, 2 for EUR). An unsupported currency is rejected with unsupported_currency rather than quoted at a fallback rate.
exactly 3 characters
destination_countrystringISO 3166-1 alpha-2. Our own plant ships to US addresses only, so any other country is refused 422 destination_unavailable.
address_to.country, when sent, is the one checked.exactly 2 characters
deadlinestring (date-time)Arrival deadline — delivery_risk warns when it's tight.
deliver_bystring (date)A day the order should arrive by, YYYY-MM-DD (UTC). Answered in
deliver_by_check, judged against the estimate's latest day rather than its earliest, from the facility's production estimate and the method's transit window. Optional; a day before today, or anything but a real date, is 400 validation_error.providerstringThe lane to price: always our own plant here, which is where POST /order makes the order. Omit it, or send
printedsimply; any other value is refused with 422provider_not_availablebefore anything is priced.Always
printedsimplyDefault printedsimply
address_toobjectThe destination. Optional: our own plant prices shipping from its own table, so only its country is read, and it ships to US addresses only (any other country is refused 422 destination_unavailable).
address_to.namestringrequirednon-empty
address_to.companystringaddress_to.address1stringrequirednon-empty
address_to.address2stringaddress_to.citystringrequirednon-empty
address_to.statestringrequirednon-empty
address_to.zipstringrequirednon-empty
address_to.countrystringrequiredISO 3166-1 alpha-2 country code
exactly 2 characters
address_to.phonestringaddress_to.emailstring (email)
Request
POST /api/v1/orders/quote
curl -X POST "https://portal.printedsimply.com/api/v1/orders/quote" \
-H "x-api-key: ps_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quote-order-$(date +%s)" \
-d '{
"items": [
{
"sku": "sticker-die-cut-3x3",
"quantity": 100
}
],
"shipping": {
"method": "standard"
}
}'Response 200
Signed quote, priced at our own plant: quote_id, quote_token (pass it to POST /order to be charged exactly this total), expires_at, lines, breakdown, margin and shipping (its method, and the peak-season surcharge the shipping figure already holds, itemised); plus estimated_delivery, deliver_by_check when the request named a deliver_by day, delivery_risk when the order carries a known risk, dispatch_promise (switched off today), presentment when a currency other than USD was asked for, variable_data when a line carries it, and, on a test key, pricing_degraded.
data
quote_idstringrequiredaddress_checkobjectWhat POST /addresses/validate finds in
address_to, when the quote was sent one. A live key is refused an address with anerrorfinding (422 address_invalid); on a test keyvalid: falsehere says a live quote would have been.address_check.validbooleanrequiredFalse when any finding is an
error.address_check.checkedbooleanrequiredFalse for an address outside the US, which is passed through as written.
address_check.normalizedobjectrequiredThe address in USPS style: upper case, no punctuation, ZIP+4 hyphenated, the state as its code, a unit typed into address1 moved to address2. Name, company, phone and email are trimmed and otherwise as sent.
address_check.normalized.namestringaddress_check.normalized.companystringaddress_check.normalized.address1stringrequiredaddress_check.normalized.address2stringaddress_check.normalized.citystringrequiredaddress_check.normalized.statestringrequiredaddress_check.normalized.zipstringrequiredaddress_check.normalized.countrystringrequiredaddress_check.normalized.phonestringaddress_check.normalized.emailstringaddress_check.findingsobject[]requiredaddress_check.findings[].codestringrequiredThe
delivery_point_*codes come from delivery-point verification, which only a live POST /order, POST /orders/bulk entry or PUT /order/{id}/address runs, on a US address the other checks pass, and only where the deployment has USPS verification configured:delivery_point_not_found(error) — USPS answered that no delivery point exists at the street address;delivery_point_incomplete(warning) — the building exists but its unit is missing or not confirmed, or more than one address matched;delivery_point_confirmed(info). When USPS cannot be reached the order proceeds without them.One of
zip_formatstate_unknownzip_state_mismatchzip_state_borderzip_unassignedpo_boxmilitary_statemilitaryaddress2_splitnot_checkeddelivery_point_not_founddelivery_point_incompletedelivery_point_confirmedaddress_check.findings[].severitystringrequirederror: the address cannot be right as written, and a live order or quote is refused with 422 address_invalid.warning: worth a look, never refused.info: what normalization did, or that nothing was checked.One of
errorwarninginfoaddress_check.findings[].fieldstringrequiredOne of
address1address2citystatezipcountryaddress_check.findings[].messagestringrequiredquote_tokenstringrequiredSigned, and what
POST /v1/orderhonours. It carries the quote's own numbers, so an order placed against it cannot be re-pointed at a cheaper one.expires_atstring (date-time)requiredAfter this the token is refused and the quote must be taken again.
linesobject[]requiredThe priced lines, each { sku, quantity, unit_cents }.
lines[].skustringrequiredlines[].quantityintegerrequiredlines[].unit_centsintegerrequiredWhat this API charges per unit, in USD cents.
breakdownobjectrequiredbreakdown.items_centsintegerrequiredbreakdown.shipping_centsintegerrequiredbreakdown.processing_fee_centsintegerrequiredCompatibility field for platform and card-processing surcharges in cents. Zero on orders priced under the current no-added-fees policy; retained on historical records.
breakdown.platform_fee_centsintegerThe platform fee as a line of its own. Always 0: there is no platform fee on any order, on any plan (since 2026-10-17). Kept so a client that reads it still parses.
breakdown.card_fee_centsintegerCard processing on everything else the order is charged, passed through at cost.
breakdown.total_centsintegerrequiredmarginobjectrequiredWhat to sell it for and what that leaves. A suggestion, not a commitment.
margin.suggested_retail_centsintegerrequiredmargin.margin_pctnumberrequiredpresentmentobjectThe total in the buyer's currency. Present only on a cross-border quote.
dutiesobjectPresent only when duties apply.
landed_costobjectlanded_cost.destination_countrystringrequiredlanded_cost.duty_basisstringrequiredWho pays at the border. The VALUE is the incoterm; what customs assesses on is goods plus shipping plus duty, which is a different fact and was the whole of this description before.
One of
DDPDAPlanded_cost.duty_ratenumberrequiredlanded_cost.tax_ratenumberrequiredlanded_cost.breakdownobjectrequiredlanded_cost.breakdown.goods_centsintegerrequiredlanded_cost.breakdown.shipping_centsintegerrequiredlanded_cost.breakdown.duties_centsintegerrequiredlanded_cost.breakdown.tax_centsintegerrequiredlanded_cost.total_landed_centsintegerrequiredlanded_cost.pre_collected_centsintegerrequiredCharged with the order.
landed_cost.due_on_delivery_centsintegerrequiredWhat the courier bills the recipient. The number a buyer is surprised by if nobody told them.
landed_cost.presentmentobjectrequiredThe same total in the destination's currency.
landed_cost.presentment.currencystringrequiredlanded_cost.presentment.amount_centsintegerrequiredlanded_cost.presentment.minor_unitsintegerrequiredlanded_cost.presentment.fx_ratenumberrequiredlanded_cost.estimatedbooleanrequiredAlways true off this path, and sent anyway: a consumer must be able to tell an estimate from a settled charge without knowing which endpoint produced it.
landed_cost.notestringrequiredguaranteeobjectThe delivery guarantee this quote would carry. Present only when one is offered on the lane.
dispatch_promiseobjectA dispatch-by date for an order made at our own plant, and what is paid if it leaves the plant later. Present only when the platform has switched the dispatch promise on AND every line's measured dispatch p95 is inside its window — so today, with the promise switched off, no quote carries it. Inside the quote signature; an order placed against this quote is held to exactly this date and remedy.
dispatch_promise.dispatch_bystring (date-time)requiredThe order is promised to leave the plant by this instant.
dispatch_promise.business_daysintegerrequiredThe window the date was computed with, in business days (Saturday and Sunday do not count).
dispatch_promise.remedyobjectrequireddispatch_promise.remedy.typestringrequiredshipping: the order's shipping charge is refunded.credit: a fixed amount is refunded.One of
shippingcreditdispatch_promise.remedy.centsintegerrequiredWhat a miss refunds, capped by what the order's charges have left.
delivery_riskobjectPresent only when the lane carries a known risk worth stating before the order. Non-binding. When a carrier advisory touches the destination between today and the estimate's latest day,
at_riskis true andadvisorieslists them; a quote may also carry its lane's lead-time read beside them.delivery_risk.at_riskbooleanPresent, and true, when
advisoriesis.Always
truedelivery_risk.advisoriesobject[]The carrier advisories that touch the destination while the parcel would be on its way.
delivery_risk.advisories[].idstringrequiredadv_….delivery_risk.advisories[].carrierstringrequiredThe carrier that posted it, as it names itself, e.g. "USPS".
delivery_risk.advisories[].statesstring[]requiredUSPS state codes it touches. With
zip_prefixesboth empty, it touches every US destination.delivery_risk.advisories[].zip_prefixesstring[]requiredZIP prefixes (one to five digits) it touches, beside
states.delivery_risk.advisories[].fromstring (date)requiredThe first day it applies, YYYY-MM-DD in UTC, inclusive.
delivery_risk.advisories[].throughstring (date)requiredThe last day it applies, YYYY-MM-DD in UTC, inclusive.
delivery_risk.advisories[].activebooleanrequiredWhether today (UTC) is inside
from–through.delivery_risk.advisories[].extra_daysintegerrequiredThe carrier's estimate of the business days it adds, as posted. An estimate.
delivery_risk.advisories[].messagestringrequiredOne sentence about it.
delivery_risk.advisories[].sourcestringrequiredThe carrier's own notice, as a URL.
estimated_deliveryobjectWhen an order placed now should arrive, as an estimate: the facility's production estimate plus the shipping method's transit window (our plant's standard parcel from the plant's own transit estimate), in business days counted in UTC, Monday to Friday, never on a day GET /calendar lists (the federal holidays, each on the weekday it is observed, and the plant's own closure days). Not a promise and not inside
quote_token: the binding date, where the account has one, isdispatch_promise. Present on a quote priced at one of our own facilities; absent on a degraded quote and on any other lane, which sets its own times.estimated_delivery.earlieststring (date)requiredYYYY-MM-DD, UTC.
estimated_delivery.lateststring (date)requiredYYYY-MM-DD, UTC.
estimated_delivery.ships_bystring (date)requiredYYYY-MM-DD, UTC: the day an order placed now 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
truedeliver_by_checkobjectThe answer to
deliver_by, read as an estimate: production and transit are the facility's and the carrier's estimates, so a day thatmeetscan still be missed. Present only whendeliver_bywas sent and there is an estimate to judge it against.deliver_by_check.requestedstring (date)requiredThe day asked about, as sent.
deliver_by_check.meetsbooleanrequiredOn a quote: whether its own method's estimate ends on or before
requested(estimated_delivery.latest, never the earliest day). On a shipping estimate: whether any parcel method listed does. False when a carrier advisory touches the destination (statusisat_risk): true exactly whenstatusismeets.deliver_by_check.earliest_methodstring | anyrequiredThe parcel method on offer whose estimate ends soonest, when it ends on or before
requested; null when no method on offer is estimated to arrive by then. Neverletter.deliver_by_check.latest_order_daystring (date) | nullrequiredThe last UTC day an order can be placed and still be estimated to arrive by
requested, computed by the same estimate asestimated_deliveryfrom the same facility: on a quote, for its own method; on a shipping estimate, for the parcel method listed that allows the latest day. Any time that day counts, and it may be a weekend or a holiday (an order placed then starts counting on the next operating day). Null exactly when no order placed today or later is estimated to arrive byrequested, so it is null whenstatusismisses. It reads the estimate alone, never a carrier advisory. An estimate, like the dates it reads.deliver_by_check.statusstringrequiredThe verdict.
misses: the estimate ends afterrequested.at_risk: the estimate ends by then, but a carrier advisory (advisories) touches the destination while the parcel would be on its way.meets: the estimate ends by then and no advisory touches it.One of
meetsat_riskmissesdeliver_by_check.advisoriesobject[]requiredThe carrier advisories that touch the destination between today and the estimate's latest day; empty when none does.
The same fields as
delivery_risk.advisoriesshippingobjectrequiredThe shipping the figure in
breakdownis for: its method, and the surcharges that figure already holds, itemised. Nothing here is added to the total.shipping.methodstringrequiredThe shipping method the figure was priced for.
shipping.surchargesobject[]requiredCompatibility itemisation, never added to the total. Current quotes and new orders contain no seasonal peak-postage surcharge: PrintedSimply absorbs the carrier expense, so this array is empty. Historical stored orders may retain their original itemisation; a fee-bearing old quote must be refreshed before a new charge.
shipping.surcharges[].codestringrequiredpeak_season: the carrier's temporary holiday price change.Always
peak_seasonshipping.surcharges[].reasonstringrequiredWhat it is, in words.
shipping.surcharges[].amount_centsintegerrequiredHow much of the shipping figure this is, in cents, folded the way that figure is (all-in where the figure is all-in).
at least 1
shipping.surcharges[].windowobjectrequiredThe days it applies on, both inclusive.
shipping.surcharges[].window.fromstring (date)requiredYYYY-MM-DD, UTC.
shipping.surcharges[].window.throughstring (date)requiredYYYY-MM-DD, UTC.
shipping.surcharges[].sourcestring (uri)requiredWhere the amount and the dates come from: the carrier's own announcement.
shipping.surcharges[].included_instringrequiredAlways
shipping: the figure this is part of. Never an additional charge.Always
shippingpricing_degradedstringSandbox-only. The $0 breakdown beside it is an ABSENCE of a price, not a price — the routing failed and a test key was given a shaped answer anyway.
One of
provider_unavailablepricing_unavailablevariable_dataobject[]One entry per line carrying
variable_data: the variant proofs (first, last and longest unit) and each slot's headroom, before the order is placed. Absent when no line carries it. The breakdown beside it is the same with or without it.variable_data[].item_indexintegerrequiredThe line's position in the request's
items.at least 0
variable_data[].skustringrequiredvariable_data[].unitsintegerrequiredUnits on the line, each printed with its own values.
at least 1
variable_data[].slotsobject[]requiredEach declared slot with its
max_lengthand thelongestvalue any unit gives it: the headroom.variable_data[].slots[].namestringrequiredvariable_data[].slots[].max_lengthintegerrequiredvariable_data[].slots[].longestintegerrequiredCharacters in this slot's longest value on the line.
variable_data[].qrobjectPresent when the line has a QR template: the payload ceiling and the longest payload on the line.
variable_data[].qr.max_lengthintegerrequiredvariable_data[].qr.longestintegerrequiredvariable_data[].proofsobject[]requiredThe variant proofs: the first unit, the last, and the longest (the most printed characters, earliest on a tie), with the exact values and QR payload each will print. A unit that is more than one of the three is listed once, with each variant it stands for. Described rather than drawn: a plain line's artwork carries no slot positions to draw a value into.
variable_data[].proofs[].unitintegerrequiredThe unit's position on the line, 1-based, in print order.
at least 1
variable_data[].proofs[].variantsstring[]requiredvariable_data[].proofs[].valuesobjectrequiredSlot name to the text this unit prints.
variable_data[].proofs[].qr_payloadstringWhat this unit's QR code encodes. Present when the line has a QR template.
variable_data[].live_productionbooleanrequiredWhether a LIVE order carrying this line would be accepted now. False while variable-data production is switched off: sandbox orders run end to end, and a live order or quote is refused with 422 variable_data_unavailable.
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": {
"quote_id": "qt_EMyYo3DyOSrd7siDgh2niPvP",
"quote_token": "eyJxdW90ZV9pZCI6InF0X0VNeVlvM0R5T1NyZDdzaURnaDJuaVB2UCJ9.3b71988dd79915637f2e65aeddd8893705bb2140ef57a64dcf71aa67148bef1e",
"expires_at": "2026-10-11T08:09:14.578Z",
"lines": [
{
"sku": "sticker-die-cut-3x3",
"quantity": 100,
"unit_cents": 64
}
],
"breakdown": {
"items_cents": 6400,
"shipping_cents": 899,
"processing_fee_cents": 0,
"platform_fee_cents": 0,
"card_fee_cents": 0,
"total_cents": 7299
},
"margin": {
"suggested_retail_cents": 15666,
"margin_pct": 50
},
"estimated_delivery": {
"earliest": "2026-10-20",
"latest": "2026-10-26",
"ships_by": "2026-10-14",
"is_estimate": true
},
"shipping": {
"method": "standard",
"surcharges": []
}
},
"request_id": "4e3753e1-48dd-4313-bfd8-48e4af4e116a"
}Errors
Invalid request body or parameters. When the request failed the operation's schema,
detailscarriesformErrors(problems with the request as a whole),fieldErrors(messages keyed by top-level field) anderrors: one{ pointer, code, message }per refused value,pointeran RFC 6901 JSON pointer into the body (or into the object of query parameters, for a query parameter) —/items/0/sku, with~written~0and/written~1, and""for the request as a whole. POST /v1/orders/bulk answerserrorsas one{ index, details }per refused order, eachdetailswith its own pointers from the request body (/orders/3/items/0/sku); POST /v1/orders/multi-ship keepserrorsas the{ formErrors, fieldErrors }it always published; POST /v1/mockups/generate and POST /v1/products/recommend keep their fields at the top ofdetails, witherrorsbeside them. A refusal of a rule the schema cannot state (an asset that does not exist, an expiry too soon) names it inmessageand carries noerrors.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).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.
422variable_data_invalidvariable_data_unavailabledestination_unavailablebelow_cost_flooraddress_invalidsku_not_availableprovider_not_availablevariable_data_invalid —
items[].variable_datafailed the preflight, before anything was priced or charged.details.problems[]names each problem (up to 50;details.problem_countis the total) byitem_index,unitandslot, with acode. variable_data_unavailable — a LIVE order or quote carries variable data while production is switched off; nothing was charged, and a test key (ps_test_*) accepts the same request end to end. destination_unavailable —address_to.country(elsedestination_country) is outside the US: our own lanes ship to US addresses only.details.reasonisdestination_not_usanddetails.countrynames the destination. below_cost_floor — the order would be made at our own plant and would cost more to make and ship than it is charged, at worst-case postage, and its loss is not one our cost floor accepts: only today's named losses are, no larger than they are today — the flat standard parcel under its postage, for a mailer up to a pound or a poster tube up to two, and list-priced goods short only of the per-order handling cost (or a partner SKU would be charged below what the partner bills for it).details.skusnames the items anddetails.contribution_centshow far below zero the order falls. Nothing was charged; add units, combine it with another order, or choose another shipping method on offer. address_invalid — a live quote'saddress_tocannot be right as written:details.findings[]names eacherror(zip_format,state_unknown,zip_state_mismatch) with its field anddetails.address_checkis the whole check, as POST /addresses/validate answers it. A test key is never refused for it; its 200 carriesaddress_check. sku_not_available — a line names a product our own plant does not make: only PrintedSimply's own products (stickers, decals, labels, magnets and posters, as GET /products lists them) are sold here.details.skusnames each one. Asked before anything is priced, so nothing was priced or charged. provider_not_available —providernames a lane other than our own plant. Orders here are made by PrintedSimply: omitprovider, or sendprintedsimply.details.providerechoes what was sent anddetails.availablelists what is accepted. Nothing was priced or charged.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.provider_unavailable — the facility that produces these items is not accepting orders right now, so there is no price to quote. Nothing was charged; a quote never charges. With
details.reason: plant_unreachableandRetry-After, our plant gave no answer about a SKU (the request did not reach it, timed out, or came back 5xx) — about the plant, not the SKU; the catalog says the same on the row (not_orderable_code: plant_unreachable). A test key gets a $0 quote markedpricing_degraded: pricing_unavailableinstead. Only for a SKU the catalog has, though: one the price book does not price, our plant's products do not name and the synced catalog does not hold is refused 400 unknown_sku withdetails.skus, as it is on POST /order, POST /orders/quote and POST /pricing/estimate alike.