HEADLESS API — VERSION 1

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:

KindFormatLength after prefix
publishablepscc_pk_32 hex characters
secretpscc_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

EndpointPurposeNeeds online ordering enabled
GET /shopThe shop profile, turnaround settings, fulfillment methods, checkout questions and catalog.no
GET /productsThe catalog on its own.no
GET /products/{style}One catalog entry by its public style.no
POST /quoteServer-priced dry run of a cart. Writes nothing.yes
POST /ordersSubmit an order for the shop to approve.yes
GET /orders/{trackingToken}Read an order by its tracking token.no
POST /leadsCapture 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 from GET /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 200 with replayed: true instead of creating a second one.
reviewedPrice
Required in practice. Echo the object from /quote verbatim. 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 delivery or shipping. { 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:

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.

StatusEnvelopeInternal codeWhat provokes it
401invalid_keyinvalid_keyNo X-Pscc-Key header at all, a key whose prefix/length is wrong, or a key whose hash matches no active row.
400validation_failedBAD_REQUESTAny 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.
400validation_failedBAD_REQUESTOn orders only: clientOperationId, customer.name and customer.email are all required by the schema.
400validation_failedFULFILLMENT_ADDRESS_REQUIREDThe 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.
400validation_failedREVIEWED_PRICE_REQUIREDreviewedPrice 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.
400validation_failedGUEST_IDENTITY_REQUIREDguestCheckoutToken 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.
400validation_failedCANNOT_PRICEsizeRun sums to zero. The schema allows a quantity of 0, so an all-zero size run passes validation and fails here.
400validation_failedCANNOT_PRICEA 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).
400validation_failedCANNOT_PRICEThe line resolved to screen printing and colorCount was omitted. Mandatory for every screen line, whether you named the method or the engine resolved it.
400validation_failedCHECKOUT_FIELD_INVALIDcheckoutFields 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.

StatusEnvelopeInternal codeWhat provokes it
403origin_not_allowedorigin_not_allowedA 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.
413validation_failedvalidation_failedA request body over 64 KiB.
400validation_failedvalidation_failedA body that is not parseable JSON.
405validation_failedvalidation_failedAn unsupported verb on a v1 path (for example GET /quote).
400validation_failedBAD_REQUESTAny 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.
400validation_failedBAD_REQUESTOn orders only: guestCheckoutToken must be a UUID; a checkoutFields entry must match its declared kind exactly (agreement carries checked, dropdown and text carry value).
400validation_failedFULFILLMENT_NOT_AVAILABLEThe (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.
400validation_failedINVALID_RUSH_TIERThe rush tierId matches no configured tier. GET /shop never returns a tierId — you derive it as rush-{withinDays}.
400validation_failedCANNOT_PRICEproductKey matches nothing in the shop's catalog.
400validation_failedCANNOT_PRICEA 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.
400validation_failedCANNOT_PRICEThe same placement key appears twice on one item.
400validation_failedCANNOT_PRICEA placement key is not in the product's allowedPlacements.
400validation_failedCANNOT_PRICEMore placements than the product's maxPlacements (4 when the product does not set one).
400validation_failedCANNOT_PRICEmethod 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.
400validation_failedCANNOT_PRICEcolor is not one of the product's listed colors (engine reason: garment color not available).
409validation_failedSTALE_REVIEWED_PRICEThe 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.
400validation_failedCHECKOUT_FIELD_INVALIDAn 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.
409validation_failedCLIENT_OPERATION_ID_CONFLICTA 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.

StatusEnvelopeInternal codeWhat provokes it
503shop_unavailableshop_unavailableThe key is valid but the shop record could not be read right now. Retryable.
403shop_unavailableshop_unavailableThe shop's subscription is suspended. Every endpoint goes dark.
403shop_unavailableshop_unavailableOnline ordering is switched off for the shop. Affects POST /quote and POST /orders ONLY — catalog, order status and leads stay up.
404not_foundSHOP_NOT_FOUNDThe shop has no readable pricing configuration.
400validation_failedCANNOT_PRICEThe shop's currency settings are unusable (code must be three letters, symbol 1-12 characters).
400validation_failedRUSH_NOT_OFFEREDA rush turnaround was requested but the shop has rush switched off (turnaround.rushOffered is false in GET /shop).
400validation_failedCANNOT_PRICEThe 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).
400validation_failedCANNOT_PRICEThe 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).
400validation_failedCANNOT_PRICEA 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).
400validation_failedCANNOT_PRICEThe 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.
400validation_failedCANNOT_PRICEcolorCount is above the shop's screen colour capacity. That capacity is not exposed by the API — only the rejection message names it.
400validation_failedCANNOT_PRICEThe 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.
400validation_failedCANNOT_PRICEA line, or the whole order, priced to zero or less. The server fails closed rather than returning a free order.
503server_errorCHECKOUT_MIGRATION_REQUIREDCheckout is temporarily unavailable while the shop is being updated. Retryable.

Transport / volume

Volume or envelope problems, not content problems. Back off and retry.

StatusEnvelopeInternal codeWhat provokes it
429rate_limitedrate_limitedMore 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.
429rate_limitedrate_limitedThe per-key bucket for this endpoint, or (on orders and leads) the parallel per-(IP, shop) bucket.
500server_errorSERVER_ERRORThe 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."
}
CodeStatusesWhen
invalid_key401No 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_allowed403A publishable key with a non-empty origin allowlist, called without a permitted Origin header.
rate_limited429Either the pre-auth per-IP ceiling or the per-endpoint bucket. Read X-RateLimit-Limit to tell which.
validation_failed400, 405, 409, 413Malformed JSON, a body over the size cap, an unsupported verb, or a request that did not validate against current server facts.
not_found404Unknown product style, unknown/foreign/revoked tracking token, or a shop whose configuration could not be read.
shop_unavailable403, 503The shop is suspended, its record is temporarily unreadable, or online ordering is switched off (quote and orders only).
server_error500, 503An upstream failure. Safe to retry.

7. Rate limits

Per key, per minute:

EndpointPublishable keySecret keyParallel per-IP
GET /shop60120
GET /products60120
GET /products/{style}60120
POST /quote3030
POST /orders555
GET /orders/{trackingToken}3030
POST /leads555

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:

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