Draft

Webhooks

Signed events for bookings, payments and conversations, with automatic retries.

Events

EventWhen
booking.createdA hold or booking was created
booking.confirmedA booking was confirmed (including after payment)
booking.cancelledCancelled or expired
booking.rescheduledMoved to a new time (payload has the old and new ids)
booking.no_showMarked as absent
payment.paidA Pix deposit was received
message.receivedA customer message arrived
conversation.handoffA conversation was handed to a person

Create endpoints in Developers → Webhooks or via POST /v1/webhook-endpoints. The signing secret is shown once.

Payload

{
  "id": "evt_01J9ZK3T6",
  "type": "booking.confirmed",
  "created_at": "2026-10-14T15:21:07-03:00",
  "workspace_id": "ws_9f2c",
  "data": { "booking": { "id": "bkg_7Qx1", "status": "confirmed", "start": "2026-10-15T09:45:00-03: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.

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.