View .mdDownload all for AI

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).
  • eventTypes is a list of types, or ["*"] for all (including types added later).
  • Send a test with POST /webhook-endpoints/{id}/test — a ping event 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 simply GET the 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.