Skip to content

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.

Call this once per credential, using your normal X-API-Key / X-API-Secret authentication:

POST /v1/third-party/webhook/
Content-Type: application/json
X-API-Key: YOUR_API_KEY
X-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.

{
"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_url is a no-op for the secret — your existing webhook_secret keeps working, it does not rotate out from under you.
  • Registering a different webhook_url (or registering for the first time) mints a new webhook_secret, again shown exactly once in that response.

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.
{
"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.

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:

import hashlib
import 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)
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.

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.

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.

  • Endpoint is HTTPS and responds within 10 seconds.
  • Registered once via POST /v1/third-party/webhook/; webhook_secret stored securely.
  • Verifies X-RestroLab-Signature (using X-RestroLab-Timestamp and the raw body) before trusting the payload.
  • Deduplicates on X-RestroLab-Event-Id / event_id.
  • Responds 2xx quickly; defers heavy work to a background job.
  • Has a periodic-polling fallback in case a delivery exhausts all 5 attempts.