Skip to main content

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 GENERAL URL 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​

typeFires when
flight_booking_pendingThe airline accepted the booking and a PNR exists. Sent for every payment method, including before payment settles.
flight_processing_issue_ticketTicketing was requested from the airline and we are waiting for confirmation.
flight_ticket_issuedThe airline confirmed. Ticket numbers are now populated.
flight_cancel_requestedA cancellation was accepted for processing, before the airline is called.
flight_processing_cancellationThe airline accepted the cancellation; the void or refund is under way.
flight_cancelledThe cancellation completed.
flight_changedThe 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​

HeaderExampleDescription
Content-Typeapplication/json
X-Webhook-Eventflight_ticket_issuedSame value as type in the body. Lets you route before parsing.
X-Webhook-Event-Idfwh_5evvuantmmru13xnSame value as event_id in the body.
X-Webhook-Signaturesha256=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​

Timeout15 seconds
SuccessHTTP 2xx and a JSON body containing "success": true
RetriesEvery 5 minutes for 30 minutes (about 6 attempts)
After thatThe 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": { }
}
FieldTypeDescription
typeStringOne of the seven event types above.
event_idStringUnique per event, identical across retries. Use for deduplication.
occurred_atStringISO 8601 with offset. When the state changed, not when it was delivered.
user_idString | nullHashed customer ID. null for a booking made without a customer account.
bookingObjectSnapshot of the booking at the time of the event.

The booking object​

FieldTypeDescription
booking_numberStringYour reference for the booking.
payment_refString | nullPayment reference.
statusStringBooking status at the time of the event.
previous_statusString | nullStatus before this change. null on flight_booking_pending.
trip_typeStringe.g. One Way, Round Trip.
pnr_codeString | nullAirline record locator. May be comma-joined when the itinerary spans several records.
booked_atString | nullISO 8601.
booking_detail_linkString | nullCustomer-facing booking lookup page.
contactObjectfirst_name, last_name, email, phone_code, phone.
passengersArraySee below.
segmentsArraySee below.
pricingObjectcurrency, sub_total, grand_total.

Passenger

FieldTypeDescription
typeStringadult, child or infant.
first_nameString
last_nameString
ticket_numbersArrayAn array, not a single value — one passenger can hold several tickets on a multi-leg itinerary. Empty until tickets are issued.

Segment

FieldTypeDescription
segment_keyStringjourney-segment index, e.g. 0-1.
marketing_airlineStringAirline display name, not an IATA code.
flight_numberStringe.g. SL3807.
cabin_classString
departure_airport_codeStringIATA code.
departure_airport_nameString
departure_datetimeStringISO 8601.
arrival_airport_codeStringIATA code.
arrival_airport_nameString
arrival_datetimeStringISO 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": [ ]
}
}
FieldTypeDescription
details_availableBooleanWhether we know what changed. See below.
reasonString | nullFree-text reason given by the airline.
old_segmentsArraySegments before the change. Same shape as booking.segments.
new_segmentsArraySegments 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_available is false
  • old_segments and new_segments are both empty
  • booking.segments still 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.

  1. Read the raw body.
  2. Verify X-Webhook-Signature. Reject with 401 if it does not match.
  3. Parse the JSON. If type is not one you handle, return {"success": true} and stop.
  4. If event_id was already processed, return {"success": true} and stop.
  5. 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.