API Docs
Dashboard

Webhooks

Register an endpoint and we'll POST a signed event to it whenever booking or invoice activity happens — so your systems stay in sync without polling.

Events

Subscribe to any of these event types when you create an endpoint:

EventWhen it firesPayload data
booking.createdA booking is created (via the API or your booking pages).Booking
booking.updatedA booking's status changes to anything except cancelled.Booking
booking.cancelledA booking is cancelled.Booking
booking.refundedA booking payment is refunded (fully or partially).Refund summary
invoice.createdAn invoice is created.Invoice
invoice.paidAn invoice becomes fully paid.Payment summary

Register an endpoint

Create a webhook with an https URL and the events you care about. The response includes a secret — this is the only time it's returned, so store it securely. You'll use it to verify every delivery.

Manage endpoints any time with the Webhooks endpoints in the reference.

Request

curl -X POST https://app.appointment.dev/api/v1/webhooks \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks",
    "events": ["booking.created", "booking.cancelled"]
  }'

Response

{
  "data": {
    "endpoint_id": 1,
    "url": "https://example.com/webhooks",
    "events": ["booking.created", "booking.cancelled"],
    "status": "enabled",
    "secret": "whsec_182022b9888a6d2f2307..."
  },
  "meta": { "request_id": "req_1a2b3c4d5e6f7a8b" }
}

The event payload

Each delivery is a POST with a JSON body and these headers:

  • X-Appointment-Event — the event type.
  • X-Appointment-Delivery — the unique event id.
  • X-Appointment-Signature — t=<timestamp>,v1=<signature>.

Respond with any 2xx status to acknowledge receipt.

POST body

{
  "id": "evt_d49eba5792d7699d5a5d",
  "type": "booking.created",
  "created": 1785148251,
  "data": {
    "booking_id": 1001,
    "status": "confirmed",
    "customer_name": "Ada Lovelace"
  },
  "meta": { "business_id": 7 }
}

Verifying signatures

Before trusting a delivery, recompute the signature and compare it. Take the t and v1 values from the X-Appointment-Signature header, then compute HMAC-SHA256(t + "." + rawBody, secret) and check it equals v1. Always compare using a constant-time function.

$payload = file_get_contents('php://input');
$header  = $_SERVER['HTTP_X_APPOINTMENT_SIGNATURE'];
parse_str(strtr($header, ',', '&'), $p); // t=..., v1=...

$expected = hash_hmac(
    'sha256', $p['t'] . '.' . $payload, $secret
);

if (!hash_equals($expected, $p['v1'])) {
    http_response_code(400);
    exit;
}
$event = json_decode($payload, true);
const crypto = require("crypto");

// rawBody must be the exact bytes received.
const header = req.headers["x-appointment-signature"];
const p = Object.fromEntries(
  header.split(",").map((kv) => kv.split("="))
);

const expected = crypto
  .createHmac("sha256", secret)
  .update(p.t + "." + rawBody)
  .digest("hex");

if (
  !crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(p.v1)
  )
) {
  return res.status(400).end();
}
const event = JSON.parse(rawBody);

Retries & delivery

If your endpoint doesn't return a 2xx, we retry with exponential backoff (roughly 1, 2, 4, 8 and 16 minutes) for up to six attempts, then mark the delivery failed. Inspect recent attempts with GET /api/v1/webhooks/{id}/deliveries.

Make your handler idempotent. A delivery may arrive more than once. De-duplicate on the event id so retries are harmless.