Headless API reference.
Connect any website or application to a Print Shop Command Center shop: read the catalog, get a server-computed quote, submit an order, check its status, and capture leads. Seven endpoints, one header, plain JSON.
1. What this is
Every shop on Print Shop Command Center can issue API keys from its own console. A key lets an outside system talk to that one shop: pull the products it sells, ask the shop's own pricing engine what a cart costs, and place an order against the resulting price. Prices are always computed on the server from the shop's configuration — a client sends products, quantities and options, never money. The shop is resolved from the key itself, so there is no shop id parameter anywhere in this API.
2. Base URL and authentication
All endpoints live under one base URL. Send your key in the X-Pscc-Key header on every request; header names are case-insensitive.
https://www.printshopcommandcenter.com/api/v1
curl https://www.printshopcommandcenter.com/api/v1/shop \
-H "X-Pscc-Key: pscc_pk_00000000000000000000000000000000"Keys come in two shapes, distinguishable by prefix:
| Kind | Format | Length after prefix |
|---|---|---|
| publishable | pscc_pk_ | 32 hex characters |
| secret | pscc_sk_ | 48 hex characters |
Request bodies are JSON and must stay under 64 KiB. Include Content-Type: application/json on every POST.
3. Publishable keys vs secret keys
Publishable keys (pscc_pk_) are intended for browser code and are gated by an origin allowlist you set in the console. An empty allowlist allows any origin. A non-empty allowlist rejects any request that does not arrive with a permitted Origin header — and a server-to-server call sends no Origin at all, so using such a key from a backend fails with origin_not_allowed every time. The allowlist is not something a server call bypasses.
Secret keys (pscc_sk_) are for server-side code. They are never origin-gated, and they get an elevated read limit on the three read endpoints (see rate limits). Keeping a secret key off the browser is guidance we cannot technically enforce — nothing stops a secret key being pasted into front-end code, and nothing about the key itself will complain. If it can be seen by a visitor, treat it as compromised and revoke it.
4. The endpoints
| Endpoint | Purpose | Needs online ordering enabled |
|---|---|---|
GET /shop | The shop profile, turnaround settings, fulfillment methods, checkout questions and catalog. | no |
GET /products | The catalog on its own. | no |
GET /products/{style} | One catalog entry by its public style. | no |
POST /quote | Server-priced dry run of a cart. Writes nothing. | yes |
POST /orders | Submit an order for the shop to approve. | yes |
GET /orders/{trackingToken} | Read an order by its tracking token. | no |
POST /leads | Capture a lead into the shop's inbox. | no |
GET /shop
Returns { "shop": { ... } }. This is where every shop-specific input for a quote lives: turnaround.standardBusinessDays and turnaround.rushTiers, fulfillment.pickup|delivery|shipping (each method with id, label, fee, appliesTo, active), checkout.fields, money, and catalog. Start here.
GET /products and GET /products/{style}
{ "products": [ ... ] } and { "product": { ... } }. Each entry carries style, name, colors, sizes, allowedMethods, allowedPlacements, maxPlacements, canQuote, contactOnly, contactHref, category, image and description. The product identity used everywhere in this API — the path segment, and productKey in a cart — is the public style, never a database id.
Read contactOnly, not canQuote
Products served from the shop's products table always report canQuote: true, and mark unquotable ones with contactOnly: true instead, so they stay visible on a storefront with a contact link. Filter on contactOnly. A cart containing a contact-only product is refused by /quote.
POST /quote
Body is a cart:
- items
- 1 to 50 lines. Each:
productKey(required),sizeRun(required, an object of size to whole-number quantity),color,placements(up to 20, each{ key, artUploadId? }),method,colorCount(1-64),specialInstructions(up to 2000 characters). - fulfillment
- Required.
{ group: "pickup" | "delivery" | "shipping", methodId }— both discovered fromGET /shop. - turnaround
- Required.
{ kind: "standard" }or{ kind: "rush", tierId }.
Returns { "cart": { ... }, "reviewedPrice": { ... } }. The cart carries the priced lines, subtotals, tax, fulfillment fee, order charges, total and dueDate. The reviewedPrice object is a server-signed statement of the price you were shown; keep it and echo it back on submit.
POST /orders
Everything /quote takes, plus:
- clientOperationId
- Required. Your own idempotency key for this submit attempt. Retrying with the same id, the same guest token and identical content returns the original order at
200withreplayed: trueinstead of creating a second one. - reviewedPrice
- Required in practice. Echo the object from
/quoteverbatim. This is the price-tamper precondition: the server re-prices the cart and refuses the submit if the two disagree. - guestCheckoutToken
- Required in practice — see the note below.
- customer
- Required.
{ name, email, phone? }. - fulfillmentAddress
- Required whenever the fulfillment group is
deliveryorshipping.{ line1, line2?, city, region, postalCode, country }. A pickup order discards any address you send. - checkoutFields
- Required whenever the shop configured any. One answer per field in
checkout.fields, exactly once each. - notes
- Optional. Up to 2000 characters.
Returns 201 with { orderId, orderNumber, trackingUrl, trackingToken, replayed } on a first submit, or 200 with replayed: true on a replay. trackingUrl and trackingToken can be null: issuing the tracking link is best-effort and never fails an order.
guestCheckoutToken is required, and you generate it yourself
The field is marked optional in the request schema, but every v1 submit takes the guest path, and the guest path refuses a submit without it: 400 validation_failed, message Refresh checkout and try again. There is no endpoint that issues one — generate your own UUID v4 (the hosted storefront does exactly this).
The two identifiers do different jobs. guestCheckoutToken is who the guest is: it is hashed server-side and becomes the order's actor identity, so keep it stable for the whole checkout session. clientOperationId is which submit attempt this is. Change the token between retries and the replay check sees a different actor and rejects the reused operation id.
GET /orders/{trackingToken}
Returns shop, order, timeline, items, showPrices and jobProgress. Every refusal — malformed token, unknown token, a token belonging to another shop, a revoked token, or a shop with tracking switched off — returns the same uniform envelope with no message:
{
"error": "not_found"
}That is deliberate. The endpoint is not an oracle for probing which tokens exist.
POST /leads
Body: email (required), name, phone, selections (required — garmentPositionId, colorChoice as 1-12 or "FULL_COLOR", qty, plus optional locations, dark, turnaroundPositionId, personalization, namedLocations), and optional artworkReview. Returns 201 with { leadId }.
Two lead fields are accepted and then discarded
selections.turnaroundFasterThanFastest and selections.locationColors pass validation on this endpoint but are stripped before storage and never appear on the lead. They exist on the shared schema for the hosted widget, which validates them against its own configuration. Omit them. Do not build behaviour on them.
5. Building a cart that will actually price
Validation passing is not the same as pricing succeeding. The request schema is deliberately permissive; the shop's pricing engine is not. These are the rules a client has to follow to get past it:
- Skip
contactOnlyproducts entirely. They cannot be quoted. - Use only the
sizes,allowedMethodsandallowedPlacementsa product actually exposes, and respectmaxPlacements. Size keys are matched case-sensitively. A product with no configured sizes accepts exactly one key:OS. - Supply
methodwhenever the product allows more than one and nothing in the request implies a choice. The server refuses to guess. - Supply
colorCounton every line that resolves to screen printing. It is mandatory whether you namedscreenyourself or the engine resolved it. Screen is the most common method in a print shop, so in practice most lines need it. - Answer every field in
checkout.fieldsexactly once, with a response whosekindmatches the field's. - Supply
fulfillmentAddresswhenever the chosen fulfillment group isdeliveryorshipping. - Read fulfillment methods fresh from
GET /shop. A method can be inactive, or restricted byappliesToto a set of products that does not cover your whole cart.
Every rejection you can provoke
The list below is the complete set of refusals on the quote and order path, enumerated from the pricing and submission code rather than summarised. It is grouped by what you can do about it.
Field omitted
Fixable by sending one more field. Several of these are optional in the request schema and required at runtime, so validation passes and the request still fails.
| Status | Envelope | Internal code | What provokes it |
|---|---|---|---|
| 401 | invalid_key | invalid_key | No X-Pscc-Key header at all, a key whose prefix/length is wrong, or a key whose hash matches no active row. |
| 400 | validation_failed | BAD_REQUEST | Any field the cart schema requires is missing: items (1-50), productKey, sizeRun, fulfillment.group, fulfillment.methodId, turnaround.kind — and tierId when the turnaround is rush. |
| 400 | validation_failed | BAD_REQUEST | On orders only: clientOperationId, customer.name and customer.email are all required by the schema. |
| 400 | validation_failed | FULFILLMENT_ADDRESS_REQUIRED | The fulfillment group is delivery or shipping and fulfillmentAddress was omitted. The field is optional in the schema and required at runtime. A pickup order discards any address you send. |
| 400 | validation_failed | REVIEWED_PRICE_REQUIRED | reviewedPrice is missing, or does not match its strict schema. Echo the object from POST /quote verbatim — the compare schema rejects extra keys, so a reshaped or partial copy fails here. |
| 400 | validation_failed | GUEST_IDENTITY_REQUIRED | guestCheckoutToken was omitted. It is optional in the schema, but every v1 submit takes the guest path, so it is always required in practice. Generate your own UUID v4 — no endpoint issues one. |
| 400 | validation_failed | CANNOT_PRICE | sizeRun sums to zero. The schema allows a quantity of 0, so an all-zero size run passes validation and fails here. |
| 400 | validation_failed | CANNOT_PRICE | A multi-method product with no method supplied and nothing for the engine to infer from: the quote is refused rather than defaulted (engine reason: method needs human review). |
| 400 | validation_failed | CANNOT_PRICE | The line resolved to screen printing and colorCount was omitted. Mandatory for every screen line, whether you named the method or the engine resolved it. |
| 400 | validation_failed | CHECKOUT_FIELD_INVALID | checkoutFields must answer every field GET /shop lists under checkout.fields, exactly once each. Omitting the key entirely fails whenever the shop configured at least one field. |
Wrong value
Fixable by sending a different value. The field was present and well-formed, but wrong for this shop or this product.
| Status | Envelope | Internal code | What provokes it |
|---|---|---|---|
| 403 | origin_not_allowed | origin_not_allowed | A publishable key with a non-empty allowlist, called with a missing or unlisted Origin. A server-to-server call sends no Origin at all, so such a key ALWAYS fails from a server. |
| 413 | validation_failed | validation_failed | A request body over 64 KiB. |
| 400 | validation_failed | validation_failed | A body that is not parseable JSON. |
| 405 | validation_failed | validation_failed | An unsupported verb on a v1 path (for example GET /quote). |
| 400 | validation_failed | BAD_REQUEST | Any bound broken: colorCount outside 1-64, a sizeRun quantity outside 0-100000, more than 20 placements, method over 50 chars, specialInstructions over 2000, an unknown fulfillment group. |
| 400 | validation_failed | BAD_REQUEST | On orders only: guestCheckoutToken must be a UUID; a checkoutFields entry must match its declared kind exactly (agreement carries checked, dropdown and text carry value). |
| 400 | validation_failed | FULFILLMENT_NOT_AVAILABLE | The (group, methodId) pair does not resolve, the method is inactive, or the method's appliesTo list does not cover EVERY product key in the cart. Read current methods from GET /shop; never hardcode a methodId. |
| 400 | validation_failed | INVALID_RUSH_TIER | The rush tierId matches no configured tier. GET /shop never returns a tierId — you derive it as rush-{withinDays}. |
| 400 | validation_failed | CANNOT_PRICE | productKey matches nothing in the shop's catalog. |
| 400 | validation_failed | CANNOT_PRICE | A sizeRun key carrying a quantity is not one of the product's sizes. Matching is case-sensitive. A product with no configured sizes accepts exactly one key: OS. |
| 400 | validation_failed | CANNOT_PRICE | The same placement key appears twice on one item. |
| 400 | validation_failed | CANNOT_PRICE | A placement key is not in the product's allowedPlacements. |
| 400 | validation_failed | CANNOT_PRICE | More placements than the product's maxPlacements (4 when the product does not set one). |
| 400 | validation_failed | CANNOT_PRICE | method names a decoration the product does not allow — or, under a shop's merged method display, a bucket literal (Printing / Embroidery) that contains none of the product's allowed methods. |
| 400 | validation_failed | CANNOT_PRICE | color is not one of the product's listed colors (engine reason: garment color not available). |
| 409 | validation_failed | STALE_REVIEWED_PRICE | The echoed reviewedPrice does not match a fresh recomputation. Triggered by shop pricing changing between quote and submit, and equally by changing ANY priced detail (items, method, colorCount, placements, fulfillment, turnaround) after quoting. Re-quote and resubmit. |
| 400 | validation_failed | CHECKOUT_FIELD_INVALID | An answer names an unconfigured id, declares the wrong kind, leaves a required agreement unchecked, leaves a required dropdown or text blank, or sends a dropdown value that is not one of the field's current options. |
| 409 | validation_failed | CLIENT_OPERATION_ID_CONFLICT | A clientOperationId was reused with different content, or with a different guestCheckoutToken. Reusing it with identical content and the same token is the safe retry and returns the original order at 200 with replayed true. |
Shop configuration
Not fixable by any request body. The shop owner has to change a setting. Some of these conditions are not visible anywhere in the API, so the rejection message is the only place the constraint appears.
| Status | Envelope | Internal code | What provokes it |
|---|---|---|---|
| 503 | shop_unavailable | shop_unavailable | The key is valid but the shop record could not be read right now. Retryable. |
| 403 | shop_unavailable | shop_unavailable | The shop's subscription is suspended. Every endpoint goes dark. |
| 403 | shop_unavailable | shop_unavailable | Online ordering is switched off for the shop. Affects POST /quote and POST /orders ONLY — catalog, order status and leads stay up. |
| 404 | not_found | SHOP_NOT_FOUND | The shop has no readable pricing configuration. |
| 400 | validation_failed | CANNOT_PRICE | The shop's currency settings are unusable (code must be three letters, symbol 1-12 characters). |
| 400 | validation_failed | RUSH_NOT_OFFERED | A rush turnaround was requested but the shop has rush switched off (turnaround.rushOffered is false in GET /shop). |
| 400 | validation_failed | CANNOT_PRICE | The product is contact-only. Note the trap: the public catalog reports canQuote true on every products-table row and marks unquotable ones with contactOnly true — skip on contactOnly, never on canQuote (engine reason: no quotable product matched). |
| 400 | validation_failed | CANNOT_PRICE | The shop requires a fact before quoting that the v1 cart cannot carry. In particular a shop whose quote requirements demand ARTWORK before quoting rejects every v1 quote and order — there is no artwork field on this API, so no request body can satisfy it. A shop demanding garment colour before quoting makes the optional color field mandatory (engine reason: missing required info). |
| 400 | validation_failed | CANNOT_PRICE | A shop routing rule matched and sends this configuration to a human, or routes it to a method that cannot price. Routing rules are not exposed by the API (engine reason: contact_shop). |
| 400 | validation_failed | CANNOT_PRICE | The resolved method is switched off at the shop level. GET /shop does not expose the shop's method settings, so a product's allowedMethods can name a method the shop has disabled — this is only discoverable by being rejected. |
| 400 | validation_failed | CANNOT_PRICE | colorCount is above the shop's screen colour capacity. That capacity is not exposed by the API — only the rejection message names it. |
| 400 | validation_failed | CANNOT_PRICE | The quantity is below the effective minimum for the resolved method (the product's own minimum, otherwise the method's). Neither minimum is exposed by GET /shop or GET /products — the rejection message is the only place the number appears. |
| 400 | validation_failed | CANNOT_PRICE | A line, or the whole order, priced to zero or less. The server fails closed rather than returning a free order. |
| 503 | server_error | CHECKOUT_MIGRATION_REQUIRED | Checkout is temporarily unavailable while the shop is being updated. Retryable. |
Transport / volume
Volume or envelope problems, not content problems. Back off and retry.
| Status | Envelope | Internal code | What provokes it |
|---|---|---|---|
| 429 | rate_limited | rate_limited | More than 240 requests/minute from one IP, counted before the key is even looked up — so this can fire on a missing or invalid key. |
| 429 | rate_limited | rate_limited | The per-key bucket for this endpoint, or (on orders and leads) the parallel per-(IP, shop) bucket. |
| 500 | server_error | SERVER_ERROR | The shop's products could not be read. Pricing fails closed rather than quoting against stale data. Retryable. |
Three constraints the API does not expose
A cart can be refused for a limit you have no way to read in advance. Minimum order quantities (the product's own, otherwise the resolved method's), the shop's screen-print colour capacity, and whether a decoration method is switched on at the shop level are all absent from GET /shop and GET /products. A product's allowedMethods can name a method the shop has disabled. When one of these bites, the rejection message names the actual number or method — surface that message to your user rather than a generic failure.
6. Errors
Every error is the same envelope: a stable error code and an optional human-readable message. Treat message as display copy that may change; branch on error and the HTTP status.
{
"error": "validation_failed",
"message": "Review every checkout question and try again."
}| Code | Statuses | When |
|---|---|---|
invalid_key | 401 | No X-Pscc-Key header, a key that is not a valid shape, or a hash that matches no active key. Also returned platform-wide when the API is not yet enabled. |
origin_not_allowed | 403 | A publishable key with a non-empty origin allowlist, called without a permitted Origin header. |
rate_limited | 429 | Either the pre-auth per-IP ceiling or the per-endpoint bucket. Read X-RateLimit-Limit to tell which. |
validation_failed | 400, 405, 409, 413 | Malformed JSON, a body over the size cap, an unsupported verb, or a request that did not validate against current server facts. |
not_found | 404 | Unknown product style, unknown/foreign/revoked tracking token, or a shop whose configuration could not be read. |
shop_unavailable | 403, 503 | The shop is suspended, its record is temporarily unreadable, or online ordering is switched off (quote and orders only). |
server_error | 500, 503 | An upstream failure. Safe to retry. |
7. Rate limits
Per key, per minute:
| Endpoint | Publishable key | Secret key | Parallel per-IP |
|---|---|---|---|
GET /shop | 60 | 120 | — |
GET /products | 60 | 120 | — |
GET /products/{style} | 60 | 120 | — |
POST /quote | 30 | 30 | — |
POST /orders | 5 | 5 | 5 |
GET /orders/{trackingToken} | 30 | 30 | — |
POST /leads | 5 | 5 | 5 |
Order submission and lead capture also carry a parallel per-IP bucket scoped to the shop, so a leaked publishable key cannot spread abuse across arbitrary clients.
Separately, and independently of any key, there is a ceiling of 240 requests per minute per IP that is charged before the key is looked up. It can return 429 for a request carrying a missing or invalid key.
Responses carry X-RateLimit-Limit and X-RateLimit-Remaining, plus Retry-After on a 429 only. There is no X-RateLimit-Reset. All three are named in Access-Control-Expose-Headers, so browser clients can read them.
Rate headers are absent from four specific responses, because they are decided before any per-endpoint bucket is consulted: 401 invalid_key, 403 origin_not_allowed, and both shop_unavailable forms (403 and 503). The pre-auth 429 is not in that group — it carries all three.
8. CORS
Preflight (OPTIONS) requests are unauthenticated — browsers do not send custom headers on a preflight, so these validate syntax only and return 204 with Content-Type and X-Pscc-Key allowed. Authentication and the origin allowlist gate the actual request.
On the actual response, Access-Control-Allow-Origin echoes your Origin when a publishable key's allowlist permits it, and is * when the allowlist is empty. Secret-key responses echo Origin when one is present. Credentials are never allowed, and every response is Cache-Control: private, no-store.
9. Worked example
A complete pass: read the shop, price a cart, submit it, track it. Every id below is illustrative. Fulfillment method ids, product styles, turnaround tiers and checkout field ids are all per-shop configuration — a hard-coded methodId will fail against any other shop. Discover current values from GET /shop on each run rather than persisting the ones printed here.
Step 1 — GET /shop
curl https://www.printshopcommandcenter.com/api/v1/shop \
-H "X-Pscc-Key: pscc_sk_000000000000000000000000000000000000000000000000"Abridged to the fields this example uses:
{
"shop": {
"_public": true,
"profile": {
"name": "Your Print Shop"
},
"money": {
"symbol": "$",
"code": "USD"
},
"turnaround": {
"standardBusinessDays": 10,
"rushOffered": true,
"rushTiers": [
{
"withinDays": 5,
"feePct": 25
},
{
"withinDays": 8,
"feePct": 10
}
]
},
"fulfillment": {
"pickup": [
{
"id": "pickup-counter",
"label": "Pick up at the shop",
"fee": 0,
"appliesTo": "all",
"active": true
}
],
"delivery": [],
"shipping": [
{
"id": "ship-ups-ground",
"label": "UPS Ground",
"fee": 14.5,
"appliesTo": "all",
"active": true
}
]
},
"checkout": {
"fields": [
{
"id": "artwork-ok",
"label": "Artwork approval",
"kind": "agreement",
"required": true,
"text": "I approve the artwork as supplied and understand print colours may vary slightly."
}
]
},
"catalog": [
{
"style": "5000",
"name": "Gildan 5000 Heavy Cotton Tee",
"colors": [
"White",
"Black",
"Safety Orange",
"Navy",
"Red"
],
"sizes": [
"S",
"M",
"L",
"XL",
"2XL",
"3XL"
],
"allowedMethods": [
"screen",
"dtg",
"dtf"
],
"allowedPlacements": [
"front",
"back",
"left_chest",
"left_sleeve",
"right_sleeve"
],
"maxPlacements": 4,
"canQuote": true
}
]
}
}That response is what supplies the next request: a quotable product style, its sizes, colours and allowed methods; an active fulfillment group and methodId; the turnaround settings; and the checkout question that must be answered at submit.
Rush turnaround: you have to derive the tierId
GET /shop returns rush tiers as { withinDays, feePct } and never returns a tierId, but /quote and /orders require turnaround: { kind: "rush", tierId }. The id is derived as rush-{withinDays}. For this shop:
withinDays: 5 (+25%) -> tierId: "rush-5"
withinDays: 8 (+10%) -> tierId: "rush-8"So the 5-business-day tier is { "kind": "rush", "tierId": "rush-5" }. This example uses standard turnaround below; the derivation is the same whenever you want rush.
Step 2 — POST /quote
One screen-printed line. Note colorCount: without it this exact request is refused, because the line resolves to screen.
curl -X POST https://www.printshopcommandcenter.com/api/v1/quote \
-H "X-Pscc-Key: pscc_sk_000000000000000000000000000000000000000000000000" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"productKey": "5000",
"color": "Black",
"sizeRun": {
"M": 12,
"L": 24,
"XL": 12
},
"placements": [
{
"key": "front"
}
],
"method": "screen",
"colorCount": 2
}
],
"fulfillment": {
"group": "shipping",
"methodId": "ship-ups-ground"
},
"turnaround": {
"kind": "standard"
}
}'The response is { "cart": { ... }, "reviewedPrice": { ... } }. Show cart.total and cart.dueDate to the customer; keep reviewedPrice untouched for the next step.
Step 3 — POST /orders
The same cart, plus identity, idempotency and the echoed price. fulfillmentAddress is present because the fulfillment group is shipping; checkoutFields answers the one question the shop configured; guestCheckoutToken is a UUID v4 this client generated itself.
curl -X POST https://www.printshopcommandcenter.com/api/v1/orders \
-H "X-Pscc-Key: pscc_sk_000000000000000000000000000000000000000000000000" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"productKey": "5000",
"color": "Black",
"sizeRun": {
"M": 12,
"L": 24,
"XL": 12
},
"placements": [
{
"key": "front"
}
],
"method": "screen",
"colorCount": 2
}
],
"fulfillment": {
"group": "shipping",
"methodId": "ship-ups-ground"
},
"turnaround": {
"kind": "standard"
},
"clientOperationId": "order-2026-07-19-0001",
"guestCheckoutToken": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"reviewedPrice": "<paste the reviewedPrice object from the /quote response, verbatim>",
"customer": {
"name": "Dana Whitfield",
"email": "dana@example.com",
"phone": "555-0142"
},
"fulfillmentAddress": {
"line1": "742 Press Ave",
"city": "Sandy",
"region": "UT",
"postalCode": "84070",
"country": "USA"
},
"checkoutFields": [
{
"id": "artwork-ok",
"kind": "agreement",
"checked": true
}
],
"notes": "Please pack the XLs separately."
}'Replace the reviewedPrice string with the object /quote returned, unchanged. On success you get 201:
{
"orderId": "0f2b1c74-1f4c-4f2e-9c0a-6d1b2e3f4a5b",
"orderNumber": 1042,
"trackingUrl": "https://example-shop.printshopcommandcenter.com/qq/track/Ab3...",
"trackingToken": "Ab3...",
"replayed": false
}Step 4 — GET /orders/{trackingToken}
curl https://www.printshopcommandcenter.com/api/v1/orders/Ab3... \
-H "X-Pscc-Key: pscc_sk_000000000000000000000000000000000000000000000000"The token is scoped to the key's own shop. A token from another shop returns the same not_found envelope as a token that does not exist.
10. Troubleshooting
Everything returns 401 invalid_key
Check the obvious first: the header name is X-Pscc-Key, the key must be the full raw string shown once at creation (not the truncated prefix the console lists afterwards), and a revoked key stops working immediately. But there is a second cause that looks identical: if the headless API is not yet enabled on the platform, every key returns 401 invalid_key, because the key lookup itself cannot resolve. If a freshly created key fails on its first ever call, ask the shop to confirm the API is switched on before hunting for a bug in your client.
I am getting 429s
Back off on any 429. Which bucket you hit is identifiable from the value of X-RateLimit-Limit:
240— the pre-auth per-IP ceiling. Charged before the key is read, so even an invalid key can be limited this way. Usually means one host is making far too many calls, not that your key is over quota.- 120 / 60 / 30 / 5 — a per-endpoint bucket. Compare it against the rate-limit table to see which one.
Both share the same envelope and the same three header names. Only the limit value distinguishes them, and the pre-auth 429 does carry all three headers — its rate headers are not missing.
My quote returns 400 validation_failed
Read the message. The pricing engine names the specific problem — the product, the size, the method, the minimum quantity, or the colour capacity. Then check the rejection catalog: if the condition is in the shop-configuration group, no change to your request will fix it and the shop owner has to act.
My order returns 409 with a stale price
The cart is re-priced at submit and compared against the reviewedPrice you echoed. This fires when the shop's pricing changed between the two calls, and equally when your own request changed — the comparison covers items, methods, colour counts, placements, fulfillment and turnaround, not just the total. Re-run /quote with the current cart and submit the fresh reviewedPrice.
My order returns 400 about checkout questions
checkoutFields must contain exactly one answer per field in checkout.fields — no more, no fewer, no duplicates — and each answer's kind must match the field's. Omitting the key entirely counts as answering nothing, which fails for any shop with a required field. Dropdown values must be one of the field's current options, so re-read GET /shop rather than caching a question set indefinitely.
Manage keys in your shop console under Settings, API keys. Questions: integrations@printshopcommandcenter.com