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

# Migrate Auth to API Key

Version 2.8 &middot; Updated 2026-10-09

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](https://www.whcc.com/developer/docs/quick-start/index.html.md) instead: new integrations use the [WHCC API](https://www.whcc.com/developer/docs/api/index.html.md), which is a different and simpler flow.

> **Hosts**
>
> - **`https://apis-stage.whcc.com/api/v1`**: Sandbox. Build and test here first. Sandbox and production keys are separate.
> - **`https://apis.whcc.com/api/v1`**: 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](https://www.whcc.com/developer/docs/legacy/editor-api/index.html.md) 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.

## The credential

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.

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

Keep the key out of browsers, logs and source control. [API Key Security](https://www.whcc.com/developer/docs/api/api-key-security/index.html.md) covers where to store it and how to rotate it.

### Your key carries two scopes

This flow needs a key granted **both**`apikey` and `apikey:oas`.

- **`apikey`**: Ordinary partner access: [event subscriptions](https://www.whcc.com/developer/docs/legacy/api-key-bridge/#webhooks), the [fulfillment routes](https://www.whcc.com/developer/docs/api/fulfillment/index.html.md), and `GET /discovery`.
- **`apikey:oas`**: The legacy routes [below](https://www.whcc.com/developer/docs/legacy/api-key-bridge/#surface): 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 &mdash; check the host and the key.

### Checking what your key reaches

`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](https://www.whcc.com/developer/docs/api/#discovery) documents the response. Call it first: the legacy routes should appear under `operations`.

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

## What this flow calls

Each row names a route, the scope it requires, and the step that calls it.

- **`GET /discovery`**: `apikey` &middot; [Checking your key](https://www.whcc.com/developer/docs/legacy/api-key-bridge/#discovery)
- **`GET /products`**: `apikey:oas` &middot; [1. Products](https://www.whcc.com/developer/docs/legacy/api-key-bridge/#products)
- **`GET /products/{productId}`**: `apikey:oas` &middot; [1. Products](https://www.whcc.com/developer/docs/legacy/api-key-bridge/#products)
- **`GET /products/{productId}/attributes`**: `apikey:oas` &middot; [1. Products](https://www.whcc.com/developer/docs/legacy/api-key-bridge/#products)
- **`GET /designs`**: [2. Designs](https://www.whcc.com/developer/docs/legacy/api-key-bridge/#designs) &middot; GZIP required &middot; scope: see step 2
- **`GET /designs/{designId}`**: [2. Designs](https://www.whcc.com/developer/docs/legacy/api-key-bridge/#designs) &middot; GZIP required &middot; scope: see step 2
- **`POST /editors`**: `apikey:oas` &middot; [3. Editor](https://www.whcc.com/developer/docs/legacy/api-key-bridge/#editors)
- **`PUT /oas/editors/export`**: `apikey:oas` &middot; [4. Export](https://www.whcc.com/developer/docs/legacy/api-key-bridge/#export)
- **`POST /oas/orders/create`**: `apikey:oas` &middot; [5. Create](https://www.whcc.com/developer/docs/legacy/api-key-bridge/#create-order)
- **`POST /oas/orders/{confirmationId}/confirm`**: `apikey:oas` &middot; [6. Confirm](https://www.whcc.com/developer/docs/legacy/api-key-bridge/#confirm)
- **`POST /fulfillment/event-subscriptions`**: `apikey` &middot; [7. Events](https://www.whcc.com/developer/docs/legacy/api-key-bridge/#webhooks)
- **`POST /fulfillment/event-subscriptions/{id}/test`**: `apikey` &middot; [7. Events](https://www.whcc.com/developer/docs/legacy/api-key-bridge/#first-subscription)
- **`POST /fulfillment/event-subscriptions/{id}/enable`**: `apikey` &middot; [7. Events](https://www.whcc.com/developer/docs/legacy/api-key-bridge/#first-subscription)

The three event-subscription rows are the ones this walkthrough calls. The resource has more &mdash; list, read, replace, disable, delete and the event-type catalogue &mdash; and all of them take `apikey`. [Event Subscriptions](https://www.whcc.com/developer/docs/api/event-subscriptions/index.html.md) 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](https://www.whcc.com/developer/docs/legacy/editor-api/webhooks/index.html.md) for every credential &mdash; `/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](https://www.whcc.com/developer/docs/legacy/api-key-bridge/#webhooks).

## Step 1: Read the products

`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.

```shell
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](https://www.whcc.com/developer/docs/legacy/editor-api/products/index.html.md) 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`](https://www.whcc.com/developer/docs/quick-start/#step-1), 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.

## Step 2: Read designs, for designed products

Products that take a design &mdash; cards, ornaments and designer acrylic blocks &mdash; 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](https://www.whcc.com/developer/docs/legacy/editor-api/designs/index.html.md) 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.

```shell
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.

## Step 3: Create an editor

`POST /editors` takes your customer's chosen product and returns an `editorId` and a URL that opens their editor session. [Creating an Editor](https://www.whcc.com/developer/docs/legacy/editor-api/create-editor/index.html.md) 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 &mdash; 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.

```shell
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.

## Step 4: Export the finished editors

`PUT /oas/editors/export` turns one or more finished editors into a priced, order-shaped payload. [Export Editors for Ordering](https://www.whcc.com/developer/docs/legacy/editor-api/order-export/index.html.md) documents the request and the response in detail; [Ordering](https://www.whcc.com/developer/docs/legacy/editor-api/ordering/#export_editors) is the short version.

```shell
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](https://www.whcc.com/developer/docs/legacy/api-key-bridge/#create-order), after you fill in the fields the export left `null`.

## Step 5: Create the order

**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](https://www.whcc.com/developer/docs/legacy/order-submit-api/order-import/index.html.md); [Ordering](https://www.whcc.com/developer/docs/legacy/editor-api/ordering/#create_order) shows a complete example body and explains the `OrderAttributes` that select shipping.

```shell
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](https://www.whcc.com/developer/docs/legacy/api-key-bridge/#export) returned, with the fields it left `null` filled in; [Ordering](https://www.whcc.com/developer/docs/legacy/editor-api/ordering/#create_order) shows a complete body. The order is attributed to the account your key resolves to. Nothing in the body changes that.

## Step 6: Confirm the order

An order is not produced until it is confirmed. `POST /oas/orders/{confirmationId}/confirm` takes no body.

```shell
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](https://www.whcc.com/developer/docs/legacy/order-submit-api/order-submit/index.html.md).

### What a refusal on these three routes looks like

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.

## Step 7: Receive order events

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](https://www.whcc.com/developer/docs/api/event-subscriptions/index.html.md) is the reference: every route, every field, every refusal. This section covers only what is different from what you have now.

### What changes

|  | 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`](https://www.whcc.com/developer/docs/api/event-subscriptions/#create), 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](https://www.whcc.com/developer/docs/api/event-subscriptions/#enable) it, so you can [send yourself a test event](https://www.whcc.com/developer/docs/api/event-subscriptions/#test) 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 &mdash; the same secret you pair with your consumer key to get an access token. | A [per-subscription signing secret](https://www.whcc.com/developer/docs/api/event-subscriptions/#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`](https://www.whcc.com/developer/docs/api/event-subscriptions/#event-types), using the dotted names, or `["*"]` for all of them. |
| **Payload** | Whichever version your integration was set up on: [V1 form-encoded](https://www.whcc.com/developer/docs/legacy/order-submit-api/webhook-v1-format/index.html.md) or [V2 JSON](https://www.whcc.com/developer/docs/legacy/order-submit-api/webhook-types/index.html.md). | V2 JSON by default. With `apikey:oas` on the key, `destination.version: 1` selects the [V1 form-encoded format](https://www.whcc.com/developer/docs/api/event-subscriptions/#version-1). 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](https://www.whcc.com/developer/docs/api/event-subscriptions/#list) 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 &mdash; 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](https://www.whcc.com/developer/docs/legacy/order-submit-api/webhook-delivery/index.html.md) describes &mdash; 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](https://www.whcc.com/developer/docs/api/event-subscriptions/#duplicate-deliveries) covers the same case from the subscription side.

A subscription delivers the V2 JSON bodies documented in [Event Types](https://www.whcc.com/developer/docs/legacy/order-submit-api/webhook-types/index.html.md), 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](https://www.whcc.com/developer/docs/legacy/order-submit-api/webhook-v1-format/index.html.md) covers.

### Verifying a delivery

Every delivery carries a `WHCC-Signature` header, and the way you compute a signature does not change on this path. [Security and Signatures](https://www.whcc.com/developer/docs/legacy/order-submit-api/webhook-security/index.html.md) has the recipe &mdash; 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](https://www.whcc.com/developer/docs/legacy/order-submit-api/webhook-security/#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"](https://www.whcc.com/developer/docs/legacy/order-submit-api/webhook-security/#multiple-signatures) 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.

### Your first subscription

```shell
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](https://www.whcc.com/developer/docs/api/event-subscriptions/#test), then [enable](https://www.whcc.com/developer/docs/api/event-subscriptions/#enable) the subscription. [Event Subscriptions](https://www.whcc.com/developer/docs/api/event-subscriptions/index.html.md) explains each of those, and why an `Idempotency-Key` on create is worth sending.

> ## What's Next
>
> [Event Subscriptions](https://www.whcc.com/developer/docs/api/event-subscriptions/index.html.md) &mdash; the full reference for step 7.
>
> [Security and Signatures](https://www.whcc.com/developer/docs/legacy/order-submit-api/webhook-security/index.html.md) &mdash; verify `WHCC-Signature` on a delivery.
>
> [Delivery, Retries & Idempotency](https://www.whcc.com/developer/docs/legacy/order-submit-api/webhook-delivery/index.html.md) &mdash; the retry schedule, the response timeout, and why your handler must deduplicate.
>
> [API Key Security](https://www.whcc.com/developer/docs/api/api-key-security/index.html.md) &mdash; where to keep the key.