Guides
Webhooks
Three events, every booking type, one signature to check. Register an endpoint and stop polling.
Subscribing
A subscription narrows on two axes.
events picks which of the three you receive, entities picks which record types. Leave either out and you get all of them, which also means a record type added later starts arriving at your endpoint. Pick explicitly if that matters to you.events
- record.created
- record.updated
- record.deleted
entities
- car_rentals
- charters
- cruises
- customers
- events
- hotel_stays
- leads
- operator_invoices
- supplier_bills
- suppliers
- tickets
- tour_bookings
- tour_departures
- transfers
- visa_cases
curl -X POST https://YOUR-CELL.lydira.com/api/v1/webhooks \
-H 'Authorization: Bearer sek_YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://example.com/hooks/lydira",
"events": ["record.created", "record.updated"],
"entities": ["hotel_stays", "tickets"]
}'Broadcasting is not the same permission as importing
These types broadcast and can be read back, but cannot be pushed: a POST to one answers
422 entity_not_writable. Subscribing to tour_departures is also the only way to hear that a departure was cancelled or sold out, because that does not move the status of the seats already sold on it.- charters
- events
- visa_cases
The payload
Every event carries the same four keys.
data is the record in the shape GET /api/v1/imports/{entity}/records/{external_id} returns, including your external_id when the record came in through an import. record.updated fires when a meaningful field moves, not on every recalculation.POST your endpoint
{
"id": "evt_8f14e45fea2c4f339a3b",
"event": "record.updated",
"occurred_at": "2026-03-14T09:30:00Z",
"data": {
"entity": "hotel_stays",
"external_id": "HOTEL-STAY-1001",
"status": "confirmed"
}
}Headers on every delivery
X-Lydira-Event: …
X-Lydira-Delivery: …
X-Lydira-Attempt: …
X-Lydira-Signature: …Verifying the signature
Four steps. Parse
t and v1 out of X-Lydira-Signature, take the SHA-256 hex digest of the whsec_ secret you stored at registration, compute HMAC-SHA256 over . , and compare against v1 in constant time. Reject an old t as well, or a captured delivery can be replayed at you later.verify.js
import crypto from "node:crypto";
const SECRET = process.env.LYDIRA_WEBHOOK_SECRET; // whsec_…
const signingKey = crypto.createHash("sha256").update(SECRET).digest("hex");
export function verify(rawBody, header) {
const parts = Object.fromEntries(
header.split(",").map((pair) => pair.split("=")),
);
// Reject anything older than five minutes: without this, a delivery
// captured once can be replayed at you for ever.
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!parts.t || age > 300) return false;
const expected = crypto
.createHmac("sha256", signingKey)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(parts.v1 ?? ""),
);
}Delivery
- Attempts
- 5, with exponential backoff
- Success
- any 2xx; anything else is retried
- Timeouts
- 10s to connect, 15s to read
- Destinations
- public addresses only; private, loopback and link-local URLs are refused