Flight Booking Webhooks
Every other page in this section documents a request you send to us. This one is the reverse: we send an HTTP request to you whenever a flight booking changes state, so your system stays in sync without polling.
- Direction: Onlynx → your server
- Method:
POST - Endpoint: the
GENERALURL registered on your partner account - Body: JSON
:::info Delivered to your existing GENERAL endpoint
Flight events arrive at the same URL that already receives the login, register and
payment_success events. Branch on the payload's type field and ignore any type you do
not recognise — seven new values start arriving once this is enabled for your account.
If your handler errors on an unknown type, we will treat the delivery as failed and retry it
up to six times. See Delivery and retries.
:::
Events
type | Fires when |
|---|---|
flight_booking_pending | The airline accepted the booking and a PNR exists. Sent for every payment method, including before payment settles. |
flight_processing_issue_ticket | Ticketing was requested from the airline and we are waiting for confirmation. |
flight_ticket_issued | The airline confirmed. Ticket numbers are now populated. |
flight_cancel_requested | A cancellation was accepted for processing, before the airline is called. |
flight_processing_cancellation | The airline accepted the cancellation; the void or refund is under way. |
flight_cancelled | The cancellation completed. |
flight_changed | The airline changed the flight schedule. Can fire more than once on the same ticket. |
A booking normally runs pending → processing_issue_ticket → ticket_issued. Cancellation
can begin from either a pending or an issued booking.
:::caution The two processing_* events are optional
Not every airline exposes an intermediate state. Some cancel and issue tickets synchronously, so
a booking can move straight from flight_cancel_requested to flight_cancelled, or from
flight_booking_pending to flight_ticket_issued, with no processing_* event in between.
Never wait for a processing_* event before accepting a terminal one.
:::
Payment events
Two more events — payment_success and payment_failed — arrive on the same endpoint. They are
not flight-specific, so they are documented with the rest of the payment flow:
Payment Webhooks.
The headers, signature scheme, retry behaviour and event_id deduplication described on this
page apply to those events identically.
Headers
| Header | Example | Description |
|---|---|---|
Content-Type | application/json | |
X-Webhook-Event | flight_ticket_issued | Same value as type in the body. Lets you route before parsing. |
X-Webhook-Event-Id | fwh_5evvuantmmru13xn | Same value as event_id in the body. |
X-Webhook-Signature | sha256=80f25af4… | HMAC of the raw body. See below. |
Verifying the signature
Every request is signed with HMAC-SHA256 over the raw request body, keyed on the same
private secret you already use to build your Partner-Access-Token. No new credential is
issued.
expected = "sha256=" + hmac_sha256(raw_body, partner_secret)
valid = constant_time_compare(expected, headers["X-Webhook-Signature"])
:::danger Sign the bytes you received, not a re-serialised object Compute the HMAC over the raw body exactly as it arrived. If you parse the JSON and re-encode it first, key order and whitespace change and the digest will never match.
In PHP read file_get_contents('php://input'); in Express use
express.raw({ type: 'application/json' }) rather than express.json().
:::
Use a constant-time comparison — PHP hash_equals(), Node crypto.timingSafeEqual(). Reject
any request whose signature does not match.
$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, $partnerSecret);
if (! hash_equals($expected, $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '')) {
http_response_code(401);
exit;
}
$payload = json_decode($raw, true);
The header is omitted entirely if your account has no secret on file. Treat a missing signature as a failed verification rather than as permission to skip the check.
Delivery and retries
| Timeout | 15 seconds |
| Success | HTTP 2xx and a JSON body containing "success": true |
| Retries | Every 5 minutes for 30 minutes (about 6 attempts) |
| After that | The event is logged as failed and abandoned — it is not redelivered later |
:::warning A 200 alone is not enough
Returning 200 OK with an empty body counts as a failure and will be retried. Your endpoint
must return a body containing success: true:
{ "success": true }
:::
Idempotency
Because retries exist, you can receive the same event more than once — most often when you
processed it successfully but your response never reached us. Every payload carries an
event_id that stays identical across all retries of the same delivery.
Store processed event_id values and ignore repeats.
Payload
All events share the same envelope.
{
"type": "flight_ticket_issued",
"event_id": "fwh_9f2c41d8e6b14a07",
"occurred_at": "2026-09-24T14:32:05+00:00",
"user_id": "0G7KYr78Nb",
"booking": { }
}
| Field | Type | Description |
|---|---|---|
type | String | One of the seven event types above. |
event_id | String | Unique per event, identical across retries. Use for deduplication. |
occurred_at | String | ISO 8601 with offset. When the state changed, not when it was delivered. |
user_id | String | null | Hashed customer ID. null for a booking made without a customer account. |
booking | Object | Snapshot of the booking at the time of the event. |
The booking object
| Field | Type | Description |
|---|---|---|
booking_number | String | Your reference for the booking. |
payment_ref | String | null | Payment reference. |
status | String | Booking status at the time of the event. |
previous_status | String | null | Status before this change. null on flight_booking_pending. |
trip_type | String | e.g. One Way, Round Trip. |
pnr_code | String | null | Airline record locator. May be comma-joined when the itinerary spans several records. |
booked_at | String | null | ISO 8601. |
booking_detail_link | String | null | Customer-facing booking lookup page. |
contact | Object | first_name, last_name, email, phone_code, phone. |
passengers | Array | See below. |
segments | Array | See below. |
pricing | Object | currency, sub_total, grand_total. |
Passenger
| Field | Type | Description |
|---|---|---|
type | String | adult, child or infant. |
first_name | String | |
last_name | String | |
ticket_numbers | Array | An array, not a single value — one passenger can hold several tickets on a multi-leg itinerary. Empty until tickets are issued. |
Segment
| Field | Type | Description |
|---|---|---|
segment_key | String | journey-segment index, e.g. 0-1. |
marketing_airline | String | Airline display name, not an IATA code. |
flight_number | String | e.g. SL3807. |
cabin_class | String | |
departure_airport_code | String | IATA code. |
departure_airport_name | String | |
departure_datetime | String | ISO 8601. |
arrival_airport_code | String | IATA code. |
arrival_airport_name | String | |
arrival_datetime | String | ISO 8601. |
:::note Phone numbers are strings
phone and phone_code are always sent as strings so a leading zero is never lost.
:::
Sample Payload
{
"type": "flight_ticket_issued",
"event_id": "fwh_9f2c41d8e6b14a07",
"occurred_at": "2026-09-24T14:32:05+00:00",
"user_id": "0G7KYr78Nb",
"booking": {
"booking_number": "UZRQ75DM",
"payment_ref": "MT2609241042XG",
"status": "issued",
"previous_status": "processing_issue_ticket",
"trip_type": "One Way",
"pnr_code": "VJLNVA",
"booked_at": "2026-09-14T04:46:24+00:00",
"booking_detail_link": "https://your-domain.com/bookings/check",
"contact": {
"first_name": "Taryar",
"last_name": "Linn",
"email": "customer@example.com",
"phone_code": "66",
"phone": "27929292"
},
"passengers": [
{
"type": "adult",
"first_name": "TAO LIN",
"last_name": "YANG",
"ticket_numbers": ["0171234567890"]
}
],
"segments": [
{
"segment_key": "0-0",
"marketing_airline": "Thai Lion Air",
"flight_number": "SL3807",
"cabin_class": "Economy",
"departure_airport_code": "CNX",
"departure_airport_name": "Chiang Mai International Airport",
"departure_datetime": "2026-11-28T21:03:00+00:00",
"arrival_airport_code": "URT",
"arrival_airport_name": "Surat Thani Airport",
"arrival_datetime": "2026-11-29T10:17:00+00:00"
}
],
"pricing": {
"currency": "MMK",
"sub_total": 1232569.5,
"grand_total": 1879206
}
}
}
Schedule changes
flight_changed carries an extra change object alongside booking.
{
"type": "flight_changed",
"event_id": "fwh_1a7c93be55d0f284",
"occurred_at": "2026-09-24T09:12:44+00:00",
"user_id": "0G7KYr78Nb",
"booking": { },
"change": {
"details_available": true,
"reason": "Schedule change advised by carrier",
"old_segments": [ ],
"new_segments": [ ]
}
}
| Field | Type | Description |
|---|---|---|
details_available | Boolean | Whether we know what changed. See below. |
reason | String | null | Free-text reason given by the airline. |
old_segments | Array | Segments before the change. Same shape as booking.segments. |
new_segments | Array | Segments after the change. |
:::caution Always check details_available
Some airlines tell us a booking's schedule changed without telling us what it changed to.
In that case:
details_availableisfalseold_segmentsandnew_segmentsare both emptybooking.segmentsstill holds the pre-change flights, not the new ones
Treat details_available: false as "this booking needs re-checking" rather than as a
description of the change, and call Booking Detail to get the current
flights. Do not show the customer the segments from this payload as if they were the new
schedule.
When details_available is true, booking.segments reflects the new schedule and
old_segments / new_segments are populated.
:::
Not included
These fields exist on the booking internally and are deliberately never sent:
- Our margin and payable amounts
- What we paid the airline supplier, and their order reference
- Which airline supplier filled the booking
If your reconciliation needs a figure that is missing, ask us rather than deriving it — the values above are intentionally withheld and will not be added to this payload.
Recommended handler
- Read the raw body.
- Verify
X-Webhook-Signature. Reject with401if it does not match. - Parse the JSON. If
typeis not one you handle, return{"success": true}and stop. - If
event_idwas already processed, return{"success": true}and stop. - Do your work, record the
event_id, return{"success": true}.
Return a non-2xx only when you want the event redelivered — for example when your own
database was briefly unavailable. Anything you return that is not 2xx with success: true
triggers the retry schedule.
Related
- Booking — creating, cancelling and refunding flight bookings.
- Booking Detail — pull the current state of a booking on demand.
- Partner Access Token — where your partner secret comes from.