Appearance
Webhooks
AvailEngine sends webhooks to your server when things happen. Your app stays in sync without polling.
Available Events
| Event | Description |
|---|---|
booking.created | A new booking was created |
booking.confirmed | Deposit paid, booking is now confirmed |
booking.updated | Booking details changed (time, resource, etc.) |
booking.checked_in | Customer has arrived |
booking.completed | Service is done |
booking.cancelled | Booking was cancelled |
booking.no_show | Customer did not show up |
deposit.paid | Deposit payment succeeded |
deposit.captured | Deposit was charged (no-show or late cancellation) |
deposit.released | Deposit was refunded |
Setting Up Webhooks
Register your endpoint:
bash
curl -X POST https://api.availengine.com/v1/manage/webhooks \
-H "Authorization: Bearer avail_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/availengine",
"events": ["booking.created", "booking.cancelled", "deposit.paid"]
}'AvailEngine generates a secret for this endpoint. Store it securely — you'll use it to verify payloads.
Payload Format
Every webhook is a POST with a JSON body:
json
{
"event": "booking.created",
"timestamp": "2026-05-15T14:00:00Z",
"data": {
"booking_id": "uuid",
"business_id": "uuid",
"customer": {
"first_name": "Jane",
"last_name": "Doe",
"email": "jane@example.com"
},
"resource_id": "uuid",
"booking_date": "2026-05-15",
"start_time": "14:00",
"end_time": "15:00",
"status": "confirmed"
}
}Verifying Signatures
Every webhook includes an X-AvailEngine-Signature header. The header format is t={timestamp},v1={signature} — an HMAC-SHA256 of {timestamp}.{payload} keyed with your webhook secret. This prevents replay attacks by including a timestamp with a 5-minute tolerance window.
JavaScript
js
const crypto = require('crypto');
function verifyWebhook(payload, signatureHeader, secret, toleranceSeconds = 300) {
// Parse the t={ts},v1={sig} header
const parts = {};
for (const part of signatureHeader.split(',')) {
const [k, v] = part.split('=', 2);
if (k && v) parts[k.trim()] = v.trim();
}
const ts = parts.t;
const sig = parts.v1;
if (!ts || !sig) return false;
// Reject signatures older than tolerance
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(ts)) > toleranceSeconds) return false;
// Recompute the signature
const signed = `${ts}.${payload}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signed)
.digest('hex');
const expectedHeader = `t=${ts},v1=${expected}`;
return crypto.timingSafeEqual(
Buffer.from(expectedHeader),
Buffer.from(signatureHeader)
);
}Python
python
import hmac
import hashlib
import time
def verify_webhook(payload: str, signature_header: str, secret: str, tolerance: int = 300) -> bool:
# Parse the t={ts},v1={sig} header
parts = {}
for part in signature_header.split(","):
try:
k, v = part.split("=", 1)
parts[k.strip()] = v.strip()
except ValueError:
continue
ts = parts.get("t")
sig = parts.get("v1")
if not ts or not sig:
return False
# Reject signatures older than tolerance
if abs(int(time.time()) - int(ts)) > tolerance:
return False
# Recompute the signature
signed = f"{ts}.{payload}"
expected_sig = hmac.new(
secret.encode(), signed.encode(), hashlib.sha256
).hexdigest()
expected_header = f"t={ts},v1={expected_sig}"
return hmac.compare_digest(expected_header, signature_header)Responding
Always respond with 200 OK within 10 seconds. If you return 4xx or 5xx, AvailEngine retries with exponential backoff (5s, 30s, 5m, 30m, 2h).
Do heavy work (email, SMS, calendar sync) in a background job — don't block the webhook handler.
Event Catalog
Every event has a standard envelope with event-specific data.
Envelope
json
{
"event": "booking.created",
"timestamp": "2026-05-15T14:00:00Z",
"sandbox": false,
"data": { ... }
}booking.created
Fires when a new booking is created (public widget or staff dashboard).
json
{
"event": "booking.created",
"data": {
"booking_id": "uuid",
"business_id": "uuid",
"customer": {
"id": "uuid",
"first_name": "Jane",
"last_name": "Doe",
"email": "jane@example.com",
"phone": "+301234567890"
},
"resource": { "id": "uuid", "name": "Maria" },
"booking_date": "2026-05-15",
"start_time": "14:00",
"end_time": "15:00",
"capacity": 1,
"status": "confirmed",
"confirmation_code": "AV-ABC123",
"source": "online",
"customer_notes": "Prefers window seat",
"deposit": null
}
}booking.confirmed
Fires when a pending booking transitions to confirmed (deposit paid or auto-confirmed).
json
{
"event": "booking.confirmed",
"data": {
"booking_id": "uuid",
"business_id": "uuid",
"previous_status": "pending",
"status": "confirmed",
"confirmation_code": "AV-ABC123"
}
}booking.updated
Fires when booking details change (time, resource, capacity, notes).
json
{
"event": "booking.updated",
"data": {
"booking_id": "uuid",
"business_id": "uuid",
"changes": {
"resource_id": { "from": "old-uuid", "to": "new-uuid" },
"start_time": { "from": "14:00", "to": "14:30" }
}
}
}booking.checked_in
Fires when a customer arrives.
json
{
"event": "booking.checked_in",
"data": {
"booking_id": "uuid",
"business_id": "uuid",
"confirmed_at": "2026-05-14T10:30:00Z",
"checked_in_at": "2026-05-15T13:55:00Z",
"confirmation_code": "AV-ABC123"
}
}booking.completed
Fires when the service or appointment is done.
json
{
"event": "booking.completed",
"data": {
"booking_id": "uuid",
"business_id": "uuid",
"checked_in_at": "2026-05-15T14:05:00Z",
"completed_at": "2026-05-15T14:35:00Z",
"duration_minutes": 30
}
}booking.cancelled
Fires when a booking is cancelled by customer or staff.
json
{
"event": "booking.cancelled",
"data": {
"booking_id": "uuid",
"business_id": "uuid",
"previous_status": "confirmed",
"cancellation_reason": "Customer request",
"cancelled_by": "staff",
"deposit_action": "released",
"deposit_amount_cents": 2000
}
}booking.no_show
Fires when a customer doesn't show up.
json
{
"event": "booking.no_show",
"data": {
"booking_id": "uuid",
"business_id": "uuid",
"previous_status": "confirmed",
"deposit_action": "captured",
"deposit_amount_cents": 2000
}
}deposit.paid
Fires when a deposit payment succeeds.
json
{
"event": "deposit.paid",
"data": {
"booking_id": "uuid",
"deposit_id": "uuid",
"business_id": "uuid",
"amount_cents": 2000,
"stripe_payment_intent_id": "pi_xxx",
"booking_status_after": "confirmed"
}
}deposit.captured
Fires when a held deposit is captured (no-show or late cancellation).
json
{
"event": "deposit.captured",
"data": {
"booking_id": "uuid",
"deposit_id": "uuid",
"business_id": "uuid",
"amount_cents": 2000,
"stripe_payment_intent_id": "pi_xxx",
"reason": "no_show"
}
}deposit.released
Fires when a held deposit is refunded (timely cancellation).
json
{
"event": "deposit.released",
"data": {
"booking_id": "uuid",
"deposit_id": "uuid",
"business_id": "uuid",
"amount_cents": 2000,
"stripe_payment_intent_id": "pi_xxx",
"reason": "cancellation_within_policy"
}
}Testing Webhooks
Use the sandbox environment (avail_test_ keys) and the X-AvailEngine-Sandbox: true header. Sandbox webhooks go to your registered endpoints but are tagged with "sandbox": true.
Local Testing
Use a tool like webhook.site to receive webhooks during development. Register the webhook.site URL as your endpoint, then trigger test bookings. Inspect the payloads in real-time.
Retry Schedule
Failed deliveries retry with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | 5 seconds |
| 2 | 30 seconds |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
After 5 failed attempts, the delivery is dropped. Monitor your webhook endpoint health to avoid losing events.