# Whatnot Webhooks

Whatnot (whatnot.com) is a live-shopping marketplace where sellers run
livestream auctions and buy-it-now sales. This skill covers webhooks from the
Whatnot Seller API (developers.whatnot.com), a GraphQL API that Whatnot
describes as being in development preview.

Canonical docs: [Webhooks overview](https://developers.whatnot.com/docs/webhooks/overview).
The docs site renders its code blocks with JavaScript, so a plain HTTP fetch of
the page shows the prose but not the envelope, headers or sample code. Open it
in a browser.

## When to Use This Skill

* How do I receive Whatnot webhooks?
* How do I verify `X-Whatnot-Webhook-Signature`?
* Why does my Whatnot signature check fail?
* How do I handle `order/created`, `product/sold` or `livestream/stopped`?
* How do I subscribe to a Whatnot webhook topic?
* Does Whatnot retry failed webhooks?

## Verification (core)

HMAC-SHA256 over the raw request body bytes, keyed with your app's
webhook secret key, sent as a lowercase hex digest in
`X-Whatnot-Webhook-Signature`. Nothing else is signed: no timestamp, no
prefix, no field concatenation.

```javascript
const crypto = require('crypto');

function verifyWhatnotSignature(rawBody, signature, secret) {
  if (!signature || !secret) return false;                  // fail closed
  const received = signature.trim().toLowerCase();          // hex is case-insensitive
  if (!/^[0-9a-f]{64}$/.test(received)) return false;      // also guards timingSafeEqual's length check
  const expected = crypto
    .createHmac('sha256', secret)                           // the webhook secret, as-is
    .update(rawBody)                                        // RAW bytes, never re-serialized JSON
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(received, 'hex'), Buffer.from(expected, 'hex'));
}

```

Whatnot's own sample (Python, from the overview page) is the same computation:

```python
calculated_signature = hmac.new(
    bytes(secret, 'utf-8'), bytes(raw_request_body, 'utf-8'), hashlib.sha256
).hexdigest()
return calculated_signature == signature

```

It compares with `==`. Use `hmac.compare_digest` (Python) or
`crypto.timingSafeEqual` (Node) instead, as the examples here do.

Hookdeck's `WHATNOT` source type verifies the same way: HMAC-SHA256, hex,
header `x-whatnot-webhook-signature`, over the raw body.

> For complete handlers with tests, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/whatnot-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/whatnot-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/whatnot-webhooks/examples/fastapi/).

There is no Whatnot SDK for webhook verification. Use `node:crypto`, Web
Crypto, or Python's `hmac` + `hashlib`.

## Envelope

Every delivery is an HTTP `POST` with this body:

```json
{
  "id": "<Event ID: UUID>",
  "timestamp": "<Event Timestamp: ISO 8601 UTC>",
  "topic": "<Event Topic>",
  "object": { "...": "<Object type specific to topic>" }
}

```

* Route on `topic`. There is no event-type header.
* The topic-specific payload is under `object`.
* Dedupe on `id`. Retries resend the same event.

### Headers

| Header | Value |
| --- | --- |
| `X-Whatnot-Seller-Id` | The seller the event belongs to. Useful for multi-seller apps |
| `X-Whatnot-Webhook-Signature` | Lowercase hex HMAC-SHA256 of the raw body |

Whatnot documents no other `X-Whatnot-*` headers.

## Topics

| Topic | Sent when | `object` fields |
| --- | --- | --- |
| `product/sold` | A product is sold | `product_id`, `listing_id`, `order_id`, `external_id`, `variant_id`, `variant_external_id`, `quantity`, `product_created_by_client_app_id` |
| `bulk_operation/finished` | A bulk operation completes | `id`, `status` (a GraphQL `BulkOperationStatus`) |
| `listing/created` | A listing is created | `product_id`, `listing_id`, `external_id`, `quantity`, `transaction_type` (`AUCTION` or `BUY_IT_NOW`), `listing_status`, `condition`, `category_name`, `category_id` |
| `listing/updated` | A listing is updated | Same shape as `listing/created` |
| `order/created` | An order is created | IDs, `quantity`, Money totals, `items[]`, `shipping_address`, `tracking_info`, `cancelled_at` (see [overview](https://github.com/hookdeck/webhook-skills/blob/main/skills/whatnot-webhooks/references/overview.md)) |
| `order/updated` | An order is updated | Same shape as `order/created` |
| `livestream/created` | A seller creates a livestream | `livestream_id`, `title`, `starts_at` |
| `livestream/stopped` | A livestream ends or is cancelled | `livestream_id` |
| `livestream/product_added` | A listing is added to a livestream | `product_id`, `livestream_id`, `listing_id`, `product_external_id` |
| `livestream/product_removed` | A listing is removed from a livestream | Same shape as `livestream/product_added` |
| `shipment/label_created` | A shipping label is generated or regenerated. Beta: Whatnot says it may change without notice | `shipment_id`, `reason` (`label_generated` or `label_regenerated`), `tracking_code`, `courier`, `label_url`, `label_generated_at` |

IDs are GraphQL Global IDs. Whatnot says more topics may be added during the
preview, so acknowledge unknown topics with `200` rather than erroring.

## Gotchas

Use the raw body. The digest covers the exact bytes Whatnot sent. Parsing
the JSON and re-serializing it can change whitespace, key order or escaping
and break the check. In Express, use `express.raw()` on the webhook route; in
Next.js, `await request.text()`; in FastAPI, `await request.body()`.

Respond with `200 OK`. Whatnot's docs say the request "must be responded
to with HTTP Status 200 OK". Any other status is retried up to 5 times over
roughly 10 minutes. Return 200, not 202 or 204, and do slow work after
responding.

Retries mean duplicates. Dedupe on the envelope `id`.

No replay protection is built in. There is no timestamp header and no
tolerance window in the scheme. The envelope's `timestamp` sits inside the
signed body, so it can't be altered without breaking the signature, but a
captured delivery can still be resent unchanged. Deduping on `id` handles
that. If you also want a staleness check, compare `timestamp` against the
clock yourself. That's this skill's suggestion, not something Whatnot
specifies.

Customer data needs a scope. `shipping_address`, `tracking_info` and
`label_url` are only populated for apps granted the `read_customers` scope.
Without it they're `null` or missing, so null-check them.

Money is a string. Monetary fields are `{ "amount": "12.34", "currency": "USD" }`.
Keep `amount` as a string or decimal, never a float.

No handshake and no IP allowlist. Whatnot documents neither. Don't invent
one.

## Subscribing

There's no dashboard or API for creating subscriptions during the preview.
Contact Whatnot by email (the address is on the
[overview page](https://developers.whatnot.com/docs/webhooks/overview#creating-a-webhook))
with your App ID, the URL that will receive the POSTs, and the
topic. A Whatnot representative also gives you the webhook secret key.

## Environment Variables

```bash
# REQUIRED. Your app's webhook secret key, issued by a Whatnot representative.
WHATNOT_WEBHOOK_SECRET=your_whatnot_webhook_secret

```

The examples fail closed: with the secret unset they return 500 rather
than skip verification.

## Local Development

```bash
npx hookdeck-cli listen 3000 whatnot --path /webhooks/whatnot

```

No account required. The CLI gives you a public HTTPS URL to send Whatnot as
your webhook URL, plus a web UI showing each request's raw body and headers.
(Use port `8000` for the FastAPI example.)

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/whatnot-webhooks/references/overview.md): envelope, every topic and its payload, Money and customer-data rules, delivery and retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/whatnot-webhooks/references/setup.md): getting a webhook secret and subscribing to topics during the preview
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/whatnot-webhooks/references/verification.md): the signature byte by byte, a test vector, and debugging failures