Calculate shipping options with costs and estimated delivery dates for a set of items to a destination. Does not create an order. Each option's cost_cents is the figure the order path will charge for that method: shipping is charged per parcel, never per item, at the class the items' estimated weight puts the parcel in (shipping_class): $8.99 up to 1 lb, $13.99 to 2 lb, $20.99 to 5 lb, $4.69 a pound above, $5.20 more for a tube (posters and wall decals), and 55¢ more on parcels from 2026-10-04 to 2027-01-16, each bare. Items made at the partner facility (apparel, mugs) ship at the larger of that class rate and the partner's own estimate for the parcel, and the option quotes that same figure: the partner's answer for a basket is reused for five minutes, so an order placed in that window is charged what this quoted. A hundred 3×3 stickers and one ship for the same price. Like a product price, it carries no platform fee and is the same on every plan, as the order path charges it. shipping_address decides which destination the estimate is echoed for; address_to, the name every order operation gives the destination, is accepted in its place, so the address you will order to can be sent as it is. One of the two is required, and both is a 400 validation_error when they name different destinations. Our own facilities ship to US addresses only, so a country other than US is refused 422 destination_unavailable. options lists only the methods on offer: standard always, priority and express once they are priced.
- 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[].product_idintegerCatalog product ID
items[].quantityintegerrequiredat least 1
items[].sizestringProduct size (e.g. S, M, L, XL)
items[].colorstringProduct color
shipping_addressobjectWhere the parcel goes. Only
countrydecides the price (the rate depends on the method and the parcel's weight class, not the zone), so a country-only address is a complete request;zipis echoed back indestinationwhen given.stateandzipalso decide which carrier advisories touch the destination (delivery_risk,deliver_by_check.status): a destination given by country alone is touched by every one. Required unlessaddress_tois sent.shipping_address.statestringshipping_address.zipstringOptional. Echoed back as
destination.zip; null in the response when omitted.non-empty
shipping_address.countrystringDefault US
address_toobjectThe same destination under the name POST /order gives it, so the order's own
address_tocan be sent as it is: onlystate,zipandcountryare read, and the rest is ignored. Send this orshipping_address; both are accepted only when every field they both give agrees (the country defaults to US), and otherwise refused 400 validation_error.address_to.statestringaddress_to.zipstringOptional. Echoed back as
destination.zip; null in the response when omitted.non-empty
address_to.countrystringDefault US
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.
Request
POST /api/v1/shipping/estimate
curl -X POST "https://portal.printedsimply.com/api/v1/shipping/estimate" \
-H "x-api-key: ps_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: estimate-shipping-$(date +%s)" \
-d '{
"items": [
{
"sku": "sticker-die-cut-3x3",
"quantity": 100
}
],
"shipping_address": {
"zip": "78701",
"country": "US"
}
}'Response 200
Every method, priced per PARCEL rather than per item, with business-day windows.
data
destinationobjectrequiredEchoed, not priced from.
destination.zipstring | nullrequiredNull when the caller estimated by country alone.
destination.countrystringrequiredtotal_itemsintegerrequiredUnits across the request. The cost is per parcel regardless.
shipping_classobjectThe parcel these items pack into, which prices every parcel option: a weight band (
parcel_1lb,parcel_2lb,parcel_5lb,parcel_over_5lb), with_tubewhen a poster or wall decal ships rolled. Estimated from the items' print area, not weighed.shipping_class.namestringrequiredshipping_class.weight_oznumberrequiredEstimated shipped weight in ounces, packing tube included.
optionsobject[]requiredoptions[].methodstringrequiredThe shipping tiers this platform prices. Per parcel, not per item:
standardby the parcel's weight class (POST /v1/shipping/estimate reports it), and for items made at the partner facility (apparel, mugs) at the larger of that and the partner's own estimate for the parcel;priorityandexpressat their price and never below standard for the same parcel. An unrecognised value is refused with 400 validation_error rather than priced asstandard.standardis always offered;priorityandexpressare offered when priced (each at or above the USPS cost of the service), and until then a request for either is refused 400shipping_method_unavailable, withdetails.offerednaming the methods that are, and nothing is charged.letteris untracked letter mail for stickers and small vinyl decals from our own plant, offered only where the platform prices it, for an order whose every item qualifies, within a unit and an order-value ceiling: otherwise a request for it is refused 400letter_mail_unavailablewithdetails.reason, and nothing is charged. On the API it is never chosen for you — no default, estimate or storefront mapping picks it — and it is placed through POST /v1/order only (bulk and bundle orders ship by parcel). A hosted store is the one exception, and only where its merchant turned on small orders by letter in the dashboard's store editor (off by default, launched stores included): a qualifying basket bought on its store page or a buy link ships as a letter, and the shopper is told before paying. A letter has no carrier tracking: its order events and tracking page state the expected window, and POST /v1/order/{id}/missing-letter reships it once if it does not arrive.One of
standardpriorityexpressletteroptions[].labelstringrequiredoptions[].coststringrequiredFormatted, e.g. "$7.50".
cost_centsis the number to compute with.options[].cost_centsintegerrequiredFlat per PARCEL, not per item: these items ship once, and this is what the order path will charge for the same method.
options[].estimated_deliveryobjectrequiredAn estimate: the producing facility's own production estimate plus this 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).
options[].estimated_delivery.earlieststring (date)requiredA date, not a timestamp: YYYY-MM-DD in UTC, business days after today.
options[].estimated_delivery.lateststring (date)requiredoptions[].estimated_delivery.ships_bystring (date)requiredYYYY-MM-DD, UTC: the day an order placed now should leave the facility, when its production estimate ends.
options[].estimated_delivery.transit_daysstringrequiredProse, e.g. "3-5 business days": the transit window after production.
options[].estimated_delivery.is_estimatebooleanAlways true: these are estimates, never a promise.
Always
trueoptions[].surchargesobject[]requiredCompatibility itemisation of charges already inside
cost_cents, never added to it. Current quotes contain no seasonal peak-postage surcharge: PrintedSimply absorbs the carrier expense, so this array is empty.options[].surcharges[].codestringrequiredpeak_season: the carrier's temporary holiday price change.Always
peak_seasonoptions[].surcharges[].reasonstringrequiredWhat it is, in words.
options[].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
options[].surcharges[].windowobjectrequiredThe days it applies on, both inclusive.
options[].surcharges[].window.fromstring (date)requiredYYYY-MM-DD, UTC.
options[].surcharges[].window.throughstring (date)requiredYYYY-MM-DD, UTC.
options[].surcharges[].sourcestring (uri)requiredWhere the amount and the dates come from: the carrier's own announcement.
options[].surcharges[].included_instringrequiredAlways
shipping: the figure this is part of. Never an additional charge.Always
shippingdeliver_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.
deliver_by_check.advisories[].idstringrequiredadv_….deliver_by_check.advisories[].carrierstringrequiredThe carrier that posted it, as it names itself, e.g. "USPS".
deliver_by_check.advisories[].statesstring[]requiredUSPS state codes it touches. With
zip_prefixesboth empty, it touches every US destination.deliver_by_check.advisories[].zip_prefixesstring[]requiredZIP prefixes (one to five digits) it touches, beside
states.deliver_by_check.advisories[].fromstring (date)requiredThe first day it applies, YYYY-MM-DD in UTC, inclusive.
deliver_by_check.advisories[].throughstring (date)requiredThe last day it applies, YYYY-MM-DD in UTC, inclusive.
deliver_by_check.advisories[].activebooleanrequiredWhether today (UTC) is inside
from–through.deliver_by_check.advisories[].extra_daysintegerrequiredThe carrier's estimate of the business days it adds, as posted. An estimate.
deliver_by_check.advisories[].messagestringrequiredOne sentence about it.
deliver_by_check.advisories[].sourcestringrequiredThe carrier's own notice, as a URL.
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.
The same fields as
deliver_by_check.advisories
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": {
"destination": {
"zip": "78701",
"country": "US"
},
"total_items": 100,
"shipping_class": {
"name": "parcel_1lb",
"weight_oz": 12.6
},
"options": [
{
"method": "standard",
"label": "Standard Shipping",
"cost": "$8.99",
"cost_cents": 899,
"estimated_delivery": {
"earliest": "2026-10-20",
"latest": "2026-10-26",
"ships_by": "2026-10-14",
"is_estimate": true,
"transit_days": "4-8 business days"
},
"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.
destination_unavailable — the destination is outside the US, and this estimate prices our own plant's shipping, which goes to US addresses only.
details.reasonisdestination_not_usanddetails.countrynames the destination. 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.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.