Callbacks#
Pass a callbackUrl when you book and we POST the booking's terminal outcome to you, instead of you polling for it.
There is no endpoint registry. The URL is per booking, on the booking request — so different bookings can go to different receivers, and nothing has to be registered in advance.
The events#
| Event | Sent when |
|---|---|
booking.confirmed | The booking is confirmed with the property |
booking.failed | The booking could not be completed |
booking.cancelled | A confirmed booking was cancelled |
booking.amended | A live booking was amended |
The first three are terminal and arrive once per booking. booking.amended is the one that repeats — a booking amended three times produces three of them. Key your handler on the event id, not on the booking id.
The delivery#
A POST with a JSON body and two headers that matter.
X-Elivaas-Event-Id is the delivery's identity. Delivery is at-least-once — a receiver that is slow or briefly down will see the same event twice. Store the id and ignore repeats.
X-Elivaas-Signature carries the timestamp and the HMAC.
occurredAt in the body is what orders events, not arrival time. Two deliveries can arrive out of order; apply the later occurredAt and discard the earlier.
POST /hooks/elivaas HTTP/1.1
Content-Type: application/json
X-Elivaas-Event-Id: evt_9fQx7Kd2m1RtY0
X-Elivaas-Signature: t=1785312000,v1=5257a869e7...{
"event": "booking.confirmed",
"occurredAt": "2026-08-01T09:15:22Z",
"bookingId": "bkg_20Etl54TyD",
"externalBookingId": "your-ref-000123",
"status": "CONFIRMED"
}Verifying the signature#
The signature header is t=<unix-seconds>,v1=<hex>, where v1 is HMAC-SHA256 over <t>.<raw request body>, keyed with your signing secret, lowercase hex.
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t, sig = parts["t"], parts["v1"]
# Reject stale signatures, or a captured callback can be replayed forever.
if abs(time.time() - int(t)) > tolerance:
return False
expected = hmac.new(secret.encode(),
f"{t}.".encode() + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig) # constant time, not ==const crypto = require('crypto');
function verify(rawBody, header, secret, tolerance = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const {t, v1} = parts;
if (Math.abs(Date.now() / 1000 - Number(t)) > tolerance) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.`)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}Two details that are security properties, not style:
- Compare in constant time.
==on a hex string leaks how much of a forged signature was correct. - Enforce a tolerance on
t. Without it, a captured callback replays forever.
Responding#
Return 2xx quickly. Anything else — including a timeout — counts as a failure and is retried.
Do the work asynchronously: acknowledge, then process. A receiver that books ten seconds of work before replying will be retried while it is still working, and will then process the same event twice.
Retries#
A failed delivery is retried 6 times over roughly 4 hours 15 minutes, with growing gaps. After that it is given up on.
That is why the outcome is also readable at GET /v1/bookings/{bookingId} — callbacks are an optimisation, not the source of truth. Reconcile periodically regardless, with GET /v1/bookings?updatedSince=…. A receiver that was down for a morning is a normal event, not an emergency.
Building a receiver that behaves#
Capture the raw body#
Before any JSON middleware touches it.
Verify the signature, then the timestamp#
Reject anything that fails either. Do not log the body of a request that failed verification as if it were real.
Deduplicate on `X-Elivaas-Event-Id`#
At-least-once means you will see repeats. This is expected traffic, not an incident.
Return 2xx immediately, then process#
Queue the work. Reply first.
Order by `occurredAt`, not arrival#
Especially for
booking.amended, which repeats. Apply the newest and discard older ones.Reconcile anyway#
A nightly
updatedSincesweep catches anything the six retries never landed.
Next#
Amendments and Cancellations — the two things that produce further events.