# Dropship webhooks and key rotation

import StepVideo from '@/components/StepVideo'

# Dropship webhooks and key rotation

<StepVideo id="aff-14-dropship-webhooks" />

Use signed webhooks to follow order changes, with [order lookup](/docs/dropshipping/orders#look-up-an-order) as a fallback. An HTTP delivery and a customer-facing storefront update are separate operations; verify and reconcile before changing the store.

## Register your endpoint

1. Open **Dropship** in your approved account.
2. Enter a public **HTTPS** URL on your store's backend.
3. Choose **Save & generate secret**.
4. Store the signing secret privately before leaving the page; it is shown once. Keep it separate from test and live API tokens.
5. Configure the receiver to read the original request body and verify its signature before parsing or processing the event.

The delivery uses these headers:

| Header | Purpose |
| --- | --- |
| `X-KeepKey-Signature` | `t=<unix timestamp>,v1=<hex HMAC>` |
| `X-KeepKey-Webhook-Id` | Delivery identifier for deduplication |
| `X-KeepKey-Event` | Event type; reconcile it with the identified order |

## Verify before parsing

The signature is **HMAC-SHA256** with the signing secret over `timestamp + "." + rawBody`. Preserve the received body bytes; parsing and reserializing JSON can change the signature input. Reject invalid signatures and timestamps outside the five-minute tolerance.

This Node.js example demonstrates the verification step. Pass the raw body and header received by your framework; it does not register a route or persist delivery IDs:

```js
import { createHmac, timingSafeEqual } from 'node:crypto'

export function verifyKeepKeyWebhook(secret, header, rawBody, now = Math.floor(Date.now() / 1000)) {
  if (!secret || typeof header !== 'string' || !Buffer.isBuffer(rawBody)) return false
  const parts = Object.fromEntries(header.split(',').map(part => part.trim().split('=')))
  const timestamp = Number(parts.t)
  if (!Number.isInteger(timestamp) || timestamp <= 0 || Math.abs(now - timestamp) > 300) return false
  if (!/^[a-f0-9]{64}$/i.test(parts.v1 ?? '')) return false
  const expected = createHmac('sha256', secret)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest()
  const received = Buffer.from(parts.v1, 'hex')
  return received.length === expected.length && timingSafeEqual(received, expected)
}
```

After verification, durably record or enqueue the delivery and deduplicate `X-KeepKey-Webhook-Id` before applying changes. Return success after your receiver has safely accepted the event. Repeated deliveries must not apply a storefront change twice. An authenticated payload still needs to match the intended account and order.

## Retain a lookup fallback

Deliveries can be late, repeated or missing. Use `GET /orders` with the known KeepKey or external order ID to reconcile an unexpected state or recover after downtime. Do not infer shipment from an accepted create request, a successful webhook HTTP response or null tracking fields.

For order creation, retries use the same external ID and identical payload. Handle conflicts and permanent rejections explicitly; use bounded backoff for temporary transport errors.

## Rotate credentials deliberately

**Rotate API keys** replaces the live and test tokens together. The previous tokens stop working, so update the intended backend configurations and verify a harmless authenticated read before relying on live order submission.

**Update & regenerate secret** changes the webhook signing secret. Update your receiver as part of that change; it is a separate secret from the order API tokens. Review pending deliveries and reconcile through order lookup after a credential change or outage.

Test the receiver, deduplication, lookup fallback and credential replacement before scaling live automation. Your recording or practice request should use fictional customer data and non-payable values; actual customer information belongs only in the authorized fulfillment workflow.
