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

# Quick Start: Submit a Print Order

Version 2.0 &middot; Updated 2026-10-09

Choose a print product, add your customer's photo, check the price, and submit it for production. One API key, and your customer never leaves your application.

A request to the WHCC API is a **submission**. WHCC validates it, queues it, and turns it into one or more real WHCC **orders** in the background. Submitting always returns `202`, so [follow the order's status](https://www.whcc.com/developer/docs/quick-start/#step-7) to hear what happens next. The [API Reference](https://www.whcc.com/developer/docs/api/#ontology) explains how submissions, orders, and fulfillment orders relate.

> **The fastest way through this page is an agent.** The WHCC API is tuned for AI coding agents. Give one the prompt on [Build It with an Agent](https://www.whcc.com/developer/docs/api/ai-agents/index.html.md) and a sandbox key, and it will place a sandbox order while you read along.

> **Before you start**, you need:
>
> - **`API key`**: Issued by WHCC and bound to one account. Start with a sandbox key; it works on the sandbox host and nowhere else. Send it as the bearer token on every call. See [API Key Security](https://www.whcc.com/developer/docs/api/api-key-security/index.html.md) for where to keep it.
> - **`image URL`**: A JPEG, HEIC, or PNG at a URL WHCC can fetch. Signed URLs are fine. Keep the file under 100 MB, on a host that answers quickly: a slow download fails the item. The build doesn't check that the image has enough pixels for the size you choose, and it returns no low-resolution warning, so check the resolution yourself before you build.
> - **`ship-to address`**: Name, street, city, state, zip, country.

> **Hosts**
>
> - **`https://apis-stage.whcc.com/api`**: Sandbox. Build and test here first with a sandbox key. Sandbox and production keys are separate.
> - **`https://apis.whcc.com/api`**: Production. **Submitting here places a real, billable order.**
>
> Every path below is relative to that base, so `/v1/catalog/products` means `https://apis-stage.whcc.com/api/v1/catalog/products`. The examples use the sandbox host, so you can run them as written with a sandbox key and nothing is produced or billed. When you're ready to ship real orders, see [Going to production](https://www.whcc.com/developer/docs/quick-start/#going-to-production). The `actions` links on every response, catalog included, are absolute URLs on the host you called: use them exactly as returned, and don't join them to the base. Send a `WHCC-API-Version: 2026-10` header on every request, including requests to those links; see [API Versions](https://www.whcc.com/developer/docs/api/versioning/index.html.md).

Not sure which routes your key can reach? Call [Discovery](https://www.whcc.com/developer/docs/api/#discovery). Every response also carries [`actions`](https://www.whcc.com/developer/docs/api/#actions) links that point at the next call, so you rarely need to build a URL by hand.

## Authentication

Send your API key as a bearer token on every request. There is no token exchange and no login flow. The key is the credential.

```plaintext
Authorization: Bearer <YOUR_API_KEY>
```

## Step 1: List products

### GET `/v1/catalog/products`

Returns the products your account can order, sorted by name. Filter with `category` (exact match on `category.ref`) or `q` (case-insensitive match on `name`), and page with `limit` (up to 200) and `cursor`.

```shell
curl "https://apis-stage.whcc.com/api/v1/catalog/products?q=Framed+Print" \
	-H "Authorization: Bearer <YOUR_API_KEY>"
```

```json
{
	"products": [
		{
			"ref": "framed-print",
			"name": "Framed Print",
			"completion": "api",
			"actions": {
				"detail": { "url": "https://apis-stage.whcc.com/api/v1/catalog/products/framed-print", "method": "GET" }
			},
			"description": "Present images in remarkable, handcrafted frames",
			"category": { "ref": "wall-art", "name": "Wall Art" },
			"kind": "wall-art",
			"resolution": 300,
			"sizedBy": ["size"]
		}
	],
	"totalCount": 1
}
```

`ref` is the product identifier you use from here on. It is case-sensitive, so send it exactly as returned. Choose a product with `completion: "api"`. Those are the products this API can configure and price on its own, with no editor session. Products with `completion: "editor"` need a design finished in a WHCC editor, so this API can't build them. When a page has more results, the response includes `nextCursor`. Pass it back as `cursor` to continue. It is absent on the last page.

> **Which products you can build.** Stock products, and any product the simple editor offers, are available now and carry `completion: "api"`. Products that need the full editor carry `completion: "editor"` and can't be built through this API yet. The catalog tells you which is which, so filter on `completion` rather than on category.

## Step 2: Get product details

### GET `/v1/catalog/products/{ref}`

Returns one product in full, including `configuration.fields[]`: the option names and values you'll send in Step 4.

```shell
curl https://apis-stage.whcc.com/api/v1/catalog/products/framed-print \
	-H "Authorization: Bearer <YOUR_API_KEY>"
```

#### Example response (trimmed)

```json
{
	"ref": "framed-print",
	"name": "Framed Print",
	"completion": "api",
	"description": "Present images in remarkable, handcrafted frames",
	"category": { "ref": "wall-art", "name": "Wall Art" },
	"kind": "wall-art",
	"resolution": 300,
	"sizedBy": ["size"],
	"configuration": {
		"fields": [
			{
				"field": "size",
				"name": "size",
				"required": true,
				"kind": "enum",
				"values": [
					{ "id": "5x7", "name": "5×7", "enabled": true, "width": 5, "height": 7 },
					{ "id": "5x10", "name": "5×10", "enabled": true, "width": 5, "height": 10 },
					"…"
				],
				"default": "8x10"
			},
			{
				"field": "photo_print",
				"name": "Paper",
				"required": false,
				"kind": "enum",
				"values": [
					{ "id": "lustre_photo_print", "name": "lustre", "enabled": true },
					{ "id": "fuji_deep_matte_photo_print", "name": "deep matte", "enabled": true }
				],
				"default": "lustre_photo_print"
			}
		]
	},
	"actions": {
		"create": { "url": "https://apis-stage.whcc.com/api/v1/commerce/configured-products", "method": "POST" }
	}
}
```

- **`configuration.fields[].field`**: A key in `selections`, such as `size` or `surface`.
- **`configuration.fields[].values[].id`**: A valid value for that key, such as `lustre_photo_print`.
- **`configuration.fields[].default`**: The catalog's suggested value for that field. It applies only when the surface or material it belongs to is the one you chose. Send the value yourself. Don't count on a default being filled in.

## Step 3: Choose product options

Build a `selections` object from the product detail. Each `field` becomes a key, and each value is an enabled `values[].id`. Add `quantity`. The request in Step 4 shows a complete set for a framed print.

Two rules while you choose:

- A `default` belongs to a surface or material. Send it only if its parent selection applies. A framed print on `surface: "photo_print"` takes the `photo_print` paper default, not the canvas or metal ones.
- Send every field the product lists. Some fields marked `required: false` are still needed, so send the default for any field your customer doesn't choose. When a field has no `default`, send its first enabled value, unless the field is `required: false` and belongs to a surface or material you didn't choose. Leave that one out.
- The catalog has no parent link on a field. A field belongs to a surface or material when its `field` name is the `id` of a value on another field: `photo_print` is a value of `surface`, so the `photo_print` field applies only when you chose `surface: "photo_print"`. A value that doesn't apply may be dropped without an error (a `mat_color` sent with no mat builds fine and doesn't appear in the summary), so check `selectionsSummary[]` as described below.
- Two fields are the exception. Leave out `orientation` unless your customer chose one: WHCC works it out from the image. And send a `dimension` field only with a custom size, as shown below.

The catalog lists what's possible, not which combinations work together. Step 4 returns a `400` for a key that isn't a field on the product (`code: "unknown_selection_field"`), a value that isn't a choice for its field, and a value that belongs to a surface or material you didn't choose. Each problem gets its own `invalidParams[]` entry, named like `configuredProducts.0.selections.mat`, with the `value` you sent and a `reason` such as `'yes' can't be combined with the other selections on this product. Remove it or change the selection it conflicts with.` or `'glossy' isn't a valid choice for surface.`

> **Check what was built.** The build doesn't reject everything. A field you leave out can be filled with the catalog default: leave `size` off a framed print and you get an 8×10. A selection that doesn't fit the others can be dropped: a 16×20 mat on a 5×7 print builds an unmatted 5×7. Neither returns an error. Before you submit, compare `selectionsSummary[]` and `unitPrice` on the Step 4 response with what your customer chose.

#### Custom sizes

A product with a `kind: "dimension"` field takes a custom size. The field carries `unit` and `constraints` (a `min` and `max` for `width` and for `height`) instead of `values[]`. Send `size: "custom"` with the field as an object of two numbers. Either side can be the longer one. A gallery wrap at 10.5 by 13.5 inches:

```json
"selections": {
	"size": "custom",
	"dimensions": { "width": 10.5, "height": 13.5 },
	"…": "the product's other fields"
}
```

A size over the maximum returns `400`. A size between two priced sizes is priced at the next one up, and the response's `pricedAs` tells you which.

## Step 4: Build the product

### POST `/v1/commerce/configured-products`

A configured product is WHCC's priced record of a product built from your selections and one image. It is what you submit in Step 6. Each entry in `configuredProducts[]` carries a `productId` (the catalog `ref` from Step 1), your `selections`, and an image URL. Three details the build checks up front, each a `400` naming the field:

- `productId` should be the `ref`, such as `framed-print`. That is the supported form. The build also accepts the internal id you see as `productId` in responses and builds the same product, but send the `ref`.
- `selections.quantity` is a JSON number: `1`, not `"1"`.
- The image URL is `https://`. `http://`, `ftp://`, `file://`, `data:` and bare hostnames are rejected.

A `productId` that is neither a `ref` nor a product id in your catalog returns `404`.

One call takes 1 to 50 entries in `configuredProducts`. More than 50 returns `400`, so split a larger batch across calls. Build isn't idempotent: an `Idempotency-Key` header is ignored, and the same request sent twice creates two sets of items.

Each entry takes one image, as `{ "url": "https://…" }`. Any other key in the image object returns `400`, apart from `imageId`, covered below. Leave `images` out, or send it empty, and the build still answers `202`, but the item comes back `failed` with `1001`.

The build checks the image's format and that it can be fetched, not its quality. An image with too few pixels for the product, a very wide or very tall one, and one whose shape doesn't match the product all build as `ready` and are valid for ordering, with no warning and the same price as any other image. Responses don't say how an image is cropped or fitted to the product, or how transparency and non-sRGB color profiles are handled. PNGs with transparency, 16-bit PNGs, and JPEGs in CMYK, Adobe RGB, or with no color profile all build as `ready`. A `ready` item carries no image information: no dimensions, resolution, or file size. `placementProof` shows the image as placed.

Send one entry in `images[]`. A second entry is not rejected and gets no warning, and nothing in the response says which image was used. `images[]` entries take `url`. An entry with only `imageId` is not a `400`: it returns `404` (`Image not found`).

WHCC fetches and processes the image after responding, so this call returns `202` and an `actions.poll` link. Follow that link until every item is `ready` or `failed`.

> The image URL below is a public-domain test photo you can use as-is while you try the API. In your integration, use the URL of your customer's photo.

```shell
curl https://apis-stage.whcc.com/api/v1/commerce/configured-products \
	-H "Authorization: Bearer <YOUR_API_KEY>" \
	-H "Content-Type: application/json" \
	-d '{
			"configuredProducts": [
				{
					"productId": "framed-print",
					"selections": {
						"size": "8x10",
						"moulding": "woodland_walnut",
						"surface": "photo_print",
						"mat": "no",
						"acrylic": "no",
						"mounting": "standard",
						"backing": "wire_wall_hanger_paper_backing",
						"quantity": 1,
						"photo_print": "lustre_photo_print",
						"lamination": "no",
						"dynamic_layout": "no"
					},
					"images": [
						{ "url": "https://whcc-platform-test-assets.s3.us-east-2.amazonaws.com/0dd4751480afa9ec95e01c04acdd376f_2509_3357.jpg" }
					]
				}
			]
		}'
```

#### Example response (trimmed)

```json
{
	"items": [
		{
			"configuredProductId": "CF_bCy42bhE4xpJXXNezD2EQSkpAzrqBurvi8ICmrzmBTetrum",
			"productId": "6aLYwjRKYk8XQ4MZk",
			"quantity": 1,
			"status": "processing",
			"validationSummary": [
				{ "id": 1005, "level": "info", "message": "This item is still processing" }
			],
			"selectionsSummary": [
				{ "id": "size", "label": "size", "name": "size", "description": "8×10" },
				{ "…": "trimmed" }
			],
			"unitPrice": { "value": "76.45", "code": "USD" },
			"totalPrice": { "value": "76.45", "code": "USD" },
			"metadata": {},
			"links": { "designSpaceProof": "https://…", "placementProof": "https://…" }
		}
	],
	"totalPrice": { "value": "76.45", "code": "USD" },
	"actions": {
		"poll": {
			"instructions": "Some items are still processing. Follow actions.poll.url (a GET request) with the same ids until every item's status is ready. Don't resend the create request -- each call to it builds brand-new items rather than checking on the ones you already have. See https://apis.whcc.com/api/documentation for details.",
			"url": "https://apis-stage.whcc.com/api/v1/commerce/configured-products?ids=CF_bCy42bhE4xpJXXNezD2EQSkpAzrqBurvi8ICmrzmBTetrum",
			"method": "GET"
		}
	}
}
```

Keep `items[].configuredProductId`. It is what you poll with below and what you send in Steps 5 and 6. `selectionsSummary[]` and `links` are ready to show your customer what they're ordering. The summary lists the choices that describe the item, not every selection you sent. On a custom-size product, `pricedAs` tells you when the dimensions were rounded up to the next priced size; see [Response Bodies](https://www.whcc.com/developer/docs/api/responses/#submission-item-fields).

The response's `items[].productId` is the product's underlying id, not the catalog `ref` you sent. Both name the same product.

To open an item in a WHCC editor, ask for a link with `POST /v1/commerce/configured-products/{configuredProductId}/edit-link`. It returns `{ "actions": { "edit": { "url": "https://…", "method": "GET" } } }`. The link carries a session token, so treat it as a credential: don't log it or store it, and ask for a new one each time. Build responses carry no editor link, so this endpoint is the only source.

The request needs a `redirects` object, which says where the editor sends your customer. `complete` and `cancel` are both required: `complete` is where your customer returns when they finish editing, and `cancel` is where the editor's exit button goes. `secondary` is optional. Each is `{ "text": "...", "url": "..." }` with an `https://` url, and any other key returns `400`. To tell which of your orders a return link belongs to, put your own order id in the url, for example `https://yourdomain.com/return?order=12345`. Each call replaces the links saved on the item, so the last call wins, even for a link you were given earlier. Items built in one request share one editor, and the links apply to all of them. The build request takes no `redirects`: sending it there returns `400`.

```shell
curl -X POST https://apis-stage.whcc.com/api/v1/commerce/configured-products/<CONFIGURED_PRODUCT_ID>/edit-link \
	-H "Authorization: Bearer <YOUR_API_KEY>" \
	-H "Content-Type: application/json" \
	-d '{
			"redirects": {
				"complete": { "text": "Back to cart", "url": "https://yourdomain.com/return?order=12345" },
				"cancel": { "text": "Cancel", "url": "https://yourdomain.com/cart?order=12345" }
			}
		}'
```

#### Poll until the item is ready

Follow `actions.poll`. It only reads status and never creates anything. Pass the same `configuredProductId` values, comma-separated, up to 50 at a time. Start with about 5 seconds between checks and back off toward 30. List each id once, in a single `ids` parameter with no spaces: a repeated id, a second `ids` parameter, or any other query parameter returns `400`.

```shell
curl "https://apis-stage.whcc.com/api/v1/commerce/configured-products?ids=CF_bCy42bhE4xpJXXNezD2EQSkpAzrqBurvi8ICmrzmBTetrum" \
	-H "Authorization: Bearer <YOUR_API_KEY>"
```

#### Example response once the item is ready (trimmed)

```json
{
	"items": [
		{
			"configuredProductId": "CF_bCy42bhE4xpJXXNezD2EQSkpAzrqBurvi8ICmrzmBTetrum",
			"status": "ready"
		}
	],
	"actions": {
		"submit": {
			"instructions": "All items are ready. Build a new submission request referencing items[].configuredProductId and call POST /v1/commerce/submissions with a unique Idempotency-Key header to place the order (a valid shipping address and other order-level requirements are still validated at submit time and may still reject the request). See https://apis.whcc.com/api/documentation for details.",
			"url": "https://apis-stage.whcc.com/api/v1/commerce/submissions",
			"method": "POST"
		}
	}
}
```

When `actions.submit` replaces `actions.poll`, every item is ready. Go on to Step 5.

`actions.fix` means at least one item needs your attention: an id that is `not-found`, or an item that is `failed`. It has `instructions` only, with no `url` or `method`, and its text says which of the two it found. It can replace `actions.poll` while other items are still `processing`, so keep polling those items by id until each one settles.

Each item's `status` is `processing`, `ready`, `failed`, or `not-found`. Stop polling an item once it is anything but `processing`. A `failed` item never becomes ready, and its `validationSummary` carries `1001`, `1006`, or `1007` to say why. Build it again. The rows below explain each value:

- **`1001`**: No image was attached (`level: "error"`, message "No photo selected"). Build again with an `images[]` entry.
- **`1005`**: Still processing. Keep polling. Don't send the create request again.
- **`1007`**: The URL didn't return an image (`level: "error"`): it answered `404`, or it returned something else, such as a web page, a text file, or an SVG. A file that isn't there, such as a mistyped path on your own bucket, gets the same code and message as a file of the wrong type. A real image in another format, such as TIFF, is rejected the same way, and so are a PDF and JSON. A file over 100 MB fails with `1007` too, even though the URL is public and does return an image, and the message doesn't mention size. The message can read as if the file wasn't fetched even when it was. The item will never be ready. Check that the URL returns a JPEG, HEIC, or PNG under 100 MB, then build again.
- **`1006`**: WHCC couldn't get the image or couldn't process it (`level: "error"`). The usual causes are on the URL's side: the host refused the request (`403`), returned a `5xx`, didn't resolve, took too long, or isn't reachable from the public internet. The item will never be ready. Check that the URL opens from outside your network without signing in, then build again. A slow host can fail once and work the next time. If the URL is fine and it keeps happening, get in touch.
- **`not-found`**: An id in `ids` is mistyped, isn't a configured product id at all, or belongs to another account. The route returns this status instead of a `404` or a `400`. When one id is `not-found` and the rest are ready, the response carries `actions.fix` instead of `actions.submit`: check the ids you sent.

To build several products at once, add more `configuredProducts[]` entries. Each comes back with its own `configuredProductId`.

## Step 5: Calculate the price

### POST `/v1/commerce/submissions/calculate`

Prices and validates the whole basket without submitting it. Send the same body you'll submit in Step 6. Name each item by its `configuredProductId`, and list each `configuredProductId` once per basket: a repeat returns `400` naming `items.N.configuredProductId`, so set `quantity` on the one line instead. `items[].quantity` is a JSON integer from 1 to 10,000. `0`, a decimal, a string or a larger number returns `400` naming `items.N.quantity`. When the product's catalog entry lists a `quantity` range (`min`, `max`, `step`) it also has to fit that range, or the `400` carries `code: "submission.quantity_out_of_range"`. Leave it out and the item uses the quantity it was built with.

Send `Content-Type: application/json`. A property the API doesn't know is rejected with a `400` naming it, at every level of the body. You can add `instructions`, an optional free-text note that is sent to WHCC with the order.

`shipping.type` is optional: WHCC infers it from `shipping.priority`. If you send it, it must match (a `dropship_*` priority with `type: "dropship"`). A mismatched pair, an unrecognized priority, or a priority the address doesn't offer returns `400` naming the field. Check `shippingType`, `shippingPriority`, and `totalPrice.subtotals.shipping.label` in the response to confirm what you'll get. To see every method the address offers, priced, call calculate without `priority` and read `options.shipping[]` (see [Shipping](https://www.whcc.com/developer/docs/api/shipping/index.html.md)).

```shell
curl https://apis-stage.whcc.com/api/v1/commerce/submissions/calculate \
	-H "Authorization: Bearer <YOUR_API_KEY>" \
	-H "Content-Type: application/json" \
	-d '{
			"items": [
				{
					"configuredProductId": "CF_bCy42bhE4xpJXXNezD2EQSkpAzrqBurvi8ICmrzmBTetrum",
					"quantity": 1
				}
			],
			"shipping": {
				"type": "dropship",
				"priority": "dropship_lowest_cost",
				"address": {
					"name": "John Doe",
					"addr1": "3005 Eagandale Place",
					"city": "Eagan",
					"state": "MN",
					"zip": "55121",
					"country": "US"
				}
			}
		}'
```

#### Example response (trimmed)

```json
{
	"accountId": "<yourAccountId>",
	"items": [
		{
			"configuredProductId": "CF_bCy42bhE4xpJXXNezD2EQSkpAzrqBurvi8ICmrzmBTetrum",
			"primaryDisplayName": "8×10 Framed Print",
			"quantity": 1,
			"unitPrice": { "value": "76.45", "code": "USD" },
			"totalPrice": { "value": "76.45", "code": "USD" },
			"productId": "6aLYwjRKYk8XQ4MZk",
			"isValidForOrder": true
		}
	],
	"totalPrice": {
		"value": "91.26",
		"code": "USD",
		"total": { "value": "84.40", "code": "USD" },
		"subtotals": {
			"product": { "value": "76.45", "code": "USD" },
			"shipping": { "label": "Economy", "value": "7.95", "code": "USD" },
			"salesTax": { "value": "6.86", "code": "USD" }
		}
	},
	"validation": { "isValidForSubmit": true, "isValidDiscount": null },
	"shippingAddress": {
		"name": "John Doe", "addr1": "3005 Eagandale Place", "city": "Eagan", "state": "MN", "zip": "55121", "country": "US"
	},
	"shippingType": "dropship",
	"shippingPriority": "dropship_lowest_cost",
	"options": {
		"shipping": [
			{ "id": "dropship_lowest_cost", "type": "dropship", "priority": "dropship_lowest_cost", "label": "Economy", "price": { "value": "7.95", "code": "USD" } },
			{ "id": "dropship_expedited", "type": "dropship", "priority": "dropship_expedited", "label": "Expedited", "price": { "value": "20.61", "code": "USD" } },
			{ "id": "dropship_one_day_standard", "type": "dropship", "priority": "dropship_one_day_standard", "label": "Standard One-Day", "price": { "value": "31.34", "code": "USD" } },
			{ "id": "dropship_one_day_priority", "type": "dropship", "priority": "dropship_one_day_priority", "label": "Priority One-Day", "price": { "value": "40.48", "code": "USD" } }
		]
	}
}
```

`totalPrice.value` is the grand total including tax. `totalPrice.total.value` excludes tax. `options.shipping[]` lists every priced shipping method for this basket and address.

Calculate is the price to show your customer. It's the current price, not a held one: the order system sets the amount billed when it places the order, and it can differ (see [Shipping](https://www.whcc.com/developer/docs/api/shipping/#estimate)). A product's unit price can drop as the quantity rises, so a line here can differ from the build response's total. An item that failed to build is still priced in `totalPrice`, so check `validation.isValidForSubmit` before you show a total.

- **`items[].isValidForOrder`**: Whether this item can be ordered now. When `false`, the item's `validationSummary` says why. `1005` means it is still processing, so keep [polling](https://www.whcc.com/developer/docs/quick-start/#polling).
- **`validation.isValidForSubmit`**: Check this before you submit. It is `true` only when every item is orderable and the basket can ship. Step 6 runs the same check and returns `400` when it fails.

#### Calculating without an address

Leave out `shipping.address` to price a basket before checkout knows the destination. The response then omits `shipping`, `salesTax`, and `options.shipping[]` rather than showing zeros, `validation.isValidForSubmit` is `false`, and a `guidance` entry explains why:

```json
{
	"accountId": "<yourAccountId>",
	"items": [ { "…": "…" } ],
	"totalPrice": {
		"value": "76.45",
		"code": "USD",
		"total": { "value": "76.45", "code": "USD" },
		"subtotals": { "product": { "value": "76.45", "code": "USD" } }
	},
	"validation": { "isValidForSubmit": false, "isValidDiscount": null },
	"guidance": [
		{
			"code": "submission.pricing.destination_required_for_full_total",
			"message": "No shipping address was supplied, so no destination is assumed: shipping, salesTax, the US MN/CO retail delivery fee and the Canada import surtax are omitted rather than defaulted or reported as zero. totalPrice reflects product, packaging, discount, surcharges and minimumSurcharge only -- it is a product-only subtotal, not a checkout total. Supply shipping.address to receive the full breakdown."
		}
	]
}
```

> **Don't send `Idempotency-Key` to calculate.** Calculate creates nothing, so there is nothing to make idempotent. Sending the header returns `400` with `code: "submission.idempotency_key_not_applicable"`.

## Step 6: Submit

> This example submits to the sandbox, where nothing is produced or billed. The same request sent to `apis.whcc.com` with a production key creates a real order that WHCC will produce, ship, and bill you for. Before a production submission, confirm the product, image, price, recipient, and shipping details in your checkout.

### POST `/v1/commerce/submissions`

Send the same body as Step 5 plus a required `Idempotency-Key` header. You can also add `reference` (32 characters, printed on fulfillment paperwork and the shipping label) and `metadata` (your own bookkeeping, returned whenever you read the submission). See [Metadata & Reference](https://www.whcc.com/developer/docs/api/metadata/index.html.md).

```shell
curl https://apis-stage.whcc.com/api/v1/commerce/submissions \
	-H "Authorization: Bearer <YOUR_API_KEY>" \
	-H "Content-Type: application/json" \
	-H "Idempotency-Key: <UNIQUE_KEY_FOR_THIS_CHECKOUT_ATTEMPT>" \
	-d '{
			"items": [
				{
					"configuredProductId": "CF_bCy42bhE4xpJXXNezD2EQSkpAzrqBurvi8ICmrzmBTetrum",
					"quantity": 1
				}
			],
			"shipping": {
				"type": "dropship",
				"priority": "dropship_lowest_cost",
				"address": {
					"name": "John Doe",
					"addr1": "3005 Eagandale Place",
					"city": "Eagan",
					"state": "MN",
					"zip": "55121",
					"country": "US"
				}
			}
		}'
```

#### Example response (trimmed)

```json
{
	"id": "<submissionId>",
	"status": "submitting",
	"createdAt": "2026-09-24T20:50:31.518Z",
	"accountId": "<yourAccountId>",
	"items": [ { "…": "the same item shape as Step 5's response" } ],
	"totalPrice": { "value": "91.26", "code": "USD", "total": { "value": "84.40", "code": "USD" } },
	"validation": { "isValidForSubmit": true, "isValidDiscount": null },
	"guidance": [
		{ "code": "order.lifecycle.submission_enqueued", "message": "The order was accepted for asynchronous submission; the response status is optimistic and placement has not been confirmed yet." }
	],
	"actions": {
		"fulfillment": { "url": "https://apis-stage.whcc.com/api/v1/fulfillment/orders?sourceOrderId=<submissionId>", "method": "GET" }
	}
}
```

Save `id`. That is your `submissionId`. A fresh submit returns `202` with `status: "submitting"` and no `orders` key yet. A retry with the same key also returns `202`, with a snapshot of the submission, so its `status` may have moved on and `orders[]` may already be there. The snapshot can lag: its `guidance[]` and order `status` can be older than what a lookup returns, so read the submission ([Step 7](https://www.whcc.com/developer/docs/quick-start/#step-7)) for the current state. It means WHCC accepted the request, not that an order was placed. [Step 7](https://www.whcc.com/developer/docs/quick-start/#step-7) shows how to follow it from here.

#### Idempotency

`Idempotency-Key` is required. Use your own order ID, or generate a UUID and save it with the order before you send. Then a timeout or crash can retry the identical request safely: the same key with the same body returns your original submission, and nothing new is placed. The same key with a different body returns `409` with `code: "submission.idempotency_conflict"`. Keys never expire, so never reuse one for a different order. A missing header returns `400` with `code: "submission.idempotency_key_required"`. Full detail: [Submission Lifecycle](https://www.whcc.com/developer/docs/api/submission-lifecycle/#idempotency).

## Step 7: Hear back about the order

Submitting returns `202` before the order is placed. In your integration, let WHCC tell you what happened next: create an [event subscription](https://www.whcc.com/developer/docs/api/event-subscriptions/index.html.md) and WHCC posts `order.accepted`, `order.rejected`, `order.shipped` and `order.cancelled` to your endpoint as each order moves. Match an event to your checkout by its `ConfirmationId`, which is the `confirmationId` on each entry in the submission's `orders[]`. Don't poll the submission or the fulfillment routes on a timer. The events carry the status; the reads below are for when you need a specific order's details.

You can try the whole event path in the sandbox without waiting on a real order. Once you have a subscription, [send a test event](https://www.whcc.com/developer/docs/api/event-subscriptions/#test): pass `order.accepted` and the `confirmationId` from your submission, and WHCC builds, signs and delivers a webhook for that order through the same pipeline as a real one. The only event you can't test yet is `order.shipped`.

Your endpoint needs a public `https` URL. To receive events on your own machine while you build, run a tunnel to it. See [Testing from your own machine](https://www.whcc.com/developer/docs/api/event-subscriptions/#local-testing).

One gap: if a submission fails before any order exists, there's no order to send an event about. So after you submit, read the submission once it has settled, usually within seconds, and act on `failed` or `partially_failed`. The same read is how you reconcile when an event never arrives.

### GET `/v1/commerce/submissions/{submissionId}`

For this walkthrough, where there is no endpoint to receive events yet, use this to see the order your submission produced.

```shell
curl https://apis-stage.whcc.com/api/v1/commerce/submissions/<submissionId> \
	-H "Authorization: Bearer <YOUR_API_KEY>"
```

#### Example response once the order is placed (trimmed)

```json
{
	"id": "<submissionId>",
	"status": "placed",
	"orders": [
		{
			"confirmationId": "<confirmationId>",
			"status": "order_placed",
			"actions": {
				"fulfillment": { "url": "https://apis-stage.whcc.com/api/v1/fulfillment/orders?sourceConfirmationId=<confirmationId>", "method": "GET" }
			}
		}
	]
}
```

While the submission is in flight the response reads `status: "staged"` or `status: "submitting"` with no `orders` key. Once it settles, `status` is one of `placed`, `shipped`, `failed`, `canceled`, or `partially_failed`, and `orders[]` has one entry per WHCC order, including any that failed. Read each entry's own `status` and `failure`. [Handle `partially_failed` with care](https://www.whcc.com/developer/docs/api/submission-lifecycle/#partially-failed): some of your customer's items are on their way and some are not.

When you need an order's detail, follow its `actions.fulfillment.url`. It returns a list of fulfillment orders, because one WHCC order can become several, and each item's `links.self` leads to that fulfillment order's `status`, `statusHistory`, and `shipments`. See [Orders & Fulfillment](https://www.whcc.com/developer/docs/api/fulfillment/index.html.md).

## If something goes wrong

- **`401 / 403`**: A plain `{ "message": … }` body means your key was rejected before the request reached the API. Check that it is the right key for this host. A Problem Details body with "Insufficient scope" in its `title` or `detail` means the key is valid but lacks the scope this route needs. Get in touch and we'll sort out the key.
- **`400`**: The request didn't validate, and `invalidParams[]` names the field, such as `items.0.quantity`. Common causes are a missing `shipping` or `Idempotency-Key`, a repeated `configuredProductId`, and a `configuredProductId` that doesn't exist on your account, including one built with another account's key. A `shipping.priority` that doesn't match `shipping.type` or isn't offered for the address lands here too (`code: "submission.shipping_priority_unsupported"` for the second). So does a basket that isn't ready to place (`code: "submission.not_submittable"`).
- **`404`**: Wrong path, or a submission that isn't yours. A `GET` to `/v1/commerce/submissions/calculate` reads as a submission lookup and returns `404` (`No submission found with id calculate`): calculate is a `POST`.
- **`409`**: An `Idempotency-Key` you already used with a different body. The same key with the same body is not an error. See [Idempotency](https://www.whcc.com/developer/docs/quick-start/#idempotency).
- **`400, 415`**: A body that isn't valid JSON returns `400` with `code: "request.invalid_json"`; a `Content-Type` other than `application/json` returns `415`; a body containing the key `__proto__` returns `400` with `code: "request.reserved_key"`. Fix the request and don't retry it as is.

Worked examples of every error shape and code are on [Errors & Rate Limits](https://www.whcc.com/developer/docs/api/errors/index.html.md).

## Going to production

When everything works in the sandbox, switch to production: point your requests at `https://apis.whcc.com/api` and use your production key. The paths, bodies and responses are the same. Keep the two keys in separate config so a test run can't hit production by accident ([API Key Security](https://www.whcc.com/developer/docs/api/api-key-security/index.html.md) has a simple setup). Your first production submission is a real order, so start small and watch it through to `order_placed` before you turn the flow on for customers.

> ## What's Next
>
> The [API reference](https://www.whcc.com/developer/docs/api/index.html.md) has the full response bodies, shipping priorities, metadata rules, error codes, and the submission lifecycle. If the API doesn't cover what you need, get in touch and we'll work out a custom solution.