HASH PAY Docs
Docs/Reference/Webhooks
API reference

Webhooks

When a payment changes state, HashPay POSTs a signed JSON event to your store's webhook URL. This page is the catalogue and contract; for a build-it-together walkthrough see Step 6.

Event catalogue

The events you'll build on, all in the buyer-driven payment flow:

EventFires when
payment.detectedA matching transfer is first seen on-chain.
payment.confirmedThe transfer reaches the chain's confirmation depth. Fulfil on this.
payment.underpaidLess than the amount due was received.
payment.overpaidMore than the amount due was received.
payment.expiredThe payment window closed with nothing received.
payment.canceledThe buyer canceled the payment.

A set of subscription.* events (created, activated, renewed, upgraded, past_due, expired, reminder) exists for the beta subscriptions feature. They're not part of the standard checkout flow; see the note at the bottom.

Payload

Every event is an envelope wrapping an event-specific data object:

payment.confirmedjson
{
  "id": "pay_66f2a19b03c84d2ea1b7c4de:payment.confirmed",
  "type": "payment.confirmed",
  "createdAt": "2026-06-28T12:15:30Z",
  "apiVersion": "2026-07-02",
  "data": {
    "paymentId": "pay_66f2a19b03c84d2ea1b7c4de",
    "accountId": "acct_5b3e8c1fa2d9407be6135f9c",
    "storeId": "store_0c47d9e2ab35f18c6e24ba70",
    "mode": "live",
    "network": "base",
    "token": "usdc",
    "address": "0xA1b2…",
    "amount": "29.00",
    "amountBaseUnits": "29000000",
    "decimals": 6,
    "txHash": "0x9f8e…",
    "blockNumber": 19283746,
    "status": "CONFIRMED",
    "confirmations": 12,
    "metadata": { "orderId": "order_123" }
  }
}

Data fields

FieldNotes
idStable event id (<paymentId>:<type>). Same on retries — your idempotency key.
typeThe event name.
createdAtWhen the event was generated (ISO 8601).
apiVersionPayload schema version, currently "2026-07-02". Bumped only on a breaking payload change.
data.paymentIdThe payment's pay_… id.
data.accountId · data.storeIdYour account and the originating store.
data.mode"test" or "live".
data.network · token · addressChain, stablecoin, and your receiving address.
data.amount · amountBaseUnits · decimalsDisplay and exact amounts.
data.txHash · blockNumberThe settling transaction. Absent until detected.
data.statusThe payment status name (e.g. "CONFIRMED").
data.confirmationsPresent once detected/confirmed.
data.originHostHost of the page that opened the checkout — present when captured.
data.metadataWhatever you attached at checkout — present when set.
data.expectedAmountunderpaid / overpaid only
data.receivedAmount · receivedAmountBaseUnitsunderpaid / overpaid only

Delivery headers

HeaderValue
X-HashPay-EventThe event type, e.g. payment.confirmed.
X-HashPay-DeliveryThe event id — stable across retries, and authenticated (it's also inside the signed body).
X-HashPay-Signaturet=<unix seconds>,v1=<hex>[,v1=<hex>…] — delivery timestamp + one signature per active secret.

Signing

Each v1 is an HMAC-SHA256 (hex) over the string <t> + "." + <raw request body>, keyed with your store's whsec_ secret. Recompute it and compare in constant time against every v1 entry — accept if any matches (there's one per active secret, so a rotation drops nothing). Reject deliveries whose t is more than 5 minutes from your clock; retries are re-signed with a fresh t, so a legitimate redelivery never ages out. The one rule that matters: hash the raw bytes before any JSON parsing reformats them.

verify.jsnode
const parts = signatureHeader.split(',')
const t = (parts.find((p) => p.startsWith('t=')) || '').slice(2)
const expected = crypto.createHmac('sha256', SECRET).update(`${t}.`).update(rawBody).digest('hex')
const ok = Math.abs(Date.now() / 1000 - Number(t)) <= 300 &&   // 5-min replay window
  parts.filter((p) => p.startsWith('v1=')).some((p) =>       // any active secret
    crypto.timingSafeEqual(Buffer.from(p.slice(3)), Buffer.from(expected)))

Full verification snippets in Node, Python, PHP and Ruby are on the Step 6 page. The secret is per store, shown once when you set the webhook URL on the Integration page.

Delivery & retries

  • Timeout: each attempt waits up to 15 seconds for your response.
  • Success: any 2xx. Anything else (or a timeout) is a failure.
  • Retries: delivery is asynchronous with exponential backoff, retried for up to 72 hours; after that the delivery is marked dead. Acknowledge quickly and do slow work afterward, or one payment becomes several deliveries.
  • Idempotency: retries reuse the same id / X-HashPay-Delivery. Record processed ids and skip duplicates so fulfilment runs exactly once.
  • Delivery log: every delivery (attempts, status, your endpoint's response) is inspectable in the dashboard under Integration → Delivery log, or via GET /v1/stores/{storeId}/webhook/deliveries with your sk_ key.

Subscription events (beta)

If you're in the subscriptions beta, seven more events fire: subscription.created, subscription.activated, subscription.renewed, subscription.upgraded, subscription.past_due, subscription.expired, and subscription.reminder. Their data carries subscriptionId, subscriberAccountId, storeId, tier, status, network, token, amount, buyerEmail, nextDueAt, renewUrl, and (on reminders) daysUntilDue. HashPay sends reminders and webhooks — it never auto-pulls funds.