Agent skill

Whatnot Webhooks Skill

Receive and verify Whatnot webhooks (whatnot.com — the live-shopping and livestream-auction marketplace). Use when building a Whatnot developer app that handles topics like orders.created, orders.updated, shows.started, shows.ended, shipments.label_generated or products.updated, when debugging webhook-id / webhook-timestamp / webhook-signature (Standard Webhooks) verification with a wn_wh_ secret, or when migrating off the deprecated X-Whatnot-Webhook-Signature scheme. Whatnot signs base64 HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}`, keyed with the base64-decoded secret after stripping `wn_wh_`, with a 5-minute timestamp tolerance. Route on the body's `type`; the payload is under `data`.

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 developer platform (developers.whatnot.com). Whatnot signs every delivery with Standard Webhooks (webhook-id, webhook-timestamp, webhook-signature).

Canonical docs: Webhooks guide and the per-topic reference.

Whatnot replaced its webhook system in October 2026. If your handler reads X-Whatnot-Webhook-Signature or routes on topic, it's on the old system. See Deprecated: the pre-2026-10 webhook system.

When to Use This Skill

  • How do I receive Whatnot webhooks?
  • How do I verify a Whatnot webhook-signature?
  • Why does my Whatnot signature check fail with a wn_wh_ secret?
  • How do I handle orders.created, shows.started or shipments.label_generated?
  • How do I subscribe to a Whatnot webhook topic?
  • How do I migrate from X-Whatnot-Webhook-Signature and product/sold-style topics?

Verification (core)

HeaderValue
webhook-idUUID, unique per event. Retries reuse it, so it's the dedupe key
webhook-timestampUnix seconds when this attempt was sent. Changes on every retry
webhook-signatureOne or more space-separated v1,<base64 HMAC-SHA256> entries

Signed content is {webhook-id}.{webhook-timestamp}.{raw body}. The key is the dashboard secret with its wn_wh_ prefix stripped, then base64-decoded. Reject timestamps more than 5 minutes from your clock.

const crypto = require('crypto');

function verifyWhatnot(rawBody, headers, secret) {
  const id = headers['webhook-id'], ts = headers['webhook-timestamp'], sigs = headers['webhook-signature'];
  if (!id || !ts || !sigs) return false;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;             // 5-minute tolerance
  const key = Buffer.from(secret.replace(/^wn_wh_/, ''), 'base64');            // strip, then decode
  const expected = Buffer.from(
    crypto.createHmac('sha256', key).update(`${id}.${ts}.${rawBody}`).digest('base64')
  );
  return sigs.split(' ').some((entry) => {
    const [version, sig = ''] = entry.split(',');
    const received = Buffer.from(sig);
    return version === 'v1' && received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}
import base64, hashlib, hmac, time

def verify_whatnot(raw_body: bytes, headers, secret: str) -> bool:
    msg_id, ts, sigs = headers.get("webhook-id"), headers.get("webhook-timestamp"), headers.get("webhook-signature")
    if not (msg_id and ts and sigs) or abs(time.time() - int(ts)) > 300:  # 5-minute tolerance
        return False
    key = base64.b64decode(secret.removeprefix("wn_wh_"))                # strip, then decode
    digest = hmac.digest(key, f"{msg_id}.{ts}.".encode() + raw_body, hashlib.sha256)
    expected = base64.b64encode(digest).decode()
    for entry in sigs.split(" "):
        version, _, sig = entry.partition(",")
        if version == "v1" and hmac.compare_digest(sig, expected):
            return True
    return False

The examples use the official standardwebhooks library (npm ^1.1.1, PyPI >=1.1.0) instead. Strip wn_wh_ before passing the secret to it. The library only strips its own whsec_ prefix. With wn_wh_ left on, the Node library throws and the Python library silently decodes the wrong key, so every check fails.

const { Webhook } = require('standardwebhooks');
const event = new Webhook(process.env.WHATNOT_WEBHOOK_SECRET.replace(/^wn_wh_/, ''))
  .verify(rawBody, headers); // returns the parsed event; throws → respond 400

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

There is no Whatnot SDK for webhook verification.

Envelope

{
  "type": "shows.started",
  "timestamp": "2026-10-20T16:00:00.123Z",
  "data": { "show_id": "9b2f6c1e-4d3a-4f8b-a1c2-3d4e5f6a7b8c" },
  "version": "v2",
  "seller_id": "1234567"
}
  • Route on type. There's no event-type header.
  • The topic-specific payload is under data. Most topics send the same object the REST API returns, such as an order.
  • timestamp is when the event happened (ISO 8601, millisecond precision) and is stable across retries.
  • seller_id is part of the signed body, so multi-seller apps can route on it.
  • There's no event id in the body. Dedupe on the webhook-id header.

Topics

Each topic needs a scope that both your app and the seller's installation hold, or that seller's events aren't delivered.

TopicScopedata
orders.createdread:ordersOrder
orders.updatedread:ordersOrder
shows.createdread:showsShow. Excludes rehearsals
shows.updatedread:showsShow. Fires even when only updated_at changed
shows.startedread:shows{ show_id }. Also fires when a stopped show resumes
shows.endedread:shows{ show_id }. Includes Whatnot ending an abandoned show
shows.cancelledread:shows{ show_id }
merchandising.products.added_to_showread:merchandising{ product_id, show_id }
merchandising.products.removed_from_showread:merchandising{ product_id, show_id }. Also fires when the listing was deleted
shipments.createdread:shipmentsShipment
shipments.label_generatedread:shipments{ shipment_id, reason, tracking, documents }
shipments.tracking_code_addedread:shipments{ shipment_id, tracking }
shipments.parcel_updatedread:shipments{ shipment_id, weight, dimensions, hazardous_material, signature_required, insurance }
shipments.items.addedread:shipments{ shipment_id, shipment_item_ids }
shipments.items.removedread:shipments{ shipment_id, shipment_item_ids }
products.createdread:inventoryProduct
products.updatedread:inventoryProduct. Doesn't say which fields changed
products.deletedread:inventory{ id }

The topic string uses underscores (shipments.label_generated), but the reference page URLs hyphenate it (.../post/shipments.label-generated). Use the underscore form in code. Whatnot says new topics and fields may be added, so acknowledge unknown types with a 2xx. Full payload shapes are in references/overview.md.

Delivery

  • Any 2xx counts as delivered. Whatnot times out after 5 seconds, so respond first and process asynchronously.
  • Non-2xx responses, timeouts and connection failures are retried with exponential backoff, up to 11 attempts over about 2.5 days, then marked permanently failed.
  • At least once, no ordering guarantee. orders.updated can arrive before orders.created. Reconcile on the payload's own timestamps and state, never on arrival order.
  • A persistently failing endpoint is rate-limited, emailed a warning and eventually unsubscribed. Re-subscribe from the dashboard.
  • Respond 4xx to a delivery that fails verification.

Gotchas

Use the raw body. Whatnot sends space-separated JSON ({"type": "..."}), so parsing and re-serializing it changes the bytes and breaks the signature. In Express, use express.raw() on the webhook route; in Next.js, await request.text(); in FastAPI, await request.body().

Strip wn_wh_, then base64-decode. HMAC with the decoded bytes, not the base64 text and not the full prefixed string.

Keep the timestamp check for direct deliveries. Every Whatnot retry is re-signed with a fresh webhook-timestamp, so the 5-minute window doesn't drop legitimate retries. Requests replayed later by a proxy that keeps the original headers (Hookdeck retries and replays, for example) fail it after 5 minutes, so behind Hookdeck rely on the source's verification or widen the window.

Customer data needs read:customers. Without it these are null, so null-check them: buyer on orders; recipient, destination_address and documents on shipments.created; documents on shipments.label_generated. Addresses and shipping documents also need customer-address access (private apps have it; public apps get it once Whatnot approves them).

Money is a string. { "amount": "49.77", "currency": "USD" }. Keep amount as a string or decimal, never a float.

No handshake and no IP allowlist. Whatnot documents neither.

Subscribing

In the Whatnot developer dashboard, open your client app, register your webhook URL and choose topics. There's no REST endpoint for managing subscriptions. Copy the webhook secret (wn_wh_...) from the same place. See references/setup.md.

Environment Variables

# REQUIRED. Copy from the developer dashboard, wn_wh_ prefix included.
WHATNOT_WEBHOOK_SECRET=wn_wh_your_base64_secret

The examples strip the prefix themselves and 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 register in the Whatnot dashboard, plus a web UI showing each request's raw body and headers. (Use port 8000 for the FastAPI example.)

Deprecated: the pre-2026-10 webhook system

These topics are deprecated as of 2026-10-24. That's the date we confirmed the replacement; Whatnot published no migration notice or sunset date of its own.

Deprecated topicStatus
product/soldDeprecated 2026-10-24
bulk_operation/finishedDeprecated 2026-10-24
listing/createdDeprecated 2026-10-24
listing/updatedDeprecated 2026-10-24
order/createdDeprecated 2026-10-24
order/updatedDeprecated 2026-10-24
livestream/createdDeprecated 2026-10-24
livestream/stoppedDeprecated 2026-10-24
livestream/product_addedDeprecated 2026-10-24
livestream/product_removedDeprecated 2026-10-24
shipment/label_createdDeprecated 2026-10-24

Whatnot no longer documents these names, the topic field, the {id, timestamp, topic, object} envelope or the X-Whatnot-Webhook-Signature hex-HMAC scheme. The old doc URLs return 404 as of 2026-10-28. Existing integrations must move to Standard Webhooks verification and the topics above.

There's no one-to-one rename table. The old resource model (product, listing, livestream, bulk operation) doesn't map directly onto the new one (shows, orders, shipments, merchandising, products), so choose new topics by what your handler needs rather than by name.

Reference Materials

Repository

hookdeck/webhook-skills

v0.2.1 · MIT · Updated Oct 19, 2026

View on GitHub →