Webhooks
Webhooks tell your system the moment something happens — a booking is ticketed, a hold is about to expire, a refund quote is ready — so you do not have to poll.
1. Register an endpoint
In the panel (Developers → Webhooks) or through the API:
curl -X POST https://quickota.com/api/v1/webhook-endpoints \
-H "Authorization: Bearer tam_live_..." -H "Content-Type: application/json" \
-d '{"url":"https://example.com/webhooks/tam","eventTypes":["*"],"description":"Booking sync"}'
The response contains the signing secret (whsec_…). Store it on your server; you need it to verify requests.
- The URL must be public
https://. Private and internal addresses are refused (webhooks.url_not_allowed). eventTypesis a list of types, or["*"]for all (including types added later).- Send a test with
POST /webhook-endpoints/{id}/test— apingevent arrives within seconds.
2. What you receive
POST /webhooks/tam HTTP/1.1
Content-Type: application/json
User-Agent: TAM-Webhooks/1.0
TAM-Signature: t=1733040000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
TAM-Event-Id: 2f9d0c3e-…
TAM-Event-Type: flight.booking.ticketed
TAM-Delivery-Id: 8a1b…
TAM-Delivery-Attempt: 1
{
"id": "2f9d0c3e-5a0e-4a8e-9a57-0f3c6f4f8b11",
"type": "flight.booking.ticketed",
"apiVersion": "v1",
"createdAt": "2026-12-01T08:31:12Z",
"data": {
"object": { "id": "…", "bookingNumber": "FB-…", "agencyReference": "ORDER-1042", "status": "ticketed", "pnr": "X7K2QP", "ticketNumbers": ["…"], "nextActions": [ … ], "…": "…" }
}
}
data.object has exactly the shape the API returns for that object (for bookings: GET /flights/bookings/{id}), captured when the event happened.
3. Event types
| Type | When |
|---|---|
flight.booking.held |
A booking was created; seats are held until deadlines.holdExpiresAt. |
flight.booking.issuing |
Payment reserved; the airline is issuing. Wait — do not issue again. |
flight.booking.ticketed |
Tickets issued (pnr, ticketNumbers). |
flight.booking.failed |
Ticketing failed; any reserved money was returned to your wallet. |
flight.booking.cancelled |
Hold released or expired, or tickets cancelled. |
flight.booking.completed |
The journey is over. |
flight.booking.hold_expiring |
The hold expires soon. Issue now or let it lapse. |
flight.booking.attention_required |
Ticketing takes unusually long; our ticketing desk took over (pendingReason: manual_ticketing_queue). |
flight.booking.schedule_changed |
The airline changed a ticketed flight; see openScheduleChanges. |
flight.booking.refunded |
Money for the booking was refunded to your wallet. |
flight.post_booking_request.updated |
A refund/void/change request changed status (e.g. quoted, completed). data.object is the request. |
ping |
Test event. |
GET /webhook-event-types returns the current list.
Bookings made in your panel are reported too, so you can mirror all your bookings in your own system.
4. Verify the signature
TAM-Signature contains a timestamp t and one or more v1 signatures:
v1 = hex( HMAC_SHA256( key = secret, message = t + "." + rawBody ) )
Accept the request only if one v1 matches and t is within 5 minutes of now. Always use the raw request body — not a re-serialized JSON object.
C#
static bool Verify(string header, string rawBody, string secret)
{
var parts = header.Split(',').Select(p => p.Split('=', 2)).Where(p => p.Length == 2).ToList();
var t = parts.First(p => p[0] == "t")[1];
if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(t)) > 300) return false;
using var hmac = new System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(secret));
var expected = Convert.ToHexString(hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(t + "." + rawBody))).ToLowerInvariant();
return parts.Where(p => p[0] == "v1").Any(p => System.Security.Cryptography.CryptographicOperations.FixedTimeEquals(
System.Text.Encoding.ASCII.GetBytes(p[1]), System.Text.Encoding.ASCII.GetBytes(expected)));
}
Node.js
const crypto = require("crypto");
function verify(header, rawBody, secret) {
const parts = header.split(",").map(p => p.split("="));
const t = parts.find(p => p[0] === "t")?.[1];
if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return parts.filter(p => p[0] === "v1").some(p =>
p[1].length === expected.length && crypto.timingSafeEqual(Buffer.from(p[1]), Buffer.from(expected)));
}
// Express: app.post("/webhooks/tam", express.raw({ type: "application/json" }), (req, res) => { verify(req.get("TAM-Signature"), req.body.toString("utf8"), SECRET) … })
Python
import hmac, hashlib, time
def verify(header: str, raw_body: bytes, secret: str) -> bool:
parts = [p.split("=", 1) for p in header.split(",") if "=" in p]
t = next((v for k, v in parts if k == "t"), None)
if t is None or abs(time.time() - int(t)) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(v, expected) for k, v in parts if k == "v1")
PHP
function verify(string $header, string $rawBody, string $secret): bool {
$t = null; $sigs = [];
foreach (explode(',', $header) as $part) {
[$k, $v] = array_pad(explode('=', $part, 2), 2, null);
if ($k === 't') $t = $v; elseif ($k === 'v1') $sigs[] = $v;
}
if ($t === null || abs(time() - (int)$t) > 300) return false;
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
foreach ($sigs as $s) if (hash_equals($expected, $s)) return true;
return false;
}
// $rawBody = file_get_contents('php://input');
5. Respond quickly, process later
Answer with any 2xx within 10 seconds, then do the work (e.g. in a queue). Anything else — a non-2xx status, a timeout, a redirect — counts as a failure.
6. Retries and failures
A failed delivery is retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h and 24 h (about 3 days in total). If every delivery to an endpoint fails for 3 days, the endpoint is disabled and your agency is e-mailed. Fix your receiver, re-enable it in the panel, and use the delivery log to catch up.
GET /webhook-deliveries?status=failed— what did not arrive, with the last HTTP status and error.POST /webhook-deliveries/{id}/retry— send one again now.GET /webhook-events?objectId={bookingId}— every event of a booking, with the exact payload (kept 30 days).
7. Things to design for
- Duplicates happen (e.g. your 2xx got lost). Deduplicate on the event
id(TAM-Event-Id). - Order is not guaranteed. Compare
createdAt, or simplyGETthe object when you need its latest state. - Webhooks tell you that something changed; the API remains the source of truth. A missed webhook never loses data — the booking is always available with
GET.
8. Rotating the secret
POST /webhook-endpoints/{id}/rotate-secret returns a new secret. For 24 hours every delivery carries two v1 signatures (new and old secret), so you can deploy the new secret without dropping events.