Webhooks

Webhook Event Reference

A worked example payload for every Booklink webhook event, including the ping.

Last updated

Every webhook request has the same outer envelope, with the changed booking or enrollment inside data. The examples below show a realistic payload for each event type. Identifiers are 16-character strings and all times are UTC in ISO 8601 format.

The envelope

{
  "id": "evt_8HgT2kQm9vLxRp4c",
  "type": "booking.created",
  "created": "2026-08-29T09:12:04.512Z",
  "data": { }
}
id
Unique per event, prefixed evt_. Use it to make your handler idempotent: the same event can arrive more than once.
type
One of the seven event names, or ping for a test.
created
When Booklink generated the event. Use it to discard payloads that arrive out of order after a retry.
data
The full booking for booking.* events, the full enrollment for enrollment.* events, and null for a ping.

Empty fields are omitted

Optional fields are left out of the JSON when they have no value, so do not assume every key below is always present. Always read fields defensively.

Referral attribution

Every booking.* and enrollment.* payload can carry an attribution object. It holds the ref, utm_source, utm_medium, utm_campaign, utm_term and utm_content query parameters that were on your booking link when the customer landed on it, so a link such as https://app.booklink.co.za/to/your-business?ref=partner-a&utm_source=newsletter lets you credit the booking to that partner or campaign in your own tools. Only those six keys are kept, each value is trimmed to 200 characters, and the object is set once at creation and never changes afterwards. It is omitted entirely when the link carried none of them.

booking.created

Sent as soon as a booking exists. On a service that requires payment, the booking arrives pending and unpaid. A booking.confirmed event follows later once payment lands or you confirm it manually.

{
  "id": "evt_8HgT2kQm9vLxRp4c",
  "type": "booking.created",
  "created": "2026-08-29T09:12:04.512Z",
  "data": {
    "id": "9mKr4Tq7XbNc2VdW",
    "client_id": "5pQz8LmT3nRk9WxB",
    "service_id": "2VnJ7kQx4LmT8pRc",
    "team_member_id": "7XcT2mQk9nLp4VrB",
    "room_id": "3JbN8vQt5mXd2LcR",
    "customer_name": "Naledi Mokoena",
    "customer_email": "[email protected]",
    "customer_phone": "+27821234567",
    "notes": "First visit. Allergic to almond oil.",
    "start_time": "2026-09-03T09:00:00Z",
    "end_time": "2026-09-03T10:00:00Z",
    "status": "pending",
    "payment_status": "unpaid",
    "reschedule_count": 0,
    "variant_id": "6TbK9mQx2nLp7VrD",
    "price_cents": 45000,
    "payment_expiry_minutes": 30,
    "conflict_status": "none",
    "custom_answers": [
      {
        "field_id": "4KcR7pQm2nTx9VbL",
        "label": "How did you hear about us?",
        "type": "text",
        "value": "Instagram"
      }
    ],
    "attribution": {
      "ref": "partner-a",
      "utm_source": "newsletter",
      "utm_campaign": "spring"
    },
    "items": [
      {
        "service_id": "2VnJ7kQx4LmT8pRc",
        "variant_id": "6TbK9mQx2nLp7VrD",
        "service_name": "Deep Tissue Massage",
        "variant_name": "60 minutes",
        "duration_minutes": 60,
        "price_cents": 45000,
        "payment_required": true,
        "deposit_cents": 15000
      }
    ],
    "policy_snapshot": {
      "auto_confirm": false,
      "payment_required": true,
      "deposit_cents": 15000,
      "payment_expiry_minutes": 30,
      "allow_cancel": true,
      "cancel_deadline_hours": 24,
      "allow_reschedule": true,
      "reschedule_deadline_hours": 24,
      "max_reschedules": 2,
      "booking_cutoff_minutes": 120,
      "require_phone": true
    },
    "created_at": "2026-08-29T09:12:04.487Z"
  }
}

A few fields are worth calling out. items is always present and lists every service on the booking, so a multi-service booking has more than one entry. policy_snapshot is the booking policy frozen at the time of booking, which is what your cancellation and reschedule logic should honour. All money is in cents, so 45000 means R450.00.

booking.confirmed

Sent when the booking becomes confirmed. On an auto-confirm service this arrives immediately after booking.created. After an online payment it arrives once the gateway confirms, with the payment fields filled in.

{
  "id": "evt_2QxL9mTk7RpN4VcB",
  "type": "booking.confirmed",
  "created": "2026-08-29T09:14:51.203Z",
  "data": {
    "id": "9mKr4Tq7XbNc2VdW",
    "client_id": "5pQz8LmT3nRk9WxB",
    "service_id": "2VnJ7kQx4LmT8pRc",
    "team_member_id": "7XcT2mQk9nLp4VrB",
    "customer_name": "Naledi Mokoena",
    "customer_email": "[email protected]",
    "customer_phone": "+27821234567",
    "start_time": "2026-09-03T09:00:00Z",
    "end_time": "2026-09-03T10:00:00Z",
    "status": "confirmed",
    "payment_status": "paid",
    "meet_link": "https://meet.google.com/abc-defg-hij",
    "transaction_id": "8LpQ2mXt4RvN7KcB",
    "amount_paid_cents": 45000,
    "payment_method": "yoco",
    "reschedule_count": 0,
    "variant_id": "6TbK9mQx2nLp7VrD",
    "price_cents": 45000,
    "conflict_status": "none",
    "items": [
      {
        "service_id": "2VnJ7kQx4LmT8pRc",
        "variant_id": "6TbK9mQx2nLp7VrD",
        "service_name": "Deep Tissue Massage",
        "variant_name": "60 minutes",
        "duration_minutes": 60,
        "price_cents": 45000,
        "payment_required": true,
        "deposit_cents": 15000
      }
    ],
    "policy_snapshot": {
      "auto_confirm": false,
      "payment_required": true,
      "deposit_cents": 15000,
      "payment_expiry_minutes": 30,
      "allow_cancel": true,
      "cancel_deadline_hours": 24,
      "allow_reschedule": true,
      "reschedule_deadline_hours": 24,
      "max_reschedules": 2,
      "booking_cutoff_minutes": 120,
      "require_phone": true
    },
    "created_at": "2026-08-29T09:12:04.487Z"
  }
}

booking.rescheduled

Sent when a booking moves. The payload carries the new times only, so store the previous times on your side if you need a before and after. reschedule_count tells you how many times the booking has moved.

{
  "id": "evt_5RpT8mQx2LcN9VbK",
  "type": "booking.rescheduled",
  "created": "2026-08-30T11:02:17.884Z",
  "data": {
    "id": "9mKr4Tq7XbNc2VdW",
    "client_id": "5pQz8LmT3nRk9WxB",
    "service_id": "2VnJ7kQx4LmT8pRc",
    "team_member_id": "7XcT2mQk9nLp4VrB",
    "customer_name": "Naledi Mokoena",
    "customer_email": "[email protected]",
    "customer_phone": "+27821234567",
    "start_time": "2026-09-05T14:00:00Z",
    "end_time": "2026-09-05T15:00:00Z",
    "status": "confirmed",
    "payment_status": "paid",
    "external_event_id": "6f2n8kqm4tvc9xbr",
    "transaction_id": "8LpQ2mXt4RvN7KcB",
    "amount_paid_cents": 45000,
    "payment_method": "yoco",
    "reschedule_count": 1,
    "calendar_sequence": 1,
    "variant_id": "6TbK9mQx2nLp7VrD",
    "price_cents": 45000,
    "conflict_status": "none",
    "items": [
      {
        "service_id": "2VnJ7kQx4LmT8pRc",
        "variant_id": "6TbK9mQx2nLp7VrD",
        "service_name": "Deep Tissue Massage",
        "variant_name": "60 minutes",
        "duration_minutes": 60,
        "price_cents": 45000,
        "payment_required": true,
        "deposit_cents": 15000
      }
    ],
    "policy_snapshot": {
      "auto_confirm": false,
      "payment_required": true,
      "deposit_cents": 15000,
      "payment_expiry_minutes": 30,
      "allow_cancel": true,
      "cancel_deadline_hours": 24,
      "allow_reschedule": true,
      "reschedule_deadline_hours": 24,
      "max_reschedules": 2,
      "booking_cutoff_minutes": 120,
      "require_phone": true
    },
    "created_at": "2026-08-29T09:12:04.487Z"
  }
}

booking.cancelled

Sent whenever a booking is cancelled. cancelled_by tells you who did it: operator for someone in your team, client for a self-service cancellation from the manage link, and system for automatic cancellations such as an unpaid booking expiring or a session being cancelled.

{
  "id": "evt_7VcN4mQt9LpR2KxB",
  "type": "booking.cancelled",
  "created": "2026-09-01T06:45:09.331Z",
  "data": {
    "id": "9mKr4Tq7XbNc2VdW",
    "client_id": "5pQz8LmT3nRk9WxB",
    "service_id": "2VnJ7kQx4LmT8pRc",
    "team_member_id": "7XcT2mQk9nLp4VrB",
    "customer_name": "Naledi Mokoena",
    "customer_email": "[email protected]",
    "customer_phone": "+27821234567",
    "start_time": "2026-09-05T14:00:00Z",
    "end_time": "2026-09-05T15:00:00Z",
    "status": "cancelled",
    "payment_status": "paid",
    "transaction_id": "8LpQ2mXt4RvN7KcB",
    "amount_paid_cents": 45000,
    "payment_method": "yoco",
    "reschedule_count": 1,
    "variant_id": "6TbK9mQx2nLp7VrD",
    "price_cents": 45000,
    "conflict_status": "none",
    "cancellation_reason": "Client is unwell.",
    "cancelled_by": "client",
    "cancelled_time": "2026-09-01T06:45:09.298Z",
    "items": [
      {
        "service_id": "2VnJ7kQx4LmT8pRc",
        "variant_id": "6TbK9mQx2nLp7VrD",
        "service_name": "Deep Tissue Massage",
        "variant_name": "60 minutes",
        "duration_minutes": 60,
        "price_cents": 45000,
        "payment_required": true,
        "deposit_cents": 15000
      }
    ],
    "policy_snapshot": {
      "auto_confirm": false,
      "payment_required": true,
      "deposit_cents": 15000,
      "payment_expiry_minutes": 30,
      "allow_cancel": true,
      "cancel_deadline_hours": 24,
      "allow_reschedule": true,
      "reschedule_deadline_hours": 24,
      "max_reschedules": 2,
      "booking_cutoff_minutes": 120,
      "require_phone": true
    },
    "created_at": "2026-08-29T09:12:04.487Z"
  }
}

booking.completed

Sent after a confirmed booking’s end time passes. A background sweep looks for newly ended bookings, so expect this shortly after the end time rather than on the exact second. The booking status stays confirmed; the event itself is what tells you the appointment is done.

{
  "id": "evt_3KxQ7mTv2NpL9RcB",
  "type": "booking.completed",
  "created": "2026-09-05T15:05:00.642Z",
  "data": {
    "id": "9mKr4Tq7XbNc2VdW",
    "client_id": "5pQz8LmT3nRk9WxB",
    "service_id": "2VnJ7kQx4LmT8pRc",
    "team_member_id": "7XcT2mQk9nLp4VrB",
    "customer_name": "Naledi Mokoena",
    "customer_email": "[email protected]",
    "customer_phone": "+27821234567",
    "start_time": "2026-09-05T14:00:00Z",
    "end_time": "2026-09-05T15:00:00Z",
    "status": "confirmed",
    "payment_status": "paid",
    "transaction_id": "8LpQ2mXt4RvN7KcB",
    "amount_paid_cents": 45000,
    "payment_method": "yoco",
    "reschedule_count": 1,
    "variant_id": "6TbK9mQx2nLp7VrD",
    "price_cents": 45000,
    "conflict_status": "none",
    "items": [
      {
        "service_id": "2VnJ7kQx4LmT8pRc",
        "variant_id": "6TbK9mQx2nLp7VrD",
        "service_name": "Deep Tissue Massage",
        "variant_name": "60 minutes",
        "duration_minutes": 60,
        "price_cents": 45000,
        "payment_required": true,
        "deposit_cents": 15000
      }
    ],
    "policy_snapshot": {
      "auto_confirm": false,
      "payment_required": true,
      "deposit_cents": 15000,
      "payment_expiry_minutes": 30,
      "allow_cancel": true,
      "cancel_deadline_hours": 24,
      "allow_reschedule": true,
      "reschedule_deadline_hours": 24,
      "max_reschedules": 2,
      "booking_cutoff_minutes": 120,
      "require_phone": true
    },
    "created_at": "2026-08-29T09:12:04.487Z"
  }
}

enrollment.created

Sent when a client is enrolled into a group or cohort, whether you added them or they signed up from the booking page. The data is the enrollment, not a booking, so the shape is different.

{
  "id": "evt_6NpL2mQx8TvR4KcB",
  "type": "enrollment.created",
  "created": "2026-08-29T13:22:41.907Z",
  "data": {
    "id": "4TbN9mQx7LpR2VcK",
    "cohort_id": "8VcR3mQt5LpN7KxB",
    "client_id": "5pQz8LmT3nRk9WxB",
    "customer_name": "Sipho Dlamini",
    "customer_email": "[email protected]",
    "customer_phone": "+27835557788",
    "variant_id": "2LmQ9xTv4RpN8KcB",
    "payment_model": "upfront",
    "total_amount_cents": 240000,
    "paid_amount_cents": 240000,
    "refunded_amount_cents": 0,
    "payment_status": "paid",
    "transaction_id": "9KxT4mQp2LvN7RcB",
    "checkout_id": "7RcB2mQx9LpT4VkN",
    "accepted_terms_at": "2026-08-29T13:22:38.114Z",
    "status": "active",
    "created_at": "2026-08-29T13:22:41.882Z"
  }
}

payment_model is one of upfront, deposit, or per_session. status is active, cancelled, or completed.

enrollment.cancelled

Sent when an enrollment is cancelled. Booklink first sends a booking.cancelled event for each of the enrollment’s remaining session bookings, then this event. Handle both, or you will see session cancellations without knowing the enrollment behind them ended.

{
  "id": "evt_9LcT4mQx7RpN2VbK",
  "type": "enrollment.cancelled",
  "created": "2026-09-02T08:15:33.441Z",
  "data": {
    "id": "4TbN9mQx7LpR2VcK",
    "cohort_id": "8VcR3mQt5LpN7KxB",
    "client_id": "5pQz8LmT3nRk9WxB",
    "customer_name": "Sipho Dlamini",
    "customer_email": "[email protected]",
    "customer_phone": "+27835557788",
    "variant_id": "2LmQ9xTv4RpN8KcB",
    "payment_model": "upfront",
    "total_amount_cents": 240000,
    "paid_amount_cents": 240000,
    "refunded_amount_cents": 120000,
    "payment_status": "paid",
    "transaction_id": "9KxT4mQp2LvN7RcB",
    "accepted_terms_at": "2026-08-29T13:22:38.114Z",
    "status": "cancelled",
    "created_at": "2026-08-29T13:22:41.882Z"
  }
}

ping

Sent only when you click Test on an endpoint. It is signed exactly like a real event and carries no data.

{
  "id": "evt_4Lm7QxT2kR9pVcNb",
  "type": "ping",
  "created": "2026-08-29T09:10:00.118Z",
  "data": null
}

Field values you can rely on

FieldPossible values
status (booking)pending, confirmed, cancelled
payment_statusunpaid, pending, partial, manual, paid, failed, package
cancelled_byoperator, client, system
conflict_statusnone, flagged, resolved
status (enrollment)active, cancelled, completed
payment_modelupfront, deposit, per_session

New values may be added

Treat these lists as the current set, not a permanent one. Write your handler so an unrecognised status or event type is logged and ignored rather than throwing an error, otherwise a future addition will start failing your deliveries.

Was this article helpful?

Still need help?

Our support team is happy to help you get the most out of Booklink.

Contact support