Webhooks
Have Hammerdown post a signed payload to your server the moment an auction changes, instead of polling for it. Available to stores on the Pro plan.
Registering an endpoint
In the app's Developers tab, enter the HTTPS URL that should receive deliveries and save it. The store is issued a signing secret at that point, shown in the same panel. One endpoint is registered per store and receives every event the store has enabled.
Which events reach it is controlled per event in the app's notification settings, on the webhook channel. Webhook delivery is off by default for every event, so nothing is sent until the store turns one on.
Endpoint verification
Before any real event is delivered, the endpoint has to prove it is yours. Pressing Verify posts a signed challenge, and your endpoint must answer 2xx with the challenge value echoed back. This is what stops the app being pointed at a third party who never agreed to receive traffic, and it doubles as a check that your signature verification works.
Verification request
{
"type": "url_verification",
"challenge": "kV9m2Xq7cRt0aZbN4pLwUeH8"
}
Reply with either the raw challenge string or a JSON object carrying it; both are accepted.
Verification response
{ "challenge": "kV9m2Xq7cRt0aZbN4pLwUeH8" }
Send test
in the same panel posts a benign payload of
{"type": "test", ...}
at any time and reports the status your endpoint returned, which is the quickest way to
confirm a deploy has not broken your receiver.
Verifying a signature
Every delivery is signed with your store's secret. Verify it before trusting a payload: the URL is public, so the signature is the only thing separating a real delivery from anyone who has guessed it.
Headers
| Name | Example | Description |
|---|---|---|
| X-Hammerdown-Event | auction_won | The event name. |
| X-Hammerdown-Delivery | 918273 | Stable id for this delivery, repeated across retries. |
| X-Hammerdown-Timestamp | 1787923200 | Unix seconds when the request was signed, and part of the signed string. |
| X-Hammerdown-Signature | sha256=9f86d0… | Lowercase-hex HMAC-SHA256, prefixed with the algorithm. |
The signed string is the timestamp, a literal dot, and the exact raw request body: HMAC-SHA256(secret, "<timestamp>.<body>"). Sign the bytes you received, not a re-serialized copy, since re-encoding JSON can
reorder keys and change whitespace. Binding the timestamp into the signature is what lets
you reject a replayed delivery: check that it is recent before accepting.
Node.js
const crypto = require("crypto");
function verify(req, secret) {
const timestamp = req.headers["x-hammerdown-timestamp"];
const signature = req.headers["x-hammerdown-signature"];
// req.rawBody is the unparsed body, e.g. express.json({ verify: ... })
const expected =
"sha256=" +
crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${req.rawBody}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
return (
fresh &&
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
);
}
Compare in constant time, as above, rather than with ===.
Payload
Every event carries the same envelope. The auction
is the
same object
the API returns, so you can act on a delivery without a follow-up request.
customer_id
is set on events about one shopper and null on events about the auction as a whole.
Request body
{
"id": 918273,
"event": "auction_won",
"occurred_at": "2026-08-26T15:00:04.221871Z",
"shop_domain": "your-store.myshopify.com",
"customer_id": "9920775979235",
"auction": {
"id": 4182,
"status": "ended",
"currency": "USD",
"high_bid": 13000,
"winner_customer_id": "9920775979235",
"...": "every field of the auction object"
}
}
Events
Customer events concern one shopper and carry their customer_id; merchant events concern the auction
and are the ones worth routing to your own operations tooling.
Event names
| Name | About | Description |
|---|---|---|
| auction_invited | customer | A customer was added to a guest list. |
| outbid | customer | A customer no longer holds the leading bid. |
| auction_won | customer | An auction ended and this customer won. |
| win_canceled | customer | A merchant released a win, for example after the winner never paid. |
| bid_canceled | customer | A merchant retracted a bid. |
| reserve_not_met | merchant | An auction ended with bids that never reached the reserve. |
| ended_no_bids | merchant | An auction ended without a single bid. |
| winner_unpaid | merchant | A winner has not paid within the store's grace period. |
| auction_stalled | merchant | A live auction has gone unusually quiet after heavy bidding. |
Retries and duplicates
Answer 2xx as soon as you have stored the delivery, and do the real work afterwards. Any other status, or a connection failure, is retried with backoff, so a slow handler that times out will be sent the same event again.
Retries reuse the delivery id in X-Hammerdown-Delivery, so treat that as an
idempotency key: record it and ignore a delivery you have already processed. Deliveries
are not ordered, either. Two events about one auction can arrive out of order, so decide
current state from the auction
object in the payload rather than from the order deliveries land in.
Recent deliveries and the status each returned are listed in the Developers tab, which is the first place to look when your receiver seems to be missing events.