Retrieve details and pricing for a specific product — its price fields as on GET /v1/products: the list price (our price book's, for a product of our own plant's that the book prices), the same on every plan since there is no platform fee — from the catalog of the facility whose capabilities cover it (a sticker SKU is answered by the flat-goods facility; apparel by the default facility). Pass provider to ask one facility's catalog explicitly. Returns 404 when that catalog has no such SKU — 'does this SKU exist?' is answerable here before ordering. When the facility gives no answer about the SKU (5xx, a refused credential, a timeout, a rate limit, or an open circuit), the row is served out of the last-good snapshot with X-Catalog-Stale: true, as GET /v1/products does — so the list and this read agree during an outage. A SKU the snapshot does not hold is a 502, never a 404: a stale list missing a row says nothing about a product added since. On our own plant's catalog, a SKU our price book lists is served from the book instead, as the list serves it (source: "price_book", X-Catalog-Stale: true), and so is another size of a product it lists, spelled as the list spells it (STICKER-DIE-CUT-2X2 beside the listed 3×3), which a test key's quote and order take at $0 while the plant does not answer. A stale answer is sent Cache-Control: private, no-store, as on the list.
- Credential
- An API key in
x-api-key - Scopes
products:read
Parameters
skupath · stringrequiredProduct SKU identifier
providerquery · stringWhich catalog to ask:
printedsimply, our own plant's. It is the default and the only value; anything else is a 400.Always
printedsimplyDefault printedsimply
Request
GET /api/v1/products/{sku}
curl "https://portal.printedsimply.com/api/v1/products/sticker-die-cut-3x3" \
-H "x-api-key: ps_test_YOUR_KEY"Response 200
The catalog row for this SKU. data is the facility's own row, unwrapped — the same shape as one element of GET /v1/products, and open for the same reason. Note that category appears here only if the facility publishes one: the derivation that fills it runs on the list, not on this route. May have been served out of the last-good snapshot — read X-Catalog-Stale, which is sent both ways; a stale row from the local synced catalog carries only sku, name and priceCents, and one from our price book sku, name, base_price, category and source: "price_book" (with alt_skus where the book knows the product by another SKU too). Every row our own plant makes, whatever its source, also carries size_in, finish, family, minimum_quantity, quantity_breaks, orderable, not_orderable_code and not_orderable_reason, as on the list.
data
skustring | numberThe identifier every other operation takes: an order item's
sku, the{sku}path parameter, a recommendation'ssku. Numeric on some facility catalogs — compare it as a string.namestringdescriptionstringcategorystringThe facility's own category, where it publishes one — an open vocabulary, not this API's.
GET /v1/productsalways fills this field (seeProductListItem); by SKU it is present only if the facility sent it.sourcestringPresent only on a row this API built from our own price book because our plant's catalog had nothing of its own to serve (the answer is then
X-Catalog-Stale: true, withX-Catalog-Degradednaming why). Its price is the book's, which is what an order of the SKU is charged; the row carriessku,name,base_priceandcategory. Absent on every row a facility sent.Always
price_booksize_innumber[] | nullOur plant's rows only: [width, height] in inches, read from the size the SKU names (the size the price book and an order price). Null when the SKU names no size, as a sticker sheet or roll does.
2 to 2 entries
finishstring | nullOur plant's rows only: the finish the plant's row names, else the one the SKU names (glossy, matte, holographic, clear, satin); null when neither names one. No finish changes the price.
familystringOur plant's rows only: which of its product families the SKU is, the family its quantity ladder and its cost are read under.
One of
stickersdecalsmagnetspostersminimum_quantityintegerOur plant's rows only: the fewest units of this SKU one order line takes, which is 1. A run of one is an order, priced at the first
quantity_breaksentry; POST /v1/order refuses a quantity below it with 400 validation_error.Always
1quantity_breaksobject[]Our plant's rows only: the unit price an order of this SKU charges, from one unit up, one entry per quantity where it changes — computed by the function POST /v1/orders/quote and POST /v1/order charge each unit with (no platform fee is added on any plan). The first entry (
min_qty1) is the row's own price. A single entry when the SKU climbs no ladder (a SKU our price book does not price). Counted on an order's whole quantity of the SKU, across its lines. A negotiated price for your account takes no break and replaces these. Empty when the row carries no usable price (not_orderable_code: no_price).quantity_breaks[].min_qtyintegerrequiredFrom this many units of the SKU on one order (counted across its lines).
at least 1
quantity_breaks[].unit_centsintegerrequiredThe unit price, in cents, an order of
min_qtyor more charges, until the next entry'smin_qty.orderablebooleanOur plant's rows only: true when nothing POST /v1/orders/quote checks before pricing would refuse this SKU right now. False says why in
not_orderable_reason(a sentence) andnot_orderable_code(a code). A hint for a cart, read when the catalog was: the quote and the order decide.not_orderable_reasonstring | nullWhy not, in the order path's own words — the same field, meaning the same thing, as on every other row of this endpoint that says
orderable. Null whenorderable. On our plant's rowsnot_orderable_codeis the same answer as a code.not_orderable_codestring | nullOur plant's rows only: null when
orderable; otherwise why not, as a code, in the order the quote meets them.facility_not_configured— the plant has no credentials in this environment (a live quote or order answers 503 provider_unavailable; a test key's degrades to $0 markedpricing_degraded).plant_unreachable— the plant's catalog did not answer this read (the row is a snapshot or the price book's); a quote, estimate or order asks the plant about the SKU, and answers 503 provider_unavailable withRetry-After(details.reason: plant_unreachable) until it does.plant_price_held— the plant's prices are held after one of its launch goods read far above its book price, and this SKU has no book price to show it is not wrong the same way; the quote refuses it until the hold lifts.no_price— the row carries no usable list price and our price book does not price the SKU, so the quote refuses it.One of
facility_not_configuredplant_unreachableplant_price_heldno_price
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": {
"sku": "sticker-die-cut-3x3",
"name": "Die-Cut Sticker, 3×3 in",
"price": "2.67",
"regular_price": "2.67",
"category": "stickers",
"purchasable": true,
"stock_status": "instock",
"size_in": [
3,
3
],
"finish": null,
"family": "stickers",
"minimum_quantity": 1,
"quantity_breaks": [
{
"min_qty": 1,
"unit_cents": 267
},
{
"min_qty": 10,
"unit_cents": 160
},
{
"min_qty": 25,
"unit_cents": 104
},
{
"min_qty": 50,
"unit_cents": 72
},
{
"min_qty": 100,
"unit_cents": 64
}
],
"orderable": true,
"not_orderable_code": null,
"not_orderable_reason": null
},
"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).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.502The facility catalog gave no answer and no snapshot holds the SKU (nor, on our own plant's catalog, our price book) — or, with not_configured, the facility has no credentials in this environment, which is an operator fact rather than an outage.
X-Catalog-Degradedcarries the reason on every 502, so a failure is diagnosable rather than indistinguishable from an empty catalog. This is not a verdict on the SKU: retry, or readGET /v1/products.