Developer documentation
Connect BookLnk to your other systems
Webhooks push every booking, payment and cancellation to your CRM, ERP or accounting tool the moment it happens, so nobody re-types bookings by hand.
Overview
You register an HTTPS URL and choose the events you care about. When one fires, BookLnk sends a POST with a JSON body to that URL, authenticated with the credentials you supply, so your receiver needs no BookLnk-specific verification code. Deliveries are queued, so a slow or offline receiver never delays a customer's booking.
Webhooks are available on plans that include them.
Set up a webhook
- Build a public HTTPS endpoint that accepts
POSTwith a JSON body. - Register it from the BookLnk dashboard (Settings), or with the Management API.
- Send a test. BookLnk sends a sample booking to your URL and shows your raw response, so a mistyped secret is caught before a real booking.
- Make sure your endpoint answers
2xxquickly, then you are live.
Authentication
Pick the scheme your receiver already understands. Credentials are optional: with none, no auth header is sent and your URL's secrecy is the only protection.
| auth_type | Header BookLnk sends | Notes |
|---|---|---|
basic | Authorization: Basic base64(client_id:secret_key) | Default. |
bearer | Authorization: Bearer <secret_key> | client_id is not used. |
custom_header | <auth_header_name>: <secret_key> | For example X-Api-Key. auth_header_name is required. |
Every delivery also carries these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
X-BookLnk-Event | The event name, e.g. booking.created. Use it to route without parsing the body. |
X-BookLnk-Delivery-Id | A UUID, identical on every retry of the same delivery. Use it to de-duplicate. |
Authorization / custom header | Your credentials, as configured. Omitted entirely if you registered none. |
Events
Subscribe to any combination. An event you did not select is never sent.
| Event | Sent when |
|---|---|
booking.created | A booking is made, by a customer online or by staff in the dashboard. |
booking.updated | Any change to a booking: date, time, services, staff, guests, notes, or a payment being recorded. |
booking.status_updated | The status moves, e.g. pending → confirmed → arrived → started → completed. |
booking.cancelled | A booking is cancelled. The business’s own reason is included. |
booking.payment | A payment lands on a booking, recorded by staff or confirmed by the online gateway. |
booking.refund | Money is returned to the customer. Never sent as a negative payment. |
Recording a payment also changes the booking, so an endpoint subscribed to both booking.payment and booking.updated receives two deliveries with different event and delivery_id values.
Payload reference
Every delivery shares one envelope. New fields may be added at any time; existing ones are never renamed or removed, so ignore fields you do not recognise.
| Field | Description |
|---|---|
event | Event name. Same value as the X-BookLnk-Event header. |
delivery_id | UUID of this delivery. Stable across retries. |
sent_at | ISO 8601 UTC timestamp of this attempt. |
data.booking | id, code, status (pending, confirmed, arrived, started, completed, cancelled), start/end date (YYYY-MM-DD) and time (HH:mm:ss), created_at, cancellation_reason (null until cancelled). |
data.customer | id, name, email, phone. Null if the booking has no customer. |
data.location | id, name, address of the branch. Null if unavailable. |
data.items[] | One entry per booked service: service_id, service_name, staff_name, number_of_guests, price. |
data.total | amount (after discount), currency, paid (gross), refunded, is_fully_paid, payment_status. Net received = paid − refunded. |
data.payment | Only on booking.payment: transaction_id, transaction_name, amount, currency, payment_method_id/name, is_online_payment, payment_status, paid_at. |
data.refund | Only on booking.refund: amount, online_amount, in_store_amount, currency, total_refunded, payment_status, refunded_at. |
Example payloads
The body your endpoint receives, by event. Amounts are numbers in the booking's currency.
{
"event": "booking.created",
"delivery_id": "8f3c1a52-0b7e-4d0f-9c0a-3e5f6a7b8c9d",
"sent_at": "2026-10-19T09:41:14.231Z",
"data": {
"booking": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"code": "BK-10482",
"status": "confirmed",
"start_date": "2026-11-02",
"start_time": "14:00:00",
"end_date": "2026-11-02",
"end_time": "15:00:00",
"created_at": "2026-10-19T09:41:12.000Z",
"cancellation_reason": null,
"cancellation_reason_id": null
},
"customer": {
"id": 5321,
"name": "Mona Adel",
"email": "[email protected]",
"phone": "+201001234567"
},
"location": {
"id": 12,
"name": "Zamalek Branch",
"address": "12 26th of July St, Cairo"
},
"items": [
{
"service_id": "88",
"service_name": "Deep Tissue Massage",
"staff_name": "Laila",
"number_of_guests": 1,
"price": 600
}
],
"total": {
"amount": 600,
"currency": "EGP",
"paid": 0,
"refunded": 0,
"is_fully_paid": false,
"payment_status": "not paid"
}
}
}{
"event": "booking.cancelled",
"delivery_id": "8f3c1a52-0b7e-4d0f-9c0a-3e5f6a7b8c9d",
"sent_at": "2026-10-19T09:41:14.231Z",
"data": {
"booking": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"code": "BK-10482",
"status": "cancelled",
"start_date": "2026-11-02",
"start_time": "14:00:00",
"end_date": "2026-11-02",
"end_time": "15:00:00",
"created_at": "2026-10-19T09:41:12.000Z",
"cancellation_reason": "Customer requested",
"cancellation_reason_id": null
},
"customer": {
"id": 5321,
"name": "Mona Adel",
"email": "[email protected]",
"phone": "+201001234567"
},
"location": {
"id": 12,
"name": "Zamalek Branch",
"address": "12 26th of July St, Cairo"
},
"items": [
{
"service_id": "88",
"service_name": "Deep Tissue Massage",
"staff_name": "Laila",
"number_of_guests": 1,
"price": 600
}
],
"total": {
"amount": 600,
"currency": "EGP",
"paid": 0,
"refunded": 0,
"is_fully_paid": false,
"payment_status": "not paid"
}
}
}{
"event": "booking.payment",
"delivery_id": "8f3c1a52-0b7e-4d0f-9c0a-3e5f6a7b8c9d",
"sent_at": "2026-10-19T09:41:14.231Z",
"data": {
"payment": {
"transaction_id": 90412,
"transaction_name": "Payment",
"amount": 600,
"currency": "EGP",
"payment_method_id": "3",
"payment_method_name": "Card",
"is_online_payment": false,
"payment_status": "paid",
"paid_at": "2026-10-19T09:41:12.000Z"
},
"booking": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"code": "BK-10482",
"status": "confirmed",
"start_date": "2026-11-02",
"start_time": "14:00:00",
"end_date": "2026-11-02",
"end_time": "15:00:00",
"created_at": "2026-10-19T09:41:12.000Z",
"cancellation_reason": null,
"cancellation_reason_id": null
},
"customer": {
"id": 5321,
"name": "Mona Adel",
"email": "[email protected]",
"phone": "+201001234567"
},
"location": {
"id": 12,
"name": "Zamalek Branch",
"address": "12 26th of July St, Cairo"
},
"items": [
{
"service_id": "88",
"service_name": "Deep Tissue Massage",
"staff_name": "Laila",
"number_of_guests": 1,
"price": 600
}
],
"total": {
"amount": 600,
"currency": "EGP",
"paid": 600,
"refunded": 0,
"is_fully_paid": true,
"payment_status": "paid"
}
}
}{
"event": "booking.refund",
"delivery_id": "8f3c1a52-0b7e-4d0f-9c0a-3e5f6a7b8c9d",
"sent_at": "2026-10-19T09:41:14.231Z",
"data": {
"refund": {
"amount": 200,
"online_amount": 200,
"in_store_amount": 0,
"currency": "EGP",
"total_refunded": 200,
"payment_status": "paid",
"refunded_at": "2026-10-19T11:02:40.000Z"
},
"booking": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"code": "BK-10482",
"status": "confirmed",
"start_date": "2026-11-02",
"start_time": "14:00:00",
"end_date": "2026-11-02",
"end_time": "15:00:00",
"created_at": "2026-10-19T09:41:12.000Z",
"cancellation_reason": null,
"cancellation_reason_id": null
},
"customer": {
"id": 5321,
"name": "Mona Adel",
"email": "[email protected]",
"phone": "+201001234567"
},
"location": {
"id": 12,
"name": "Zamalek Branch",
"address": "12 26th of July St, Cairo"
},
"items": [
{
"service_id": "88",
"service_name": "Deep Tissue Massage",
"staff_name": "Laila",
"number_of_guests": 1,
"price": 600
}
],
"total": {
"amount": 600,
"currency": "EGP",
"paid": 600,
"refunded": 200,
"is_fully_paid": false,
"payment_status": "paid"
}
}
}The test payload has the same structure plus "test": true, and its X-BookLnk-Event header is test.
Responding & retries
Answer with a 2xx within 15 seconds. Acknowledge first, process afterwards.
| Your response | What BookLnk does | Retried |
|---|---|---|
2xx | Success. The failure counter resets. | No |
429, 5xx, timeout, network error | Treated as temporary. | Yes |
401, 403 | Credentials rejected. Update secret_key with PATCH. | No |
Other 4xx | Payload or configuration problem; retrying cannot help. | No |
- Up to 5 attempts per delivery, with exponential backoff starting at 30 seconds.
- Retries reuse the same
X-BookLnk-Delivery-Id. Make your handler idempotent on that value, otherwise a retry after a timeout that actually succeeded creates a duplicate. - After 5 consecutive failed attempts the destination is switched off automatically. Fix the problem, then re-enable it with
PATCH(is_active: true), which also resets the counter. - Redirects are not followed. Register the final URL.
// Express: acknowledge fast, process later, be idempotent
app.post('/booklnk/webhook', express.json(), async (req, res) => {
// 1. Verify the credential you registered with BookLnk
if (req.get('authorization') !== EXPECTED_AUTHORIZATION) return res.sendStatus(401)
// 2. Drop retries of a delivery you already handled
const deliveryId = req.get('x-booklnk-delivery-id')
if (await alreadyProcessed(deliveryId)) return res.sendStatus(200)
// 3. Acknowledge within 15 seconds, then do the real work asynchronously
res.sendStatus(200)
await queue.add(req.get('x-booklnk-event'), req.body)
await markProcessed(deliveryId)
})Management API
Manage destinations programmatically at https://api.booklnk.app, authenticated with a dashboard user's bearer token. Viewing needs Settings view access; changes need Settings edit access.
| Method | Path | Purpose |
|---|---|---|
POST | /webhooks/endpoints | Register a destination (url and events required). |
GET | /webhooks/endpoints | List your destinations. The secret is never returned. |
GET | /webhooks/endpoints/:id | Get one destination. |
PATCH | /webhooks/endpoints/:id | Update any field, including secret_key when you rotate it. Send the current row_version; a stale one returns 409. |
POST | /webhooks/endpoints/:id/test | Send a sample payload now and see your receiver’s raw response. |
GET | /webhooks/endpoints/:id/deliveries | Delivery log. Filter with succeeded=false; limit max 100. |
DELETE | /webhooks/endpoints/:id | Delete a destination. |
curl -X POST https://api.booklnk.app/webhooks/endpoints \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Odoo production",
"url": "https://erp.example.com/api/booklnk/bookings",
"auth_type": "bearer",
"secret_key": "sk_live_a3f9c2e7",
"events": ["booking.created", "booking.cancelled", "booking.payment"]
}'{
"id": "2f6b1d52-8a41-4c1e-b6a4-5d9c0f1e7a33",
"name": "Odoo production",
"url": "https://erp.example.com/api/booklnk/bookings",
"auth_type": "bearer",
"client_id": null,
"auth_header_name": null,
"secret_last_four": "c2e7",
"has_credentials": true,
"events": ["booking.created", "booking.cancelled", "booking.payment"],
"is_active": true,
"last_success_at": null,
"last_failure_at": null,
"consecutive_failures": 0,
"row_version": 1,
"created_at": "2026-10-19T09:30:00.000Z",
"updated_at": "2026-10-19T09:30:00.000Z"
}// POST /webhooks/endpoints/:id/test
{ "delivered": true, "response_status": 200, "response_body": "ok" }
// Unreachable receiver (timeout, DNS, TLS)
{ "delivered": false, "response_status": null, "error_message": "timeout of 15000ms exceeded" }// GET /webhooks/endpoints/:id/deliveries?limit=10&offset=0&succeeded=false
{
"data": [
{
"delivery_id": "8f3c1a52-0b7e-4d0f-9c0a-3e5f6a7b8c9d",
"event_type": "booking.created",
"attempt": 2,
"succeeded": false,
"response_status": 503,
"response_body": "Service Unavailable",
"error_message": null,
"created_at": "2026-10-19T09:41:45.000Z"
}
],
"total": 1,
"limit": 10,
"offset": 0
}Rotated the secret on your side? Send the new secret_key with PATCH; leaving it out keeps the stored one.
Security
- The URL must be https and cannot point to private, loopback or link-local addresses.
- Your secret is stored encrypted and never shown again after you save it.
- Payloads include customer name, email and phone. Share them only with systems that are allowed to hold that data.
Need a field we do not send, or help wiring up a system? Email [email protected].