Skip to content

Webhooks

AvailEngine sends webhooks to your server when things happen. Your app stays in sync without polling.

Available Events

EventDescription
booking.createdA new booking was created
booking.confirmedDeposit paid, booking is now confirmed
booking.updatedBooking details changed (time, resource, etc.)
booking.checked_inCustomer has arrived
booking.completedService is done
booking.cancelledBooking was cancelled
booking.no_showCustomer did not show up
deposit.paidDeposit payment succeeded
deposit.capturedDeposit was charged (no-show or late cancellation)
deposit.releasedDeposit 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:

AttemptDelay
15 seconds
230 seconds
35 minutes
430 minutes
52 hours

After 5 failed attempts, the delivery is dropped. Monitor your webhook endpoint health to avoid losing events.

Released under the MIT License.