Skip to main content
Webhooks notify your server when a booking or post-booking operation changes. Jinko sends a signed POST when payment starts, a booking completes or fails, or a cancellation or exchange reaches an outcome.

How it works

  1. Register a public HTTPS endpoint and choose the events you need.
  2. Jinko sends a signed event when the booking changes.
  3. Verify the signature and deduplicate the event.
  4. Return 2xx, then process the event in your application.
When Jinko sends a traveler email at the same moment, the event also includes its content in data.customer_emails. See Send your own customer emails to use that data with your own brand.

Prerequisites

  1. A Jinko account
  2. A public HTTPS endpoint that can receive a POST
The /v1/webhooks management endpoints do not accept tenant keys. Register the endpoint on the Webhooks page in the dashboard when you use a tenant key. The API accepts only legacy person-bound keys with the bare jnk_ prefix.

Tenant subscriptions

A booking made with a tenant key sends events to the subscriptions registered for that tenant. The payload, headers, and signature are the same for every authentication mode. Register the endpoint in the dashboard, select the tenant, and choose the events. Subscriptions are environment-specific, so register them separately in production and sandbox. For sandbox bookings, turn on Sandbox mode in the dashboard before creating the subscription.

Register an endpoint

Open Webhooks in the dashboard, select Add webhook, paste your URL, choose the events, and select Create. Copy the signing secret shown once. You need it to verify deliveries.

Choose events

Seat confirmation: Jinko sends booking.completed and the flight booking confirmation email once the flight is ticketed, even if a seat is still pending. Read booked_ancillaries for each seat’s outcome. See Seat maps. Exactly one of booking.completed, booking.partial and booking.failed fires per trip. They are mutually exclusive. booking.processing reports an earlier moment: the payment is authorized and the booking is being confirmed. The servicing.* events fire after the booking, once for each operation, and only if you subscribe to them.
booking.partial is not a success. If you subscribe to it, treat it as “money moved and the trip is incomplete”, and reconcile per item using the data object below. If you do not subscribe to it, a partially-fulfilled trip sends you nothing at all, and you will only find it by polling get_trip.This is why booking.partial is a distinct event rather than a flag on booking.completed: a consumer that has never heard of it cannot silently mistake a half-booked trip for a confirmed one.
More event types will be added over time. Treat the event field as an open enum and ignore events you don’t handle. No event reports a change the airline makes after ticketing, such as a schedule change or a cancellation. Jinko doesn’t detect those today, and get_booking returns the itinerary as it was booked. Every event in the table is in the WebhookEvent enum of the production OpenAPI contract.

Read the payload

Every event carries these six fields. The example leaves out data, described below:
Those six fields are on every event and never change. status is deprecated: branch on event, which already names the outcome. Events also carry a data object with the facts of the booking or the operation and, when Jinko emails your traveler at that moment, that email: Fetch the full booking with get_booking using the booking_ref.

Customer emails

Every event except booking.partial carries data.customer_emails: one entry for each email Jinko sends, or would send, your traveler at that moment, with what the email shows. Use it to send the email in your own brand. Send your own customer emails explains how, and how to ask Jinko to stop sending its own.
The example shows only some fields of each content. Each template’s content has its own schema in the OpenAPI contract, named after the template: for example FlightTicketingConfirmationEmailContent. A later version can add fields to content and entries to customer_emails, so ignore what you don’t use.
customer_emails contains traveler personal data: names, email addresses, itineraries and ticket numbers. manage_booking_url contains the traveler’s last name. Treat every payload that carries them as personal data, including in logs and storage. Jinko keeps each delivery payload for the delivery log and deletes it 30 days after the delivery’s last attempt or replay.

Completed bookings

booking.completed lists every booked item in data.items, so you can tell the traveler the airline or hotel reference without calling get_booking.
data.items is absent on events sent before contract 0.75.0.

Partial bookings

booking.partial carries data describing what actually happened per item and how much was captured.
The *_money fields use the platform’s one money shape: value is an integer in minor units, divide it by 10 ** decimal_places, and display is the same figure ready to show. See Money & prices. The deprecated captured_amount and captured_total are still sent, in major units, so do not divide those.

Failed bookings

booking.failed carries failure_reason when the failure has a classified cause: price_changed, offer_unavailable, booking_not_created or booking_cancelled_at_provider. failure_message, a customer-facing sentence, is sent for the first two causes only. Both are absent when the failure has no classified cause. customer_emails carries the booking-failed email.

Cancellations

servicing.completed reports a post-booking operation on one booked item, today a cancellation, that succeeded and whose money was settled. It is a separate event from booking.completed because it reports a booking being undone. Its data names the operation and the money, and carries the cancellation email in customer_emails (left out of this example):
refund and penalty use the same money shape as the *_money fields above, with decimal_places at the ISO 4217 digits of the currency. null means the operation carries no such figure, which is different from a figure of zero.

Exchanges and failed operations

servicing.exchange_confirmed, servicing.ticket_issued and servicing.failed each report one operation on one booked item. Their data carries:
The event_id is an opaque string, unique to each occurrence of an event. Its format is not part of the contract: don’t parse it. The four servicing.* events can happen more than once for the same booking, for example servicing.completed for each cancelled item, and each occurrence has its own event_id. Retries and replays reuse it, so your dedupe on X-Jinko-Event-Id covers them.
Each request also carries these headers:

Verify the signature

Compute HMAC-SHA256(secret, "<X-Jinko-Timestamp>.<raw request body>") and compare it, in constant time, to the hex in X-Jinko-Signature (after the sha256= prefix). Use the raw request body: parsing and re-serializing the JSON will change the bytes and break the check.
Reject the request if the signature doesn’t match, or if X-Jinko-Timestamp is outside your tolerance in either direction (the examples use 5 minutes) to guard against replays. Respond 2xx once you’ve accepted the event.

Handle retries and duplicates

  1. Return 2xx as soon as you have stored the event. Process slower work asynchronously.
  2. Jinko retries a timeout or non-2xx response with exponential backoff, up to 8 attempts over about 80 minutes.
  3. Deduplicate retries with X-Jinko-Event-Id. Every attempt for the same business event uses the same value.
  4. After the eighth failed attempt, the delivery becomes exhausted. You can replay it from the delivery log.

Inspect and replay deliveries

Jinko records every attempt to deliver an event.
  1. Dashboard: open Webhooks in the dashboard, select the webhook, then open Deliveries. You can inspect each attempt and replay a delivery.
  2. API with a legacy person-bound key: GET /v1/webhooks/{id}/deliveries lists deliveries. POST /v1/webhooks/{id}/deliveries/{delivery_id}/replay replays one.
A replay sends the same event again: same event_id, same payload, signed with a new X-Jinko-Timestamp. It starts a fresh set of attempts and adds one to the delivery’s replay_count. Replay is accepted for a delivered or exhausted delivery. It is refused with 409 and code delivery_in_progress while the delivery is pending or failed, and with 409 and code payload_purged once the payload is deleted, 30 days after the delivery’s last attempt or replay. Because a replay keeps the event_id, a receiver that deduplicates on X-Jinko-Event-Id treats it as an event it already has, if it processed the first delivery.

Test the endpoint

The dashboard has no test button. With a legacy person-bound key, send a test event through the API:
This delivers a sample event with "livemode": false and booking_ref: "JNK-TEST00", so you can confirm your signature handling end-to-end without a real booking.