Errors
Every error this API returns is an RFC 9457 problem document, served as
Content-Type: application/problem+json. That includes the 401 from the authorizer and the
429 from the throttle, which are produced by the gateway rather than by application code but
are shaped identically - so one parser and one branch on code handles the whole API. The
sole exception is the edge firewall’s 403, described at the end.
{ "type": "about:blank", "title": "Bad Request", "status": 400, "code": "invalid_cursor", "detail": "The \"cursor\" query parameter is malformed.", "requestId": "8f3c1e2a-3d4b-4a1e-9f77-2b6c5e0a1d33"}| Field | Notes |
|---|---|
type |
Always the literal string about:blank today. Reserved by RFC 9457 for a future problem-type URI. |
title |
Short human summary of the status, for example Not Found. Derived from status; carries no information status does not. |
status |
The HTTP status code, repeated in the body. |
code |
The stable, machine-readable identifier. This is the field to branch on. |
detail |
Human-readable prose. May be reworded at any time. Do not parse it. |
requestId |
Echoes the request’s id. Include it when you escalate a specific failure. |
Codes the service returns
Section titled “Codes the service returns”Fix and resend means retrying the identical request will produce the identical response. Retry means the request was fine and the condition is transient. Back off means retry, but not immediately.
code |
Status | What happened | Action |
|---|---|---|---|
invalid_cursor |
400 | The cursor you sent could not be decoded. Usually truncated, edited, or carried over from a different query shape. |
Fix and resend Restart pagination with no cursor. Do not resend the same value. |
invalid_site_id |
400 | The siteId filter is not a positive integer, or it names a site outside your grant. |
Fix and resend Check GET /v1/me’s grantedSiteIds. |
invalid_order_number |
400 | The order number in the path is not a positive integer. | Fix and resend |
invalid_product_id |
400 | The productId path segment does not match the required SLO_/SLS_-prefixed shape. |
Fix and resend |
invalid_service_type |
400 | The serviceType filter on GET /v1/products is not one of the values this API can order (STOCK or POD). |
Fix and resend |
invalid_query |
400 | The q search parameter is empty or contains only whitespace. |
Fix and resend Send a search term, or omit q to list the catalog. |
site_id_required_for_search |
400 | q was sent without siteId. Listing spans your whole grant; searching is per site, because relevance scores from different sites are not comparable. |
Fix and resend Add siteId, or call GET /v1/sites/{siteId}/products?q=. |
cursor_not_supported_with_search |
400 | q was sent together with cursor. A search returns a ranked result set rather than a page, so nextCursor is always null and there is nothing to resume. |
Fix and resend Drop the cursor and raise limit instead. |
search_not_available |
400 | q was sent in an environment where the search service is not available. Not a fault in your request, and not a transient failure either – retrying will not change it. |
Fix and resend Omit q and page the catalog; ask Partner Integrations when search reaches this environment. |
updated_since_not_supported |
400 | You sent an updatedSince parameter. It is rejected rather than ignored, on purpose. |
Fix and resend See why below. |
invalid_request |
400 | A request body failed validation - malformed JSON, a missing or malformed field, an unknown key, or a body-level rule such as partnerLineId uniqueness. Also a malformed query or path parameter on routes that have not been given their own code yet - the submissions list’s status/limit filter, GET /v1/orders’s placedFrom/placedTo, and the proof and artwork bodies. |
Fix and resend detail names the exact violation. |
unknown_query_parameter |
400 | A query parameter this endpoint does not accept. Rejected rather than ignored: a filter that is silently dropped returns a full result set that looks filtered. detail names the parameter and lists the accepted ones. |
Fix and resend Check the spelling and the case - siteId, not siteid. |
invalid_submission_id |
400 | The submissionId path segment is not a well-formed UUID. |
Fix and resend |
invalid_product_patch |
400 | The PATCH /v1/products/{productId} body was empty, named a field that is not writable, or gave one the wrong type. detail names the field. |
Fix and resend The writable set is a strict allow-list - see Catalog and products. |
invalid_division_id |
400 | The divisionId path segment or query parameter - or GET /v1/divisions’s parentId, which names a division too - is not a positive whole number of at most nine digits. |
Fix and resend Use a divisionId from GET /v1/divisions. |
invalid_credit_account_id |
400 | The creditAccountId path segment is not a positive whole number of at most nine digits. |
Fix and resend Use a creditAccountId from GET /v1/credit-accounts. |
invalid_active |
400 | The active filter on GET /v1/credit-accounts is something other than exactly true or false. |
Fix and resend Rejected rather than coerced - 1, yes and on all disagree with each other about what 0 means. |
invalid_adjustment_type |
400 | The type filter on the adjustments list is not an upper-case movement code. |
Fix and resend Codes look like PRODUCT_DEBIT. An unknown but well-formed code is accepted and returns an empty page. |
invalid_user_id |
400 | The userId path segment or query parameter is not a positive whole number of at most nine digits. Nine is the cap because the column is a 32-bit integer - a longer value would be a database error rather than a clean “not found”. |
Fix and resend Use a userId from GET /v1/users. |
invalid_variant_patch |
400 | The PATCH /v1/products/{productId}/variants/{variantId} body was empty, named a field that is not writable on a variant, or gave one the wrong type. detail names the field. |
Fix and resend Five fields are writable: attribute1Choice, attribute2Choice, attribute3Choice, location, weight. See Catalog and products. |
invalid_variant_id |
400 | The variantId path segment is not a positive whole number. Print-on-demand products publish variantId: null and have no per-site addressable variant. |
Fix and resend Use a variantId from the product’s variants array. |
invalid_image_patch |
400 | The PATCH /v1/products/{productId}/images/{imageId} body was empty, named a field that is not writable on an image, or gave one the wrong type. detail names the field. |
Fix and resend Three fields are writable: imageAlt, sequence, primary. imagePath, attribution and the dimensions are read-only. |
invalid_image_id |
400 | The imageId path segment is not a positive whole number. |
Fix and resend Use an imageId from GET /v1/products/{productId}/images. |
division_create_requires_address |
400 | That site stores a postal address on every division, and Collaterate requires it at creation. This API does not manage division addresses, so it cannot create a division there. | Not retryable Ask Partner Integrations to create the division. |
invalid_segment_ids |
400 | One or more ids in PUT /v1/products/{productId}/segments is not a segment on that product’s site. detail names them. Nothing was changed - the whole request is refused rather than applying the ids that did resolve. |
Fix and resend Use GET /v1/segments for the ids you can assign. An id belonging to another site and an id that does not exist answer identically, on purpose. |
invalid_segments_body |
400 | The PUT /v1/products/{productId}/segments body is malformed - segmentIds missing or not an array, a member that is not a positive whole number, or an unrecognised field. |
Fix and resend The only accepted field is segmentIds. An unknown key is refused rather than ignored because this endpoint replaces the assignment. |
monolith_rejected |
400 | Collaterate refused the change on its own validation grounds. detail carries Collaterate’s message and, where it named one, the field. |
Fix and resend |
null_not_supported |
400 | You sent null for a field that can be set but not cleared - visibility, requiresApproval, overrideDisplay, minQuantity, maxQuantity - or for any field of a proof or artwork request body. |
Fix and resend Send a value, or omit the field. Generated clients that serialise an absent optional as null are the usual cause. |
sls_null_backfills_master |
400 | On a share-backed (SLS_) product you sent null for description, metaDescription or metaKeywords. That does not restore inheritance from the master product - it copies the master’s current value into your share, which then stops tracking it. |
Fix and resend Send "overrideDisplay": false to restore inheritance for those fields together. See Catalog and products. |
invalid_quantity |
400 | The quantity on a product quote request is missing or not a positive integer. |
Fix and resend |
invalid_shipping_quote_request |
400 | The POST /v1/shipping/quote body failed validation - malformed JSON, a missing or mistyped field, an unknown key at the top level or inside shipTo, or a weightOunces that is not greater than 0 and at most 5,000,000. |
Fix and resend detail names the field. |
invalid_item_number |
400 | The itemNumber in a proof or artwork route’s path is not a positive integer. It is the line’s itemNumber from GET /v1/orders/{orderNumber}/items, never an internal id. |
Fix and resend |
invalid_token |
401 | Your credential is missing, expired, or otherwise not accepted. Produced by the authorizer, so detail is generic. |
Mint a fresh token and retry once. If that fails too, the credential needs attention. |
insufficient_scope |
403 | Your credential is valid but its effective scopes do not include the one this operation requires. | Fix and resend Mint a token carrying the scope, or ask Partner Integrations to add the capability. |
order_not_found |
404 | No such order, or an order that exists outside your granted sites. These are indistinguishable by design. | Fix and resend Do not treat as proof of non-existence. |
order_item_not_found |
404 | No line matching (orderNumber, itemNumber) is visible to you - absent, outside your granted sites, or hidden from the customer, indistinguishably. |
Fix and resend Do not treat as proof of non-existence. |
product_not_found |
404 | No such product, or one that exists outside your granted sites - the same rule as order_not_found. |
Fix and resend Do not treat as proof of non-existence. See Tenancy and access. |
quote_product_not_found |
404 | The product resolved in our catalog, but Collaterate could not price it. Rare. | Escalate with the requestId if it persists. See Pricing. |
image_not_found |
404 | The imageId is not one of that product’s images. Returned identically whether the image belongs to a different product or does not exist. |
Not retryable List the product’s images with GET /v1/products/{productId}/images. Images are added and removed in the Collaterate admin UI, not through this API. |
division_not_found |
404 | No such division on your granted sites. Returned identically whether it does not exist or sits on a site you were not granted. | Not retryable List with GET /v1/divisions. |
division_code_conflict |
409 | A division with that code already exists on the site. Codes match users at sign-in, so a duplicate makes that match ambiguous. |
Fix and resend Choose another code. The check is best-effort - the platform enforces no constraint itself. |
credit_account_not_found |
404 | No such credit account on your granted sites. Returned identically whether it does not exist or sits on a site you were not granted. | Not retryable List with GET /v1/credit-accounts. |
all_segments_visible_reset_not_acknowledged |
409 | The user currently has allSegmentsVisible: true, and assigning specific segments would set it to false - which Collaterate does even when the list is unchanged. |
Fix and resend Resend with "acknowledgeAllSegmentsVisibleReset": true if you intend the change. |
auto_assigned_segments_present |
409 | Your segmentIds would remove a segment that a site rule assigned automatically. detail names the ids. |
Fix and resend Include those ids to keep them. |
user_already_exists |
409 | A user with that username or email already exists on that site. Checked before anything is created. | Not retryable The check is best-effort: it can occasionally miss a duplicate the platform then rejects as monolith_rejected. |
user_not_found |
404 | No such user on your granted sites. Returned identically whether the user does not exist or exists on a site you were not granted. | Not retryable List with GET /v1/users. |
site_not_found |
404 | No such site among your granted sites. Returned identically whether it does not exist or is simply not one of your granted sites. | Not retryable List with GET /v1/sites. |
template_not_found |
404 | No Chili template is configured for this product - whether it is not template-classified, uses a legacy vendor, or is template-classified with no document set up. | Not fixable by retrying. See Catalog and products. |
submission_not_found |
404 | No submission exists with this id for your credential, whether it never existed or belongs to another partner - indistinguishable by design. | Fix and resend Do not treat as proof of non-existence. |
ordering_not_provisioned |
409 | The order’s siteId has no ordering user configured. Set up per (partner, site) by staff at onboarding; a partner cannot provision it themselves. |
Ask Partner Integrations to provision an ordering user for the site, then retry. See Submitting orders. |
partner_order_id_reused |
409 | The partnerOrderId was already used, with a request body that does not match this one. |
Fix and resend Use a new partnerOrderId, or resend the exact original body to poll the existing submission. See Submitting orders. |
proof_not_pending_review |
409 | You approved or declined a proof on a line whose proofStatus is not PENDING_REVIEW - no proof yet, already decided, or a state we do not recognise. detail says which. |
Fix and resend On a retry of a decision that already succeeded, this is the expected answer: treat it as success. Otherwise read the line’s current proofStatus. See Approving a proof. |
order_item_cancelled |
409 | You acted on a cancelled line - approving or declining its proof, or attaching artwork to it. It will never be produced, so neither is meaningful. | Fix and resend Check the cancelled flag on the line in GET /v1/orders/{orderNumber}/items. |
artwork_reservation_not_usable |
409 | The reservationName on POST .../artwork cannot be attached: the upload has not finished, the name is unknown, or it has already been attached - a reservation is single-use. detail carries Collaterate’s own wording. |
Fix and resend Confirm the PUT to uploadUrl returned 200, then attach. If the URL had expired, request a new one and upload again. See Artwork and print files. |
monolith_target_missing |
409 | The product resolved in our catalog but Collaterate no longer has it, so the two stores disagree. Deliberately not a 404: the catalog still lists it. |
Escalate with the requestId. |
product_write_partially_applied |
409 | Your change spanned several upstream sections and a later one was refused after an earlier one had already been saved. detail names the sections that applied. |
Fix and resend These writes are not atomic. Re-send the fields that did not apply rather than assuming nothing happened. |
quote_invalid |
422 | Collaterate rejected the quote on business-validation grounds, for example a quantity over the product’s configured maximum. | Fix and resend detail carries Collaterate’s own message. See Pricing. |
unknown_country_code |
422 | shipTo.country on a shipping quote is not a country code Collaterate recognises. |
Fix and resend Use a two-letter code Collaterate carries (US, CA, MX, …). |
unknown_state_code |
422 | shipTo.state on a shipping quote is not a state or province of the country you sent. Codes are resolved within the country, because they are not unique across them - BC is British Columbia under CA and Baja California under MX. |
Fix and resend Check the state against the country. |
shipping_quote_unavailable |
422 | A shipping quote produced no priced service at all for that destination and weight. detail carries Collaterate’s own reasons when it gave any - an invalid postal code for the country is the common one. |
Fix and resend An empty result is never returned as a 200, so you never have to tell it apart from a real one. See Pricing. |
denial_reason_not_available |
422 | The reason on a proof decline is well-formed but is not one that line’s proof offers - including a code valid on a different line, and UNKNOWN, which never is. |
Fix and resend detail lists the codes valid for that line. Read them per line, not once per account. See Approving a proof. |
division_parent_invalid |
422 | The parentId is not a division on that site, or is already nested at the 10-level depth limit. Absent and foreign parents answer identically. |
Fix and resend Use GET /v1/divisions. |
segment_not_on_site |
422 | One or more segmentIds is not a segment on that user’s site. Returned identically whether it does not exist or belongs to another site. Nothing was changed. |
Fix and resend Use GET /v1/segments. |
division_not_on_site |
422 | The divisionId you sent is not a division on that user’s site. Returned identically whether the division does not exist or belongs to another site. |
Fix and resend Use GET /v1/divisions for the divisions you can assign. |
parent_not_on_site |
422 | The parentId you sent to PATCH /v1/divisions/{divisionId} is not a division on that division’s own site. Returned identically whether it does not exist or belongs to another site. |
Fix and resend Use GET /v1/divisions. |
parent_is_self |
422 | You asked to make a division its own parent. | Fix and resend Send another division’s id, or null to make it a root. |
unsupported_field_for_service_type |
422 | The field cannot be written on this product’s service type. minQuantity and maxQuantity are readable everywhere but writable only on STOCK - on POD the bounds belong to the shared print-job classification, not the product. |
Fix and resend Refused rather than accepted upstream and then invisible here. Reading still works. |
variant_not_site_addressable |
422 | This product’s variant cannot be changed for one site. Share-backed (SLS_) and print-on-demand products expose a variantId shared with every other site the same master product is shared to, so changing it would change it for them too. |
Not retryable Only site-owned (SLO_) products have a per-site variant. Product-level fields stay writable via PATCH /v1/products/{productId}. See Catalog and products. |
rate_limit_exceeded |
429 | Your per-partner rate, burst, or daily quota. Carries Retry-After. |
Back off See Rate limits and quotas. |
internal_error |
500 | An unexpected server-side failure. detail is always the fixed string An unexpected error occurred. and never echoes the underlying error. |
Retry once, well spaced. If it persists, escalate with the requestId. |
service_unavailable |
503 | A transient failure in the service or the database it reads from. | Back off Exponential, starting around one second. |
audit_unavailable |
503 | This API records every write in its own audit trail before making it, and that record could not be written - so your change was not attempted. | Back off Retry. Nothing was applied, so a retry is safe. |
This is the complete set. One test in the service compares the OpenAPI contract to the codes the handlers actually construct, so a code cannot be added on one side and forgotten on the other; a second test compares this very table to that same contract, so the guide cannot drift back out of sync with it either. New codes can still appear inside v1 - treat one you do not recognize as “unhandled error for this status” rather than failing on it.
Where the 401 and the 429 come from
Section titled “Where the 401 and the 429 come from”Both are produced by the API gateway, before your request reaches any application code: the
401 by the authorizer, the 429 by your usage plan. They are nonetheless shaped as the same
problem document as everything else, with the same fields and a requestId, so you do not
need a second code path for them.
The 401 is identical for every cause - no header, a malformed one, an expired token, a
disabled credential - because a 401 that explained which check failed would help someone
probing with guessed values more than it helps you. See
when authentication fails.
The one response that is not a problem document
Section titled “The one response that is not a problem document”403, from the edge
Section titled “403, from the edge”The edge web application firewall enforces a per-IP rate-based rule and a set of managed rule
sets. When either blocks a request the response is a 403 from the firewall: no problem
document, and unrelated to your token’s scopes.
This is the one case in this API where a 403 does not mean “your credential lacks a
capability”. If you are seeing 403 without an insufficient_scope body, look at your
request rate and your source IP before you look at your scopes. See
Rate limits and quotas.
Retry policy
Section titled “Retry policy”A summary you can encode directly:
| Status | Retry? | How |
|---|---|---|
| 400 | No | The request must change. Retrying it unchanged returns the same answer forever. |
| 401 | Once | Mint a fresh token, retry once. Then stop and investigate the credential. |
| 403 | No | Either a missing scope (fix the token) or the edge firewall (slow down). Neither is fixed by an immediate retry. |
| 404 | No | |
| 409 | No | partner_order_id_reused means use a new partnerOrderId or resend the exact original body; ordering_not_provisioned means ask Partner Integrations. |
| 422 | No | Fix the request per detail. |
| 429 | Yes | Honor Retry-After, and add exponential backoff with jitter on top of it. |
| 500 | Cautiously | Once, well spaced. A persistent 500 is a bug on our side and needs the requestId, not more traffic. |
| 503 | Yes | Exponential backoff from around one second. |
Cap your retries and add jitter. A fleet of integrations retrying a transient failure in lockstep turns a blip into an outage, and the 503 path exists precisely for moments when the service has less capacity than usual.
Two rules worth internalising
Section titled “Two rules worth internalising”404 is not proof of non-existence
Section titled “404 is not proof of non-existence”order_not_found means “no order matching this request is visible to you”. It does not
distinguish “this order number was never issued” from “this order belongs to a site you are
not granted”, and it never will - confirming existence is itself a disclosure. Do not write
logic that treats a 404 from this API as grounds to mark a local record cancelled, deleted,
or invalid. See Tenancy and access.
No change polling
Section titled “No change polling”updatedSince is rejected with a 400 rather than accepted and ignored, because a filter you
believe is working is how you end up trusting an incomplete feed. The updatedOn field is
not a usable watermark either: it does not move when a shipment posts, a tracking number is
added, or item quantities change on an existing order, and a separate internal process can
move an order into your site without touching it.
GET /v1/orders ordered by placedOn discovers new orders and nothing else. What is missing
is only the ability to ask which orders changed: fetching one order you already know about
always returns current state, so re-requesting GET /v1/orders/{orderNumber}/shipments on your
own schedule is the supported way to watch for tracking. See
Tracking and line items.
A 404 on a sub-resource is about the order, not the collection
Section titled “A 404 on a sub-resource is about the order, not the collection”GET /v1/orders/{orderNumber}/shipments and .../items return 200 with an empty array when
the order resolves for you and has nothing to show yet. A 404 from either means the order
number itself does not resolve for you. Do not collapse the two: the first means “not yet”, the
second means “wrong order number, or not your site”.
Escalating
Section titled “Escalating”Include the requestId from the failing response. It resolves to a single log entry, which
is dramatically faster than a search by approximate timestamp. The only failure with no
requestId to send is the edge 403, which never reaches our logs at all - for that one,
include the exact time with a timezone, the host you called, and your source IP instead.