Gareth Wilson Gareth Wilson

Guide to Whatnot Webhooks: Features and Best Practices

Published


Whatnot webhooks tell your systems what is happening in a seller's shop: a product sells, an order is placed or cancelled, a listing changes, a livestream is scheduled or ends, or a shipping label is printed. If you are building an app on the Whatnot Seller API, the GraphQL API behind the live-shopping marketplace, webhooks are how that activity reaches your inventory, fulfilment and reporting systems without polling.

This guide covers how Whatnot webhooks are delivered and signed, the 11 topics and their payloads, the constraints that come with an API still in development preview, and the best practices for running a Whatnot receiver in production.

What are Whatnot webhooks?

A Whatnot webhook is an HTTP POST to an endpoint you own, with a JSON body. Every delivery carries an X-Whatnot-Webhook-Signature header holding an HMAC-SHA256 of the raw request body, keyed with your app's webhook secret and encoded as lowercase hex.

The MAC covers the body bytes and nothing else. Whatnot signs no timestamp and adds no sha256= prefix, so if you have verified GitHub webhooks, it is the same computation with the prefix removed.

Every topic arrives in the same envelope, and the topic name is in the body rather than in a header. Whatnot describes the Seller API as a development preview with "a growing set of event topics", so new topics can appear without notice.

Whatnot webhook features

FeatureDetails
ConfigurationNo dashboard or API during the preview. Subscriptions are requested from Whatnot by email with your App ID, the URL and the topic
Signature headerX-Whatnot-Webhook-Signature: <hex digest>
SchemeHMAC-SHA256, lowercase hex, no prefix
Signed contentThe raw request body, nothing else
SecretOne webhook secret key per app, issued by a Whatnot representative
Replay windowNone. No timestamp is signed outside the body
Other headersX-Whatnot-Seller-Id, the seller the event belongs to
RoutingThe topic field in the body. There is no event-type header
Success response200 OK specifically
RetriesAny other response is retried up to 5 times over about 10 minutes
HandshakeNone documented
SDKNone for webhooks. Use your language's standard HMAC library

No source IP allowlist is published, so the signature is the only credential.

Common events

Whatnot documents 11 topics, each on its own page under the webhooks section of the developer docs. Route on the body's topic field.

TopicSent whenKey object fields
product/soldA product is sold. Whatnot suggests using it to remove sold quantity from your own stockproduct_id, listing_id, order_id, variant_id, quantity
order/createdAn order is createdorder_id, items[], subtotal, shipping_price, taxation, total, shipping_address, tracking_info
order/updatedAn existing order changes, including cancellationSame shape as order/created, with cancelled_at set on a cancelled order
listing/createdA listing is createdlisting_id, product_id, quantity, transaction_type (AUCTION or BUY_IT_NOW), listing_status
listing/updatedA listing is updatedSame shape as listing/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 livestreamlivestream_id, listing_id, product_id
livestream/product_removedA listing is removed from a livestreamSame shape as livestream/product_added
bulk_operation/finishedA bulk operation completes, so you know when to fetch its resultid, status
shipment/label_createdA shipping label is generated or regenerated. Marked Betashipment_id, reason, tracking_code, courier, label_url

Whatnot marks shipment/label_created as Beta: its types, behaviour and existence may change without notice, and Whatnot does not recommend it for production use.

Every delivery uses this envelope. Whatnot documents it with placeholder values rather than a real example:

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

id identifies the event and is your idempotency key. topic names the event, for example order/created, and object holds the topic-specific payload. The per-topic pages document only the object, again with placeholders. Here is the documented object for product/sold:

{
  "product_id": "<Product Global ID>",
  "listing_id": "<Listing Global ID>",
  "order_id": "<Order Global ID>",
  "external_id": "<Product External ID>",
  "variant_id": "<Variant Global ID, or null>",
  "variant_external_id": "<Variant External ID, or null>",
  "quantity": 1,
  "product_created_by_client_app_id": "<Client App ID that created the product, or null>"
}

IDs are GraphQL Global IDs, which you can pass straight back to the Seller API. external_id is your own product ID, if you set one. Money fields are objects with a string amount and an ISO 4217 currency, such as { "amount": "12.34", "currency": "USD" }.

Setting up Whatnot webhooks

During the development preview there is nowhere to add a webhook yourself:

  1. Create a Seller API app, if you do not have one, and note its App ID.
  2. Email Whatnot at the address on the webhooks overview page with your App ID, the URL that will receive the POST requests, and the topic or topics to subscribe to.
  3. Store the webhook secret key the Whatnot representative gives you server-side, for example as WHATNOT_WEBHOOK_SECRET.
  4. Request the read_customers scope for your app if you need shipping addresses, tracking details or label URLs. Without it those fields are null or missing.

The docs site renders its code blocks with JavaScript, so open it in a browser. A plain HTTP fetch returns the prose without the envelope, headers or sample code.

Because every subscription change goes through Whatnot staff, pick a URL you will not need to change. Pointing Whatnot at a stable gateway URL, and moving the destination behind it, saves a new request each time your receiver moves.

Securing Whatnot webhooks

Read the raw body, compute HMAC-SHA256 over it with your webhook secret, encode the result as hex, and compare it with X-Whatnot-Webhook-Signature in constant time. Check that the header is 64 hex characters first. That rejects a base64 or prefixed value outright, and it stops Node's timingSafeEqual throwing on a length mismatch, which would surface as a 500 that Whatnot then retries.

const crypto = require('crypto');
const express = require('express');

const HEX_SHA256 = /^[0-9a-f]{64}$/;

function verifyWhatnotSignature(rawBody, signature, secret) {
  if (!signature || !secret) return false; // fail closed

  const received = signature.trim().toLowerCase();
  if (!HEX_SHA256.test(received)) return false; // rejects base64, prefixes, wrong length

  // The secret is used as-is. The body is the raw bytes, never re-serialised JSON.
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(received, 'hex'), Buffer.from(expected, 'hex'));
}

const app = express();

app.post('/webhooks/whatnot', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.WHATNOT_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500);

  if (!verifyWhatnotSignature(req.body, req.get('x-whatnot-webhook-signature'), secret)) {
    return res.sendStatus(400);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  const sellerId = req.get('x-whatnot-seller-id');

  // Route on event.topic. Persist the event under event.id, then process async.
  // Whatnot expects exactly 200 OK, including for topics you don't handle yet.
  res.sendStatus(200);
});

The same check in Python:

import hashlib
import hmac
import os
import re

HEX_SHA256 = re.compile(r"^[0-9a-f]{64}$")


def verify_whatnot_signature(raw_body: bytes, signature: str | None, secret: str | None) -> bool:
    if not signature or not secret:
        return False  # fail closed

    received = signature.strip().lower()
    if not HEX_SHA256.match(received):
        return False

    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(received, expected)


# In FastAPI: raw_body = await request.body()
# verify_whatnot_signature(raw_body, request.headers.get("x-whatnot-webhook-signature"),
#                          os.environ["WHATNOT_WEBHOOK_SECRET"])

Whatnot's own Python sample computes the same digest with hexdigest() but compares it with ==, which is not constant-time. Use hmac.compare_digest or crypto.timingSafeEqual instead. Whatnot's sample also takes the body as a string and re-encodes it. Most frameworks hand you bytes already, so sign those directly.

Make Whatnot webhooks production-ready. Hookdeck Event Gateway verifies the X-Whatnot-Webhook-Signature header, answers Whatnot with an immediate 200 OK, and durably queues every order, listing and livestream event before your service sees it.

Whatnot webhook limitations and pain points

Ten minutes of retries, then nothing

The Problem: An outage longer than about ten minutes loses events for good, and an order or sale your systems never heard about has to be found some other way.

Why It Happens: Whatnot retries any response other than 200 OK up to 5 times over roughly 10 minutes, then stops. There is no manual redelivery. Whatnot also asks for 200 specifically, so a receiver that answers 202 Accepted or 204 No Content is retried as if it had failed.

Workarounds:

  • Return exactly 200 for every verified delivery, including topics you do not recognise yet.
  • Acknowledge first and process afterwards, so slow work never turns into a failed delivery.
  • After any outage, reconcile orders and listings from the Seller API for the period you were down.

How Hookdeck Can Help: Hookdeck answers each delivery as soon as it arrives and keeps it, then retries against your service on a schedule you set and lets you replay any event, so your own downtime does not count against Whatnot's ten-minute budget.

Nothing in the signature limits replay

The Problem: A delivery captured in transit or from a log verifies forever, so it can be sent to your endpoint again at any time.

Why It Happens: The MAC covers only the body. There is no signed timestamp header and no tolerance window, and Whatnot documents no source IP range to filter on.

Workarounds:

  • Store every envelope id and drop any delivery you have already processed.
  • If you want a staleness check as well, compare the envelope timestamp with your clock. It sits inside the signed body, so it cannot be altered without breaking the signature. This is your own policy, not part of Whatnot's scheme, so allow for the ten-minute retry window.

How Hookdeck Can Help: Hookdeck verifies the signature at the edge and can deduplicate deliveries before they reach you, so a resent event is caught in one place.

Subscriptions are changed by email

The Problem: Moving your receiver to a new URL, adding a topic or rotating a secret means waiting on a person at Whatnot.

Why It Happens: During the development preview there is no dashboard or API for webhook subscriptions, and the secret is issued by a Whatnot representative. Every change is a support request.

Workarounds:

  • Give Whatnot a URL that will not move, such as a gateway or load balancer address you control.
  • Ask for every topic you might need in the first request rather than one at a time.
  • Keep your App ID and current subscription list written down, because there is nowhere to look them up.

How Hookdeck Can Help: A Hookdeck source gives you one fixed URL to register with Whatnot. You can then point it at local development through the CLI, staging or production by changing the connection, without asking Whatnot for anything.

The docs show shapes, not payloads

The Problem: Code written against the documentation can break on the first real delivery, because no documented example is a real payload.

Why It Happens: Every example in Whatnot's webhook docs uses placeholder values such as "<Product Global ID>", and the per-topic examples show only the object, without the envelope. There is no test-delivery button either, so the first real payload you see is a live one.

Workarounds:

  • Treat ID formats and nullable fields as unknown until you have captured real deliveries.
  • Null-check shipping_address, tracking_info, label_url and customs, which can be null or missing.
  • Log the raw body of your first deliveries for each topic and build your fixtures from those.

How Hookdeck Can Help: Hookdeck keeps the full headers and raw body of every request a source receives, so the first live delivery of each topic becomes a real fixture you can inspect and replay into your handler.

Some topics leave you to work out what happened

The Problem: A handler cannot tell an ended livestream from a cancelled one, or rely on the shipment topic staying as it is.

Why It Happens: livestream/stopped fires when a livestream ends and when it is cancelled, and its payload is just the livestream_id. shipment/label_created is marked Beta, so its fields, behaviour and existence may change without notice.

Workarounds:

  • Query the livestream from the Seller API when you need to know how it stopped.
  • Keep shipment/label_created handling behind a feature flag, and fall back to the order's tracking_info on order/updated.
  • Answer 200 for unknown topics and log them, so a topic Whatnot adds during the preview shows up in your logs instead of in its retry queue.

How Hookdeck Can Help: Hookdeck filters let you route each topic to its own destination, so Beta or newly added topics can go to a separate consumer while you decide what to do with them.

Best practices

Verify against the raw body

The signature covers the exact bytes Whatnot sent. In Express, mount express.raw() on the webhook route and parse only after verification. In Next.js route handlers, read await request.text(). In FastAPI, read await request.body(). Parsing and re-serialising the JSON can change whitespace, key order or escaping, and the signature will not match.

Use the secret exactly as issued

The webhook secret key is the HMAC key as a UTF-8 string. Do not hex-decode or base64-decode it, and trim any whitespace that crept in when it was pasted into your configuration. If every delivery fails, check this before anything else.

Deduplicate on the envelope id

Retries resend the same event, so the same id can arrive more than once. Store it before you process, and skip anything you have already seen. See our guide to webhook idempotency.

Acknowledge with 200 and process asynchronously

Verify, persist the event, return 200, and do the real work afterwards. Whatnot gives you about ten minutes of retries, so a slow handler that times out burns through them quickly and produces duplicates on the way. See why to process webhooks asynchronously.

Keep money as strings

Money fields carry amount as a string such as "12.34". Keep it as a string or parse it into a decimal type. Converting it to a floating-point number introduces rounding errors in totals and tax.

Use the seller id in multi-seller apps

X-Whatnot-Seller-Id tells you which seller a delivery belongs to. If your app serves more than one seller, use it to look up the right account before you touch any data, and store it alongside each event.

Conclusion

Whatnot signs every delivery with a lowercase hex HMAC-SHA256 of the raw body in X-Whatnot-Webhook-Signature, with no timestamp and no prefix. Verify against the raw bytes in constant time, route on the body's topic, deduplicate on the envelope id, and answer 200 OK quickly, because ten minutes of retries is all you get.

Hookdeck Event Gateway sits in front of your receiver, checks every X-Whatnot-Webhook-Signature, and holds each event until your service has processed it, so an outage on your side costs latency instead of lost orders.

Get started with Hookdeck for free and handle Whatnot webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.