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

    1. Build a public HTTPS endpoint that accepts POST with a JSON body.
    2. Register it from the BookLnk dashboard (Settings), or with the Management API.
    3. 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.
    4. Make sure your endpoint answers 2xx quickly, 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_typeHeader BookLnk sendsNotes
    basicAuthorization: Basic base64(client_id:secret_key)Default.
    bearerAuthorization: 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:

    HeaderValue
    Content-Typeapplication/json
    X-BookLnk-EventThe event name, e.g. booking.created. Use it to route without parsing the body.
    X-BookLnk-Delivery-IdA UUID, identical on every retry of the same delivery. Use it to de-duplicate.
    Authorization / custom headerYour credentials, as configured. Omitted entirely if you registered none.

    Events

    Subscribe to any combination. An event you did not select is never sent.

    EventSent when
    booking.createdA booking is made, by a customer online or by staff in the dashboard.
    booking.updatedAny change to a booking: date, time, services, staff, guests, notes, or a payment being recorded.
    booking.status_updatedThe status moves, e.g. pending → confirmed → arrived → started → completed.
    booking.cancelledA booking is cancelled. The business’s own reason is included.
    booking.paymentA payment lands on a booking, recorded by staff or confirmed by the online gateway.
    booking.refundMoney 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.

    FieldDescription
    eventEvent name. Same value as the X-BookLnk-Event header.
    delivery_idUUID of this delivery. Stable across retries.
    sent_atISO 8601 UTC timestamp of this attempt.
    data.bookingid, 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.customerid, name, email, phone. Null if the booking has no customer.
    data.locationid, 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.totalamount (after discount), currency, paid (gross), refunded, is_fully_paid, payment_status. Net received = paid − refunded.
    data.paymentOnly on booking.payment: transaction_id, transaction_name, amount, currency, payment_method_id/name, is_online_payment, payment_status, paid_at.
    data.refundOnly 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.

    booking.created (also the shape of booking.updated and booking.status_updated)
    {
      "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"
        }
      }
    }
    booking.cancelled
    {
      "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"
        }
      }
    }
    booking.payment
    {
      "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"
        }
      }
    }
    booking.refund
    {
      "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 responseWhat BookLnk doesRetried
    2xxSuccess. The failure counter resets.No
    429, 5xx, timeout, network errorTreated as temporary.Yes
    401, 403Credentials rejected. Update secret_key with PATCH.No
    Other 4xxPayload 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.
    Minimal receiver
    // 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.

    MethodPathPurpose
    POST/webhooks/endpointsRegister a destination (url and events required).
    GET/webhooks/endpointsList your destinations. The secret is never returned.
    GET/webhooks/endpoints/:idGet one destination.
    PATCH/webhooks/endpoints/:idUpdate any field, including secret_key when you rotate it. Send the current row_version; a stale one returns 409.
    POST/webhooks/endpoints/:id/testSend a sample payload now and see your receiver’s raw response.
    GET/webhooks/endpoints/:id/deliveriesDelivery log. Filter with succeeded=false; limit max 100.
    DELETE/webhooks/endpoints/:idDelete a destination.
    Register 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"]
      }'
    Response (the secret is never returned, only its last four characters)
    {
      "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"
    }
    Test result
    // 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" }
    Delivery log: answers “did you send it?”
    // 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].