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:
| Event | When |
|---|---|
booking.created | A hold or booking was created |
booking.confirmed | A booking was confirmed |
booking.cancelled | Cancelled or expired |
booking.rescheduled | Moved to a new time (payload has the old and new ids) |
booking.no_show | Marked as absent |
booking.completed | Appointment 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
httpsand resolve to a public address: we rejectlocalhost, private networks, link-local and cloud metadata addresses (when you register it and on every delivery). Redirects are not followed (a3xxcounts as a failure). - Extra headers:
Wagend-Event(type) andWagend-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}/pingsends awebhook.pingtest event;POST /v1/webhook-endpoints/{id}/rotate-secretissues a new secret (shown once; the old one stops working immediately).- With an API key use the
webhooks:managescope. The log is atGET /v1/webhook-endpoints/{id}/deliveriesand resending atPOST …/deliveries/{delivery_id}/resend.