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 to hear what happens next. The API Reference 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 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 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://
Sandbox. Build and test here first with a sandbox key. Sandbox and production keys are separate.
https://
Production. Submitting here places a real, billable order.
Every path below is relative to that base, so /v1/catalog/products means https://. 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. 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.
Not sure which routes your key can reach? Call Discovery. Every response also carries actions links that point at the next call, so you rarely need to build a URL by hand.
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.
1
Authorization: Bearer <YOUR_API_KEY>
/v1/catalog/productsReturns 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.
1
2
curl "https://apis-stage.whcc.com/api/v1/catalog/products?q=Framed+Print" \
-H "Authorization: Bearer <YOUR_API_KEY>"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"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.
/v1/catalog/products/{ref}Returns one product in full, including configuration.fields[]: the option names and values you'll send in Step 4.
1
2
curl https://apis-stage.whcc.com/api/v1/catalog/products/framed-print \
-H "Authorization: Bearer <YOUR_API_KEY>"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
"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.
A key in selections, such as size or surface.
configuration.
A valid value for that key, such as lustre_photo_print.
configuration.
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.
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:
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.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.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.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.
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:
1
2
3
4
5
"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.
/v1/commerce/configured-productsA 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".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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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" }
]
}
]
}'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"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.
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.
1
2
3
4
5
6
7
8
9
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" }
}
}'
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.
1
2
curl "https://apis-stage.whcc.com/api/v1/commerce/configured-products?ids=CF_bCy42bhE4xpJXXNezD2EQSkpAzrqBurvi8ICmrzmBTetrum" \
-H "Authorization: Bearer <YOUR_API_KEY>"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"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.
/v1/commerce/submissions/calculatePrices 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).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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"
}
}
}'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{
"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). 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[]
Whether this item can be ordered now. When false, the item's validationSummary says why. 1005 means it is still processing, so keep polling.
validation
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.
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"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".
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.
/v1/commerce/submissionsSend 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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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"
}
}
}'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"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) for the current state. It means WHCC accepted the request, not that an order was placed. Step 7 shows how to follow it from here.
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.
Submitting returns 202 before the order is placed. In your integration, let WHCC tell you what happened next: create an event subscription 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: 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.
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.
/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.
1
2
curl https://apis-stage.whcc.com/api/v1/commerce/submissions/<submissionId> \
-H "Authorization: Bearer <YOUR_API_KEY>"
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"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: 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.
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.
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.
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 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.
The API reference 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.