Webhooks

Signed notifications for what happens to your bookings, with automatic retries, a delivery log and examples to verify the signature.

Updated:

Events

These booking events can be subscribed to today:

EventWhen
booking.createdA hold or booking was created
booking.confirmedA booking was confirmed
booking.cancelledCancelled or expired
booking.rescheduledMoved to a new time (payload has the old and new ids)
booking.no_showMarked as absent
booking.completedAppointment completed

The contract also reserves payment.paid, message.received and conversation.handoff, but they cannot be subscribed to yet and are not emitted (collecting the deposit is Coming soon). An endpoint that asks for them gets 422.

Create endpoints in Settings → Webhooks in the dashboard or with POST /v1/webhook-endpoints (url and events). The signing secret (whsec_…) is shown once.

curl -X POST https://api.fiuit.com/v1/webhook-endpoints \
  -H "Authorization: Bearer $WAGEND_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/wagend", "events": ["booking.confirmed", "booking.cancelled"] }'

Payload

{
  "id": "evt_3f6c1d9e0b7a4c2f8e5d1a9b7c3e6f20",
  "type": "booking.confirmed",
  "created_at": "2026-10-14T15:21:07-03:00",
  "workspace_id": "b3a6c8e2-5f1d-4c7a-9e0b-2d8f4a1c6e53",
  "data": { "booking": { "id": "6d1f0c4a-7b2e-4a58-9c3d-0e5f8a2b1c47", "status": "confirmed", "start_at": "2026-10-15T12:45:00+00:00" } }
}

Delivery is at least once: use id to ignore duplicates.

Verifying the signature

Every request has a header:

Wagend-Signature: t=1791040867,v1=5c2b9f...e81

v1 is HMAC-SHA256(secret, t + "." + raw_body) in hex. Reject requests older than 5 minutes and compute the signature over the raw body, without re-serializing the JSON.

import crypto from 'node:crypto'

export function verifyWagend(rawBody: string, header: string, secret: string) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=') as [string, string]))
  const age = Math.abs(Date.now() / 1000 - Number(parts.t))
  if (!parts.t || !parts.v1 || age > 300) return false
  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex')
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}
import hashlib, hmac, time

def verify_wagend(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts.get("t", "0"))) > 300:
        return False
    signed = f"{parts['t']}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Retries

Respond with any 2xx within 10 seconds. Otherwise we retry after 1 min, 5 min, 30 min, 2 h and 12 h. Every attempt is visible in the delivery log, where you can also resend manually.

Security and limits

  • The URL must be https and resolve to a public address: we reject localhost, private networks, link-local and cloud metadata addresses (when you register it and on every delivery). Redirects are not followed (a 3xx counts as a failure).
  • Extra headers: Wagend-Event (type) and Wagend-Delivery (delivery id). Every attempt is signed with a fresh timestamp.
  • Up to 10 endpoints per workspace. After 10 consecutive failures the endpoint is disabled (re-enable it with PATCH /v1/webhook-endpoints/{id} and {"active": true}).
  • POST /v1/webhook-endpoints/{id}/ping sends a webhook.ping test event; POST /v1/webhook-endpoints/{id}/rotate-secret issues a new secret (shown once; the old one stops working immediately).
  • With an API key use the webhooks:manage scope. The log is at GET /v1/webhook-endpoints/{id}/deliveries and resending at POST …/deliveries/{delivery_id}/resend.