Hammerdown

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.