Developers

Webhooks

Subscribe once, react to everything. Webhooks let your stack respond to bookings, payments, and member lifecycle events without polling.

Lifecycle

  1. Create a subscription via POST /webhooks with your callback URL and the event types you care about.
  2. Hapana stores a per-subscription signing secret and returns it once on creation. Save it — it can't be retrieved again, only rotated.
  3. When a matching event fires, Hapana POSTs the event payload to your URL with an X-Hapana-Signature header.
  4. Your endpoint returns 2xx within 10 seconds to acknowledge. Anything else (or a timeout) is a delivery failure.

Verifying the signature

Every delivery includes X-Hapana-Signature as an HMAC-SHA256 of the raw request body, hex-encoded, prefixed with the timestamp:

X-Hapana-Signature: t=1745601234,v1=4f8c3b...
X-Hapana-Delivery: del_2N7s8x9KqR
Content-Type: application/json

Reconstruct and compare in constant time:

import crypto from 'node:crypto'

function verify(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
  const signed = `${parts.t}.${rawBody}`
  const expected = crypto.createHmac('sha256', secret).update(signed).digest('hex')
  const provided = Buffer.from(parts.v1, 'hex')
  const expectedBuf = Buffer.from(expected, 'hex')
  if (provided.length !== expectedBuf.length) return false
  return crypto.timingSafeEqual(provided, expectedBuf)
}
Always verify on the raw bytes. Re-serializing the JSON body changes whitespace and key order, breaking the signature. Capture the body before any parser touches it.

Replay protection

Reject deliveries where t is more than 5 minutes from the current time. Combined with HMAC verification, this prevents an attacker who captures one delivery from replaying it later.

Retry schedule

Failed deliveries are retried with exponential backoff over 24 hours. After the final attempt, the delivery is marked permanently failed and surfaced in the admin app.

AttemptDelay after failure
1immediate
230 seconds
32 minutes
410 minutes
51 hour
64 hours
712 hours
8final, gives up

Manually re-deliver any past event via POST /webhooks/{id}/deliveries/{deliveryId}/redeliver.

Idempotency on your side

Treat the X-Hapana-Delivery header as the unique ID for the delivery and de-duplicate against it. The same event may be delivered more than once during retries or operator-triggered redeliveries.

Event catalogue

The full list lives in the OpenAPI spec under the webhooks section. Common events:

  • client.created, client.updated, client.archived
  • booking.created, booking.cancelled, booking.checkedIn, booking.noShow
  • payment.succeeded, payment.failed, payment.refunded
  • package.purchased, package.cancelled, package.expired
  • session.created, session.updated, session.cancelled

Rotating the signing secret

Call POST /webhooks/{id}/rotate-secret to issue a new secret. Hapana signs deliveries with both the old and new secret for a 24-hour overlap window so you can roll without dropping deliveries.