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). Standard shipping uses the configured light-parcel classes and published whole-pound weight rows above five pounds, with the applicable tube addition. The existing single-sticker cap and exact flag/hat fulfillment benchmarks are separate policies. There is no customer peak surcharge. Sized own-plant baskets above the estimated70lb single-parcel limit are refused before ordering; no multi-parcel plan is implied. Weight is an estimate and does not establish measured packed mass or dimensional-weight eligibility. 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. The returned shipping cost 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://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
fulfillment_policyobjectOptional exact-five approved fulfillment disclosure. Applies only to the four listed double-sided flags and one Santa hat, each a separate quantity-1 US standard order. Actual manufacturing expense and contribution remain unknown; the shipping benchmark includes packing. Supplies no live mapping by itself.
fulfillment_policy.fulfillment_cost_basisstringrequiredApproved per-size US standard shipping benchmark; actual postage, manufacturing cost and profit remain unknown.
Always
approved_size_shipping_benchmarkfulfillment_policy.goods_cost_centsanyrequiredActual manufacturing cost is unknown, never represented by zero.
fulfillment_policy.manufacturing_cost_centsanyrequiredfulfillment_policy.actual_contribution_centsanyrequiredfulfillment_policy.maximum_quantityintegerrequiredOne product, one SKU, quantity 1 in its own order.
Always
1fulfillment_policy.shipping_constraintsobjectrequiredfulfillment_policy.shipping_constraints.destination_countrystringrequiredAlways
USfulfillment_policy.shipping_constraints.methodstringrequiredAlways
standardfulfillment_policy.shipping_constraints.separate_orderbooleanrequiredAlways
truefulfillment_policy.shipping_benchmark_centsintegerrequiredThe approved quantity-1 US standard benchmark, packing included.
at least 0
fulfillment_policy.packagingstringrequiredAlways
included_in_shipping_benchmarkfulfillment_policy.artwork_layoutstringrequiredAlways
prepared_combined_allfulfillment_policy.artwork_instructionsstringrequiredPrepare one combined ALL PNG: flag front and back, or Santa hat front and band, according to the exact guide; remove guide lines.
fulfillment_policy.artwork_guide_urlstringrequiredLocal path to the layout guide SVG. This reference is not customer artwork and must not be sent as print_file.
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_classobject | nullThe 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. Null for the exact-five approved benchmark policy; no weight is inferred.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_deliveryobject | nullrequiredAn 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). Null for the exact-five approved benchmark policy, which establishes no production/transit dates.
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": "$6.50",
"cost_cents": 650,
"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.