Agent skill

Whatnot Webhooks Skill

Receive and verify Whatnot webhooks (whatnot.com — the live-shopping and livestream-auction marketplace). Use when building a Whatnot Seller API app that handles webhook topics like order/created, order/updated, product/sold, listing/created, livestream/stopped or bulk_operation/finished, or when debugging X-Whatnot-Webhook-Signature verification. Whatnot signs with HMAC-SHA256 over the RAW request body, lowercase hex, keyed with the app's webhook secret key (issued by a Whatnot representative). No timestamp, no prefix. The topic is in the JSON body (`topic`), the payload under `object`.

Install this skill

npx skills add hookdeck/webhook-skills --skill 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. 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.

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:

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/, examples/nextjs/, 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:

{
  "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

HeaderValue
X-Whatnot-Seller-IdThe seller the event belongs to. Useful for multi-seller apps
X-Whatnot-Webhook-SignatureLowercase hex HMAC-SHA256 of the raw body

Whatnot documents no other X-Whatnot-* headers.

Topics

TopicSent whenobject fields
product/soldA product is soldproduct_id, listing_id, order_id, external_id, variant_id, variant_external_id, quantity, product_created_by_client_app_id
bulk_operation/finishedA bulk operation completesid, status (a GraphQL BulkOperationStatus)
listing/createdA listing is createdproduct_id, listing_id, external_id, quantity, transaction_type (AUCTION or BUY_IT_NOW), listing_status, condition, category_name, category_id
listing/updatedA listing is updatedSame shape as listing/created
order/createdAn order is createdIDs, quantity, Money totals, items[], shipping_address, tracking_info, cancelled_at (see overview)
order/updatedAn order is updatedSame shape as order/created
livestream/createdA seller creates a livestreamlivestream_id, title, starts_at
livestream/stoppedA livestream ends or is cancelledlivestream_id
livestream/product_addedA listing is added to a livestreamproduct_id, livestream_id, listing_id, product_external_id
livestream/product_removedA listing is removed from a livestreamSame shape as livestream/product_added
shipment/label_createdA shipping label is generated or regenerated. Beta: Whatnot says it may change without noticeshipment_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) 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

# 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

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

Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Oct 19, 2026

View on GitHub →