You already run an integration on the Editor API (Legacy), the Order Submit API (Legacy), or both. This page moves that flow onto a WHCC API key instead of the consumer key, secret and access token, with an event subscription in place of the consumer callback. Each step below says what's different and links to the existing reference for the payload.
This page is for an existing integration being moved onto an API key. If you are building something new, start at the Quick Start instead: new integrations use the WHCC API, which is a different and simpler flow.
Hosts
https://
Sandbox. Build and test here first. Sandbox and production keys are separate.
https://
Production. A confirmed order here is a real, billable order.
Every path on this page is relative to that base, so /products means https://apis-stage.whcc.com/api/v1/products. The Editor API pages show these same paths on the prospector.dragdrop.design host, which belongs to the consumer-credential flow. Send API-key requests to the host above. The paths and the bodies are otherwise the same. The examples show the sandbox host and run as written with a sandbox key. On production, only the host and the key change.
Send the API key as a bearer token on every request. There is no token exchange, no /auth/access-token call, and no 90-minute expiry to manage.
1
Authorization: Bearer <YOUR_API_KEY>
Keep the key out of browsers, logs and source control. API Key Security covers where to store it and how to rotate it.
This flow needs a key granted both apikey and apikey:oas.
apikey
Ordinary partner access: event subscriptions, the fulfillment routes, and GET /discovery.
apikey:oas
The legacy routes below: the product reads, POST /editors, and the three ordering routes.
A request whose key doesn't carry a scope the route requires is refused with 403, with no code field to branch on. A one-field {"message":"Forbidden"} or {"message":"Unauthorized"} body comes from the gateway, before the request reached the service — check the host and the key.
GET /discovery lists the operations the key you present can call, with the scope each one requires, and returns the key's own scopes in grantedScopes. Discover routes for your key documents the response. Call it first: the legacy routes should appear under operations.
1
2
curl https://apis-stage.whcc.com/api/v1/discovery \
-H "Authorization: Bearer <YOUR_API_KEY>"
Each row names a route, the scope it requires, and the step that calls it.
GET /discovery
apikey · Checking your key
GET /products
apikey:oas · 1. Products
GET /products/
apikey:oas · 1. Products
GET /products/
apikey:oas · 1. Products
GET /designs
2. Designs · GZIP required · scope: see step 2
GET /designs/
2. Designs · GZIP required · scope: see step 2
POST /editors
apikey:oas · 3. Editor
PUT /oas/
apikey:oas · 4. Export
POST /oas/
apikey:oas · 5. Create
POST /oas/
apikey:oas · 6. Confirm
POST /fulfillment/
apikey · 7. Events
POST /fulfillment/
apikey · 7. Events
POST /fulfillment/
apikey · 7. Events
The three event-subscription rows are the ones this walkthrough calls. The resource has more — list, read, replace, disable, delete and the event-type catalogue — and all of them take apikey. Event Subscriptions is the full list.
The webhook registration routes are not on this list, and are not reachable with an API key. POST /oas/webhooks/create and POST /oas/webhooks/verify refuse an API key with 403, and they are retired for every credential — /oas/webhooks/create on a consumer credential overwrites the callback you registered through Order Submit, so don't call it to refresh the old path. Registration moved: see step 7.
GET /products returns the full list of products your credentials can order, and GET /products/{productId} returns one product. GET /products/{productId}/attributes returns that product's attribute categories and order attributes.
1
2
3
curl https://apis-stage.whcc.com/api/v1/products \
-H "Authorization: Bearer <YOUR_API_KEY>" \
-H "Accept: application/json"
The response shape is unchanged. Products documents GET /products, including editorCompatibility, which tells you which editor a product opens in. The single-product and attributes reads return the same product shape; neither has its own reference page yet.
These are the Editor API's product routes, kept for this flow. The same key also reads the WHCC API's catalog at /catalog/products, and you may prefer it for browsing what you can order. The two return different shapes, and the editor and ordering steps below are documented against the product fields these routes return.
Products that take a design — cards, ornaments and designer acrylic blocks — need a designId as well as a productId when you create the editor. GET /designs lists designs and GET /designs/{designId} returns one. Pass the product ids you want designs for in productIds: with none, the call returns 5x7 flat card designs only. Designs documents both calls.
GZIP compression is mandatory on both routes. Send Accept-Encoding: gzip on every request to GET /designs and GET /designs/{designId} (with curl, pass --compressed). Without it, the response is too large for some clients to receive.
1
2
3
curl --compressed "https://apis-stage.whcc.com/api/v1/designs?productIds=<PRODUCT_ID_1>,<PRODUCT_ID_2>" \
-H "Authorization: Bearer <YOUR_API_KEY>" \
-H "Accept: application/json"
Designs apply only to some products such as cards, ornaments, and designer acrylic blocks. Other products, including prints, wall art, albums, and books, do not have designs. For products that do not have designs, you will create the editor with a productId alone and omit designId entirely. Including a designId with a product that does not support designs returns an error.
POST /editors takes your customer's chosen product and returns an editorId and a URL that opens their editor session. Creating an Editor documents the payload in full, including the per-editor-type examples and the redirects you send your customer back through.
Leave userId out. On a consumer credential, userId had to match the accountId claim you minted the access token with. An API key resolves to one account on its own, so the field is optional and falls back to that account. If you do send it, it must be exactly the account the key resolves to — any other value is refused with 400 API-key requests cannot specify a different userId, and an empty string is refused too. Sending the identifier your old integration sent is the most likely cause of that refusal.
1
2
3
4
5
6
7
8
9
10
11
curl https://apis-stage.whcc.com/api/v1/editors \
-H "Authorization: Bearer <YOUR_API_KEY>" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"productId": "uLSWZLZzEWY5KWr9z",
"redirects": {
"complete": { "text": "Checkout", "url": "https://yourdomain.com/return?editor=%EDITOR_ID%" },
"cancel": { "text": "Cancel", "url": "https://yourdomain.com/cancel" }
}
}'
Redirect your customer to the returned URL. They come back to your complete URL when they finish.
PUT /oas/editors/export turns one or more finished editors into a priced, order-shaped payload. Export Editors for Ordering documents the request and the response in detail; Ordering is the short version.
1
2
3
4
5
curl -X PUT https://apis-stage.whcc.com/api/v1/oas/editors/export \
-H "Authorization: Bearer <YOUR_API_KEY>" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"editors": [{ "editorId": "sampleEditorId" }]}'
The response carries an order object. That object is the body of step 5, after you fill in the fields the export left null.
On the Order Submit API (Legacy)? Start here. Steps 1 to 4 are the Editor API path. You already build an OrderImport body yourself; send that same body to this route, then confirm it in step 6. You don't call the Order Submit API's own import and submit routes on an API key.
POST /oas/orders/create takes the order object from the export as its body and returns a ConfirmationID. The schema is the Order Submit API's OrderImport schema, documented field by field under Order Import; Ordering shows a complete example body and explains the OrderAttributes that select shipping.
1
2
3
4
5
curl https://apis-stage.whcc.com/api/v1/oas/orders/create \
-H "Authorization: Bearer <YOUR_API_KEY>" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d @order.json
order.json is the order object step 4 returned, with the fields it left null filled in; Ordering shows a complete body. The order is attributed to the account your key resolves to. Nothing in the body changes that.
An order is not produced until it is confirmed. POST /oas/orders/{confirmationId}/confirm takes no body.
1
2
curl -X POST https://apis-stage.whcc.com/api/v1/oas/orders/a3ff9b4a-3112-4101-88ab-6ba025fd7600/confirm \
-H "Authorization: Bearer <YOUR_API_KEY>"
The response fields are the Order Submit API's, documented under Order Submit.
The three ordering routes share an error contract that is narrower than the rest of this API:
400
Almost everything. A malformed body, a rejected field, an editor that doesn't exist, and an upstream failure all arrive as 400. Read the message rather than branching on the status.
403
Either your key doesn't carry apikey:oas, or the request points at an account that isn't yours. These are passed through rather than flattened, so a 403 here is always one of those two.
500
Something is wrong with the key itself rather than with your request. Retrying the same call won't help. Contact WHCC support.
This is the part of the flow that changes most. On a consumer credential you registered one callback URL, out of band, through POST /api/callback/create, and completed a verification handshake. On an API key you create an event subscription yourself, through the WHCC API, and there is no handshake.
Event Subscriptions is the reference: every route, every field, every refusal. This section covers only what is different from what you have now.
| Consumer callback (what you have) | Event subscription (what you move to) | |
|---|---|---|
| Registering | POST /api/callback/create on the Order Submit API, with your consumer credentials. |
POST /fulfillment/event-subscriptions, with your API key. Self-service, and you can also list, replace, enable, disable, test and delete. |
| Proving you own the URL | A verification POST arrives during the create call, carrying a verifier you post back to /api/callback/verify. Registering again resets you to unverified. |
No handshake. A new subscription is created disabled and delivers nothing until you enable it, so you can send yourself a test event first. |
| How many | One per set of credentials. A second registration overwrites the first. | Several per account, each with its own URL, event selection and secret. |
| Which account it covers | The consumer's default account. | The one account your key resolves to. You never send an account identifier, and a body that names one is refused. |
| Signing key | Your consumer secret — the same secret you pair with your consumer key to get an access token. | A per-subscription signing secret, returned in the create response and by no later read. Replaying the same create with the same Idempotency-Key returns it again; nothing else does. Store it before you do anything else. |
| Choosing events | No selection. You receive everything the registration produces. | eventTypes, using the dotted names, or ["*"] for all of them. |
| Payload | Whichever version your integration was set up on: V1 form-encoded or V2 JSON. | V2 JSON by default. With apikey:oas on the key, destination.version: 1 selects the V1 form-encoded format. Without it, asking for 1 returns 403 (insufficient_scope). |
| If it stops working | Nothing to read. You find out from the absence of webhooks. | WHCC suspends a subscription after 20 consecutive failed attempts. Read it back to see status, statusDetail and lastDeliveryError. |
The two registrations are independent, and both can be live at once. Creating an event subscription does not cancel a consumer callback, and deleting one does not affect the other. This describes a V1 consumer callback alongside a V2 subscription. If both point at the same URL, your endpoint receives every event twice — once signed with your consumer secret, once with the subscription's signing secret. Plan the cutover deliberately. While both are live, deduplicate on ConfirmationId, SequenceNumber and event type together, as Delivery, Retries & Idempotency describes — but translate the event name into the subscription vocabulary before you key on it. If your consumer callback is on V1, its copy of an accepted order says order.received where the subscription copy says order.accepted, so keying on the name as delivered never matches the two copies, and every event is processed twice. A destination.version: 1 subscription also delivers order.received, so a V1 callback paired with a V1 subscription on the same URL produces two identical bodies; deduplicate those by EntryId and EventId. Duplicate deliveries covers the same case from the subscription side.
A subscription delivers the V2 JSON bodies documented in Event Types, each ending with an EventType field; branch on that rather than on Event. If your integration still receives the form-encoded V1 bodies, moving to a subscription is also a move to V2, which V1 Webhook Format covers.
Every delivery carries a WHCC-Signature header, and the way you compute a signature does not change on this path. Security and Signatures has the recipe — read it before you write the handler. Three things about it matter more here than on the path you are leaving:
A subscription's deliveries are signed with its signing secret, not your consumer secret. Your consumer secret still signs consumer-callback deliveries, and nothing else. Verify a subscription delivery with the consumer secret and every check fails, in exactly the way a wrongly assembled payload fails.
While both registrations are live, your endpoint holds two secrets, and a delivery does not say which one signed it. Don't guess. Compute an expected signature for each secret you hold for that endpoint and accept the delivery if any expected signature matches any v1= element. Which secret walks through it. Once the consumer callback is gone, the same loop runs over one secret.
Parse v1= as a list, not a single value. The header can carry more than one signature, and the recipe collects every element. Why "one or more" describes the window in which WHCC signs with both an old and a new secret; a subscription's secret can't be rotated yet, so on this path you will see one element per delivery today. Write the loop anyway. A handler that reads only the first element looks correct now and rejects deliveries it should have accepted later.
1
2
3
4
5
6
7
8
9
10
11
12
curl https://apis-stage.whcc.com/api/v1/fulfillment/event-subscriptions \
-H "Authorization: Bearer <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"destination": {
"type": "webhook",
"callbackUri": "https://path.to.your/webhook/endpoint"
},
"eventTypes": ["order.accepted", "order.rejected", "order.shipped"],
"description": "Production endpoint"
}'
Store signingSecret from the 201, deploy your handler, send a test event, then enable the subscription. Event Subscriptions explains each of those, and why an Idempotency-Key on create is worth sending.
Event Subscriptions — the full reference for step 7.
Security and Signatures — verify WHCC-Signature on a delivery.
Delivery, Retries & Idempotency — the retry schedule, the response timeout, and why your handler must deduplicate.
API Key Security — where to keep the key.