> Part of [WHCC Developer Documentation](https://www.whcc.com/developer/llms.txt)

# Errors & Rate Limits

Version 1.7 &middot; Updated 2026-10-09

WHCC API errors come in two shapes, and the shape tells you what to fix. A one-field `{ "message": … }` body comes from the gateway in front of the API, which checks the key: the request was rejected before the API saw it. A Problem Details body comes from the API itself and tells you what was wrong with the request. A `401` is always the first kind. On the commerce, catalog and discovery routes (`/v1/commerce`, `/v1/catalog` and `/v1/discovery`), a `400`, `404`, `409`, `410` or `413` is the second. The fulfillment routes and a few other responses differ; see [Other response bodies](https://www.whcc.com/developer/docs/api/errors/#other-bodies). A `403` can be either, so check the body before deciding what to fix.

## Key errors: the `{ "message": … }` body

A missing or unrecognized API key is rejected before your request reaches the API, with a one-field JSON body:

- **`401 Unauthorized`**: No `Authorization` header, or one that can't be read. Send `Authorization: Bearer <YOUR_API_KEY>`.An API key can also expire or be revoked. The gateway then answers `401` before the request reaches WHCC, with no Problem Details body to parse. Ask WHCC support for a new key.
- **`403 Forbidden`**: The key isn't recognized, isn't valid for this host (for example, a sandbox key sent to production), or can't use this route. Check you're using the right key for the host.

```json
{ "message": "Unauthorized" }
```

```json
{ "message": "Forbidden" }
```

> Because the key is checked first, a request without a key returns `401`, not `404`, even for a path that doesn't exist. If you see `{ "message": … }`, fix the key before debugging anything else.

## API errors: Problem Details (`400`, `403`, `404`, `409`, `410`, `413`, `415`)

Errors from the API itself are [Problem Details](https://www.rfc-editor.org/rfc/rfc9457), served as `application/problem+json`, with these fields:

- **`type`** (string): `about:blank` on the commerce, catalog, discovery and fulfillment routes, with one exception: a request for an [API version that doesn't exist](https://www.whcc.com/developer/docs/api/errors/#example-400-version) has the type `https://developer.whcc.com/problems/unsupported-api-version`. Don't build on `about:blank`. Some responses outside those routes send an empty string; see [Other response bodies](https://www.whcc.com/developer/docs/api/errors/#other-bodies).
- **`title`** (string): A human-readable summary of the problem.
- **`detail`** (string, sometimes present): More explanation, when there is something to say beyond the title. On a validation `400` it is often a generic line such as `Invalid request payload input`. The specifics are in `invalidParams[]`.
- **`status`** (number): The HTTP status code.
- **`instance`** (string): The request path that produced the error. Errors from the fulfillment routes don't carry it.
- **`invalidParams[]`** (array, sometimes present): Present on most `400` responses. One `{ name, reason }` entry for each field that failed, where `name` is the dotted path, such as `items.0.quantity`. Some entries also carry the `value` you sent.
- **`code`** (string, sometimes present): **Build your error handling on this field.** It's present on the errors listed in the [code table](https://www.whcc.com/developer/docs/api/errors/#submission-error-codes) below. Other errors don't have one, and that's normal. New codes can be added over time, so treat one you don't recognize as a new case to handle, not a breaking change.

### `400`: the request didn't validate

```json
{
	"type": "about:blank",
	"title": "Your request parameters didn't validate.",
	"status": 400,
	"invalidParams": [
		{
			"reason": "'items[0].quantity' must be a number",
			"name": "items.0.quantity"
		}
	],
	"instance": "/api/v1/commerce/submissions"
}
```

### `400`: `shipping.type` and `shipping.priority` disagree

A priority belongs to the type in its name, and a pair that disagrees is reported on both fields. The same shape, with one entry, covers an unknown type (`'ground' isn't a shipping type. Send studio or dropship.`) and a malformed priority (`'nope' isn't a shipping priority. Call calculate with an address and pick one from options.shipping[].`):

```json
{
	"type": "about:blank",
	"title": "Your request parameters didn't validate.",
	"status": 400,
	"invalidParams": [
		{
			"name": "shipping.priority",
			"reason": "'studio_two_day' isn't a dropship shipping priority. Send a shipping.priority that matches shipping.type, or change shipping.type."
		},
		{
			"name": "shipping.type",
			"reason": "'dropship' doesn't match shipping.priority: a studio priority needs shipping.type studio."
		}
	],
	"instance": "/api/v1/commerce/submissions"
}
```

A well-formed priority that the destination doesn't offer is rejected only once you've sent an address, and it carries a `code`:

```json
{
	"type": "about:blank",
	"title": "Your request parameters didn't validate.",
	"status": 400,
	"detail": "Invalid request payload input",
	"invalidParams": [
		{
			"name": "shipping.priority",
			"value": "dropship_canada",
			"reason": "'dropship_canada' isn't available for this address. Available: dropship_lowest_cost, dropship_expedited, dropship_one_day_standard, dropship_one_day_priority. To compare prices, call calculate without shipping.priority."
		}
	],
	"instance": "/api/v1/commerce/submissions/calculate",
	"code": "submission.shipping_priority_unsupported"
}
```

### `400`: the body isn't JSON

A body that can't be parsed as JSON, such as one cut off partway, returns `400` with `code: "request.invalid_json"` and no `invalidParams[]`:

```json
{
	"type": "about:blank",
	"title": "Your request body isn't valid JSON.",
	"status": 400,
	"instance": "/api/v1/commerce/submissions/calculate",
	"code": "request.invalid_json"
}
```

Fix the body and send it again. A request whose `Content-Type` isn't `application/json` returns `415` with the title `Unsupported Media Type` and the detail `Send the body as application/json.` The fulfillment routes, including event subscriptions, don't return `415` for a missing `Content-Type`: the body is read as JSON. Send `application/json` anyway.

### `400`: the body contains a reserved key

A body that contains the key `__proto__` returns `400` with `code: "request.reserved_key"` and no `invalidParams[]`. It applies wherever the key appears in the body, including inside `metadata`:

```json
{
	"type": "about:blank",
	"title": "Your request body contains the reserved key `__proto__`.",
	"status": 400,
	"instance": "/api/v1/commerce/submissions/calculate",
	"code": "request.reserved_key"
}
```

Remove the key and send the request again.

### `400`: the API version doesn't exist

A `WHCC-API-Version` header whose value isn't a [listed version](https://www.whcc.com/developer/docs/api/versioning/index.html.md) returns `400` before the route runs. That covers an unlisted month such as `2026-11`, and values such as `v1`, `latest` or an empty value. The `type` is `https://developer.whcc.com/problems/unsupported-api-version`, the `detail` names the value you sent, and `supportedVersions` lists every version that exists. The response carries none of the `WHCC-API-Version` headers, because no version was served. This example is the response to `GET /api/v1/catalog/products` with the request header `WHCC-API-Version: 2026-11`:

```json
{
	"type": "https://developer.whcc.com/problems/unsupported-api-version",
	"title": "Unsupported API version",
	"status": 400,
	"detail": "API version '2026-11' is not supported. Use one of the supported versions.",
	"instance": "/api/v1/catalog/products",
	"supportedVersions": ["2026-10"]
}
```

Send a version from `supportedVersions`, or omit the header to get your account's default version. As with other problem bodies from the fulfillment routes, the same error on a fulfillment route has no `instance`.

### `403`: the key lacks the route's scope

The API returns its own `403`, as Problem Details, when your key is valid but doesn't carry the scope this route needs. The words `Insufficient scope` are in the `title` on the commerce, catalog and discovery routes and at the start of the `detail` on the fulfillment routes, so check both.

> **Which kind of `403` is mine?** A `{ "message": "Forbidden" }` body means the key: check it's the right key for this host. A Problem Details body means the key is right but can't use this route: get in touch and we'll sort out the key.

### `404`: submission not found

A submission that belongs to another account returns the same `404` as one that doesn't exist. Check the ID, and that you're using the key it was submitted with:

```json
{
	"type": "about:blank",
	"title": "No submission found with id <unknownSubmissionId>",
	"status": 404,
	"instance": "/api/v1/commerce/submissions/<unknownSubmissionId>"
}
```

### `404`: product not found

A `ref` that isn't in your catalog. Refs are case-sensitive, so check the spelling against `GET /v1/catalog/products`:

```json
{
	"type": "about:blank",
	"title": "Product not found for the supplied reference \"no-such-product\"",
	"status": 404,
	"instance": "/api/v1/catalog/products/no-such-product"
}
```

A path that isn't a route at all, including a known route with a trailing slash, returns a `404` with the title `Not Found` and the detail `No operation matches this path.` That holds on the commerce, catalog and discovery routes. The fulfillment routes accept a trailing slash, so `GET /v1/fulfillment/orders/` answers like `GET /v1/fulfillment/orders`. A method a route doesn't support gets the same `404`, not a `405`, and the response carries no `Allow` header.

### `409`: `Idempotency-Key` already used for a different order

You sent an `Idempotency-Key` you'd already used, with a different body. The same key with the same body isn't an error: you get your original submission back with a `202`. See [the `Idempotency-Key` header](https://www.whcc.com/developer/docs/api/submission-lifecycle/#idempotency).

```json
{
	"type": "about:blank",
	"title": "The request body does not match a previous request that used this Idempotency-Key. Use a new Idempotency-Key for a different request.",
	"status": 409,
	"instance": "/api/v1/commerce/submissions",
	"code": "submission.idempotency_conflict"
}
```

> **A `409` means your first submission stands.** If you meant to retry that order, look it up rather than resending with a new key. A new key places a second order. Use one only when this really is a different order.

> **A `409` without a `code` is something else.** It's rare and has nothing to do with your `Idempotency-Key`, so don't change the key. Contact WHCC support with the message.

## The `submission.*` codes

There are two families. The first is the `code` on a Problem Details error, meaning your request was rejected. The second, [below](https://www.whcc.com/developer/docs/api/errors/#submission-failure-codes), appears on a successful response, on an order within your submission that failed at WHCC.

- **`submission.shipping_priority_unsupported`**: `400`, submit and calculate, only when you sent both a `shipping.priority` and an address. The priority is well formed but isn't offered for that destination. `invalidParams[]` names `shipping.priority` and its `reason` lists the priorities of the same shipping type that are available, if there are any. Pick one of those, or call calculate without a `priority` and choose from `options.shipping[]`. A type/priority mismatch is a plain `400` without a code, naming both `shipping.type` and `shipping.priority`; see the [worked example](https://www.whcc.com/developer/docs/api/errors/#example-400-shipping).
- **`submission.not_submittable`**: `400`. The submission isn't ready to place, usually because an item isn't valid for order yet, the shipping address is incomplete, or a shipping choice can't be met. A missing or too-short address field gets its own `invalidParams[]` entry, such as `{ "name": "shipping.address.zip", "reason": "Required for this shipping type." }`. When the cause is an item, such as one whose image failed, the `detail` reads `Some items aren't valid for ordering. Each one is named in invalidParams.` and there is one entry per failing item, in request order, named `items.N.configuredProductId` with the `value` you sent and a reason such as `This configured product isn't valid for ordering: no-photo-selected.` Fix the named items and submit again. Send the same body to calculate and check `validation.isValidForSubmit`, `validation.invalidParams[]` and each item's `isValidForOrder`.
- **`unknown configuredProductId, no code`**: `400`. An item names a configured product that doesn't exist or belongs to someone else; the two can't be told apart. The `invalidParams[]` entry is named `items.N.configuredProductId` with the reason `No configured product with this id exists on your account.` and the `value` you sent. A string that isn't shaped like an id at all gets the same name with a type reason. In each case, send the `configuredProductId` the build returned.
- **`submission.idempotency_conflict`**: `409`. The `Idempotency-Key` was already used with a different body. Your first submission stands; see [above](https://www.whcc.com/developer/docs/api/errors/#example-409).
- **`submission.idempotency_key_required`**: `400`, submit only. The `Idempotency-Key` header is missing. Add it; see [the `Idempotency-Key` header](https://www.whcc.com/developer/docs/api/submission-lifecycle/#idempotency).
- **`submission.idempotency_key_not_applicable`**: `400`, calculate only. Calculate doesn't create anything, so it doesn't take an `Idempotency-Key`. Remove the header.
- **`CURSOR_INVALID`**: `400`, `GET /v1/commerce/submissions` only. The `cursor` is malformed or was altered. Start again from the first page. An expired cursor is a `410` instead.
- **`CURSOR_FILTER_MISMATCH`**: `400`, `GET /v1/commerce/submissions` only. The `cursor` is valid but was issued for different filters. Keep the filters the same while you page, or start again from the first page.
- **`unknown_selection_field`**: `400`, `POST /v1/commerce/configured-products` only. A key in `selections` isn't a field on that product. Use the `field` names from the product detail.
- **`413, no code`**: The request body is over 1 MiB. Send fewer items per call. Don't count on it for the fulfillment routes: a body over 1 MiB sent to `POST /v1/fulfillment/orders/{id}/shipping/preview` is read, not refused with a `413`.
- **`submission.pricing.destination_required_for_full_total`**: Not an error. This is a `guidance` note on a calculate response with no shipping address, naming the charges that were left out. See [Response Bodies](https://www.whcc.com/developer/docs/api/responses/#calculate-no-address).
- **`submission.pricing.total_excludes_tax`**: Not an error. A `guidance` note on the submit response, the submission detail, and calculate with a shipping address. It reminds you that `totalPrice.value` includes tax and `totalPrice.total.value` doesn't, so bill your customer `totalPrice.value`. Calculate without an address carries `destination_required_for_full_total` instead.

### When an order fails: `submission.failure.*`

Each entry in a submission's `orders[]` that failed carries a `failure: { code, message }`. The `code` is one of these. The `message` is a plain sentence you can log or show. Where a row says to contact WHCC support, reach your WHCC representative or use the Get in Touch form, and include the submission `id`.

- **`submission.failure.invalid_business_rules`**: WHCC couldn't accept the order as sent, commonly because a shipping method or option combination isn't available for the destination or product. This is the most common failure. Contact WHCC support to find out which rule applied.
- **`submission.failure.rejected_by_whcc`**: WHCC's order system rejected the order for another reason. Contact WHCC support for details.
- **`submission.failure.unconfirmed_after_import`**: WHCC received the order, but we couldn't confirm it went through. Contact WHCC support before resubmitting, so it isn't placed twice.
- **`submission.failure.submission_error`**: We couldn't send the order to WHCC because of an error on our side. It may need to be resubmitted; contact WHCC support if you're unsure.
- **`submission.failure.print_asset_generation_failed`**: WHCC accepted the order but couldn't make a print file for one of its items, often because an image is corrupt, unreadable or too small for the product. Check the images and resubmit, or contact WHCC support.
- **`submission.failure.production_error`**: WHCC accepted the order, but it ran into a problem in production. Contact WHCC support for details.
- **`submission.failure.unknown`**: The order failed for a reason we couldn't identify. Contact WHCC support.

## Other response bodies

The two shapes above cover the commerce, catalog, discovery and fulfillment routes. Some responses use neither, so don't write your error handling to require `instance` or `type: "about:blank"` on every non-2xx response.

- **`fulfillment routes`**: Errors are Problem Details with a `detail` and, usually, a `code`, but no `instance`. This includes the `404` for an order that doesn't exist and the `400` for an unknown query parameter. See [fulfillment refusals](https://www.whcc.com/developer/docs/api/fulfillment/#refusals).
- **`Paths and methods that aren't routes`**: A path outside the documented route families, such as `/v1/nothing-here`, `/v2/discovery` or `/`, and a method a fulfillment route doesn't support, return a `404` in a different shape: `{ "statusCode": 404, "error": "Not Found", "message": "Not Found", "type": "", "title": "Not Found", "status": 404, "instance": "…" }`. `type` is an empty string, there is no `detail`, and there are extra fields. A scope `403` on a route outside the documented route families can use this shape too. Read `status`, not `type` or `detail`.
- **`Malformed URL`**: A path with a malformed percent-escape, such as `%ZZ` or `%00`, can be refused by the load balancer in front of the API with a `400` whose body is HTML, not JSON, even with a valid key. Fix the URL.

## Rate limits

The WHCC API has no published rate limit today, so there is no fixed quota to track and no `Retry-After` header to honor. That can change: as more integrations come on, WHCC may add rate limits or quotas, and we'll announce them here and honor `Retry-After` when we do. Build as if a `429` could arrive tomorrow. In practice that means not polling for order completion: following `actions.poll` while a configured product builds is fine and expected, but order status should reach you through an [event subscription](https://www.whcc.com/developer/docs/api/event-subscriptions/index.html.md), not by re-reading the submission or the fulfillment routes on a timer.

Heavy traffic can still be slowed down, sometimes with a response that matches neither body shape above. Treat any `429` or repeated `5xx` as a signal to back off. `GET`s and calculate calls don't create anything, so they're safe to retry. **Never retry a submit blindly.** A `5xx` can arrive after your submission was accepted. Retry only the identical request with the same `Idempotency-Key`. If the retry returns the original submission ID, use `GET /v1/commerce/submissions/{submissionId}` to reconcile it. A new key after an unclear failure can place a second order. If your integration needs a steady high request rate, let us know during onboarding.