Webhooks
Webhooks let RestroLab notify your platform in real time when an order’s status changes, or when a reservation is created, modified, or cancelled — instead of you polling GET /v1/third-party/orders/ or GET /v1/third-party/hotels/reservations/ repeatedly.
One webhook registration is shared by both the Restro and Hotel modules.
Register your endpoint
Section titled “Register your endpoint”Call this once per credential, using your normal X-API-Key / X-API-Secret authentication:
POST /v1/third-party/webhook/Content-Type: application/jsonX-API-Key: YOUR_API_KEYX-API-Secret: YOUR_API_SECRET
{ "webhook_url": "https://your-platform.example.com/webhooks/restrolab"}webhook_url must be HTTPS — RestroLab will not deliver events to a non-HTTPS URL.
Response
Section titled “Response”{ "webhook_url": "https://your-platform.example.com/webhooks/restrolab", "webhook_secret": "whsec_..."}webhook_secret is returned exactly once, on registration — store it immediately. You’ll use it to verify every future delivery.
Calling POST /webhook/ again is safe and does not return an error:
- Re-registering the same
webhook_urlis a no-op for the secret — your existingwebhook_secretkeeps working, it does not rotate out from under you. - Registering a different
webhook_url(or registering for the first time) mints a newwebhook_secret, again shown exactly once in that response.
Event types
Section titled “Event types”RestroLab sends one of the following event types, asynchronously, whenever the corresponding change happens:
| Event | Sent when |
|---|---|
order.status_changed |
An order’s status field changes. |
reservation.created |
A reservation created through this API is confirmed — fired at creation time, not from a later save. |
reservation.modified |
A reservation’s status changes to anything other than cancelled (after creation). |
reservation.cancelled |
A reservation’s status changes to cancelled. |
Payload
Section titled “Payload”{ "event": "order.status_changed", "event_id": "5b6c7d8e-9fa0-4b1c-8d2e-3f4a5b6c7d8e", "occurred_at": "2026-09-22T14:03:11.482Z", "data": { "order_id": "ORD-000123", "external_order_id": "your-own-order-id", "status": "preparing" }}Reservation events use the same envelope, with data containing reservation_id, external_reservation_id, and status instead:
{ "event": "reservation.cancelled", "event_id": "9c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f", "occurred_at": "2026-09-22T14:05:00.100Z", "data": { "reservation_id": "BKG-000456", "external_reservation_id": "your-own-reservation-id", "status": "cancelled" }}| Field | Type | Description |
|---|---|---|
event |
string | One of the four event types above. |
event_id |
uuid | Stable across retries of the same delivery — same value as the X-RestroLab-Event-Id header below. |
occurred_at |
datetime | ISO 8601, when the underlying status change happened. |
data |
object | Minimal — just the id(s) and new status. Re-fetch GET /v1/third-party/orders/{external_order_id}/ or GET /v1/third-party/hotels/reservations/{external_reservation_id}/ for full details if you need more than the status. |
Verifying the signature
Section titled “Verifying the signature”Every delivery includes:
X-RestroLab-Signature: sha256=<hex-encoded HMAC>X-RestroLab-Timestamp: <unix timestamp, seconds>X-RestroLab-Event-Id: <event id>The signature is an HMAC-SHA256 of the string {timestamp}.{raw request body} (the timestamp from X-RestroLab-Timestamp, and the body exactly as sent — compact JSON, no extra whitespace), computed using your webhook_secret. Verify it against the raw bytes of the request body, before parsing it as JSON:
Python
Section titled “Python”import hashlibimport hmac
def verify_signature(timestamp: str, raw_body: bytes, signature_header: str, webhook_secret: str) -> bool: signed_payload = f"{timestamp}.{raw_body.decode()}".encode() expected = hmac.new(webhook_secret.encode(), signed_payload, hashlib.sha256).hexdigest() received = signature_header.removeprefix("sha256=") return hmac.compare_digest(expected, received)JavaScript
Section titled “JavaScript”import crypto from 'node:crypto';
function verifySignature(timestamp, rawBody, signatureHeader, webhookSecret) { const signedPayload = `${timestamp}.${rawBody}`; const expected = crypto.createHmac('sha256', webhookSecret).update(signedPayload).digest('hex'); const received = signatureHeader.replace(/^sha256=/, ''); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));}Always verify the signature before processing a payload. Do not trust the contents of an unverified webhook request.
Idempotency
Section titled “Idempotency”X-RestroLab-Event-Id (also present as event_id in the payload) stays the same across every retry of the same event. Persist ids you’ve already processed and skip duplicates — your handler must be safe to run more than once for the same event.
Retries
Section titled “Retries”Respond with a 2xx status code within 10 seconds — that’s the delivery timeout. Do heavy processing asynchronously after responding, not inside the request handler.
If your endpoint returns a non-2xx status or the request times out, RestroLab retries with exponential backoff with jitter, up to 5 attempts total (1 initial delivery + up to 4 retries). Roughly: each retry waits up to min(300, 2 × 2^attempt) seconds, randomized, capped at 5 minutes — so retries get further apart, up to about 5 minutes between the last ones. After the 5th failed attempt, delivery is marked permanently failed and not retried again — there’s no manual replay exposed on this API, so a webhook endpoint that’s down for an extended period will miss events. Poll GET /v1/third-party/orders/ or GET /v1/third-party/hotels/reservations/ periodically as a fallback if uptime for your endpoint isn’t guaranteed.
Summary checklist
Section titled “Summary checklist”- Endpoint is HTTPS and responds within 10 seconds.
- Registered once via
POST /v1/third-party/webhook/;webhook_secretstored securely. - Verifies
X-RestroLab-Signature(usingX-RestroLab-Timestampand the raw body) before trusting the payload. - Deduplicates on
X-RestroLab-Event-Id/event_id. - Responds
2xxquickly; defers heavy work to a background job. - Has a periodic-polling fallback in case a delivery exhausts all 5 attempts.
