# QuickOTA Developer API — complete documentation > Generated 2026-10-22. Source: https://quickota.com/developers. Base URL: https://quickota.com/api/v1. Contents: guides, then the endpoint reference, then every error code. ## QuickOTA Developer API The QuickOTA API lets your own software — a website, a mid-office, a CRM, another booking engine — search, book, ticket and service travel through your QuickOTA agency account, exactly as your staff do in the panel. Version 1 covers **flights**. Other products (hotels, CMS, cart, CRM) will be added under the same rules, keys and conventions. ### Base URL ``` https://quickota.com/api/v1 ``` Every path in these docs is relative to it. Keys start with `tam_live_`; bookings are made with the airlines your agency uses and paid from your agency wallet. Opening the base URL in a browser (no key needed) shows the API name and links to these docs — a quick way to check the address is right. To build and test without real tickets, ask your platform for access to its **test server**: it runs the same API with simulated airlines. Use a key created on that server and its own base URL. ### Five-minute tour ```bash # 1. Who am I? curl https://quickota.com/api/v1/account -H "Authorization: Bearer tam_live_..." # 2. Search flights curl -X POST https://quickota.com/api/v1/flights/search \ -H "Authorization: Bearer tam_live_..." -H "Content-Type: application/json" \ -d '{"slices":[{"origin":"IKA","destination":"IST","departureDate":"2026-12-01"}],"passengers":{"adults":1}}' ``` Then follow the [Quick start](https://quickota.com/developers/guides/quick-start.md) to hold and ticket a booking. ### Conventions - **JSON everywhere**, `camelCase` field names, UTF-8. - **Successful responses** are wrapped: ```json { "data": { ... }, "meta": { "requestId": "req_7f3a..." } } ``` Lists add `pagination`: `{ "page": 1, "pageSize": 20, "total": 57, "hasMore": true }`. - **Errors** use real HTTP status codes and a `application/problem+json` body with a stable `code`. See [Errors](https://quickota.com/developers/guides/errors.md). - **Money** is an object with the amount as a string (no floating-point surprises): `{ "amount": "180.00", "currency": "USD" }`. - **Timestamps** are ISO-8601 in UTC and end with `Z` (`2026-12-01T08:30:00Z`). Flight departure/arrival times are also given as local airport time (`departureLocal`). - **Dates** are `yyyy-MM-dd`. - **IDs** are opaque strings. Store them as strings. - **Your reference**: pass `agencyReference` (your order number) when you book; it is echoed on the booking, in lists and in webhooks, and you can search by it. ### Headers you will see | Header | Meaning | |---|---| | `X-Request-Id` | Unique id of the request. Quote it when you contact support. | | `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` | Your rate limit. See [Rate limits](https://quickota.com/developers/guides/rate-limits-and-look-to-book.md). | | `X-Quota-Searches-Remaining` | Searches left today. | | `Retry-After` | Seconds to wait before retrying (on 429 and 503). | | `Idempotent-Replayed` | `true` when a response was replayed for a repeated `Idempotency-Key`. | ### Getting a key Your agency must have the **Developer API** module installed. Then, in your panel, open **Developers → API keys** and choose the permissions (scopes) the key needs. The key is shown **once** — store it in a secret manager, never in a browser or mobile app. | Scope | Allows | |---|---| | `flights:read` | Search, price, fare rules, read bookings and e-tickets | | `flights:book` | Hold, issue and cancel bookings | | `flights:post_booking` | Refunds, voids, changes and schedule-change replies | | `account:read` | Account, balance and usage | | `webhooks:manage` | Webhook endpoints and deliveries | ### Using this documentation with an AI assistant Every page is plain Markdown underneath: - `https://quickota.com/developers/llms.txt` — index of all pages, for AI tools. - `https://quickota.com/developers/llms-full.txt` — **everything in one file** (guides, full endpoint reference and every error code). Paste it into ChatGPT, Claude or your IDE assistant and ask it to write your integration. - `https://quickota.com/developers/openapi.json` — OpenAPI 3 description, for code generators and Postman. - Add `.md` to any guide URL to get its Markdown. ### Versioning The version is in the URL (`/v1`). Within v1 we only make **additive** changes: new endpoints, new optional request fields, new response fields, new enum values and new error codes. Your code must ignore fields it does not know. Breaking changes get a new version and at least six months' notice. See the [Changelog](https://quickota.com/developers/guides/changelog.md). --- ## Quick start — your first ticket in 10 minutes > **Issuing a ticket charges your agency wallet.** Run this guide on your platform's **test server** (same API, simulated airlines) with a key created there, then repeat it on the real one. ### 1. Get a key In your panel open **Developers → API keys → Create key** and keep the default scopes. Copy the key (`tam_live_…`) — it is shown once. ```bash export TAM_URL=https://quickota.com/api/v1 export TAM_KEY=tam_live_... curl $TAM_URL/account -H "Authorization: Bearer $TAM_KEY" ``` ### 2. Search ```bash curl -X POST $TAM_URL/flights/search \ -H "Authorization: Bearer $TAM_KEY" -H "Content-Type: application/json" \ -d '{ "slices": [{ "origin": "IKA", "destination": "IST", "departureDate": "2026-12-01" }], "passengers": { "adults": 1 }, "maxResults": 5 }' ``` Each offer has an `offerId`, an `expiresAt`, the itinerary (`slices` → `segments`), the price you pay (`price.total`), the fare conditions (`fare.refundable`, `fare.baggage`) and `seatsLeft`. ```json { "data": { "offers": [{ "offerId": "off_4Qb3…", "expiresAt": "2026-11-20T10:25:00Z", "price": { "total": { "amount": "180.00", "currency": "USD" } }, "fare": { "brandName": "Economy Light", "cabin": "economy", "refundable": true, "baggage": [ … ] }, "slices": [ … ], "seatsLeft": 9 }], "warnings": [] } } ``` ### 3. (Optional) Re-check the price and read the rules ```bash curl -X POST $TAM_URL/flights/offers/off_4Qb3.../price -H "Authorization: Bearer $TAM_KEY" curl $TAM_URL/flights/offers/off_4Qb3.../fare-rules -H "Authorization: Bearer $TAM_KEY" ``` If `priceChanged` is `true`, show the new price to the traveller before booking. ### 4. Hold the seats ```bash curl -X POST $TAM_URL/flights/bookings \ -H "Authorization: Bearer $TAM_KEY" -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "offerId": "off_4Qb3...", "agencyReference": "ORDER-1042", "passengers": [{ "type": "adult", "gender": "male", "firstName": "ALI", "lastName": "REZAEI", "birthDate": "1985-05-05", "document": { "type": "passport", "number": "P7654321", "issuingCountry": "IR", "expiryDate": "2031-01-01" } }], "contact": { "name": "Ali Rezaei", "email": "ali@example.com", "phone": "+989121111111" } }' ``` The booking comes back `held`, with the deadline to issue and what you can do next: ```json { "data": { "id": "7c1e0a52-…", "bookingNumber": "FB-260-…", "agencyReference": "ORDER-1042", "status": "held", "isFinal": false, "deadlines": { "holdExpiresAt": "2026-11-20T10:40:00Z", "voidUntil": null }, "nextActions": [ { "action": "issue", "method": "POST", "href": "/flights/bookings/7c1e…/issue", "deadline": "2026-11-20T10:40:00Z" }, { "action": "cancel", "method": "POST", "href": "/flights/bookings/7c1e…/cancel", "deadline": "2026-11-20T10:40:00Z" } ], "price": { "amountDue": { "amount": "180.00", "currency": "USD" }, … }, "pnr": "X7K2QP" } } ``` Holding costs nothing. If you do not issue before `holdExpiresAt`, the seats are released. ### 5. Issue (ticket) ```bash curl -X POST $TAM_URL/flights/bookings/7c1e.../issue \ -H "Authorization: Bearer $TAM_KEY" -H "Idempotency-Key: $(uuidgen)" ``` `amountDue` is taken from your wallet and the booking becomes `ticketed` with `ticketNumbers`. Some airlines confirm later: then the status is `issuing` — **wait** for the `flight.booking.ticketed` webhook or poll `GET /flights/bookings/{id}` every `pollAfterSeconds`. Never call issue again with a new key. Details: [Managing booking states](https://quickota.com/developers/guides/managing-booking-states.md). ### 6. Get the e-ticket ```bash curl $TAM_URL/flights/bookings/7c1e.../documents/e-ticket -H "Authorization: Bearer $TAM_KEY" -o ticket.pdf ``` ### Next steps - Read [Managing booking states](https://quickota.com/developers/guides/managing-booking-states.md) — the most important page for a robust integration. - Set up [Webhooks](https://quickota.com/developers/guides/webhooks.md) so you do not have to poll. - Going live: create a key on the real server, use its base URL, and make sure your wallet has funds. --- ## Authentication Every request carries an API key of your agency. Keys are created in your panel under **Developers → API keys** and start with `tam_live_`. The API works on your agency's real data: bookings are made with the airlines your agency uses and paid from your agency wallet, and they appear in your panel's booking list like any other booking. To build and test without real tickets, use your platform's **test server** (same API, simulated airlines) with a key created there. ### Sending the key ```http Authorization: Bearer tam_live_ab12cd34XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ``` Keys are secrets. Call the API **from your server only** — never from a browser, a mobile app or anything a customer can inspect. ### Scopes A key only reaches the endpoints its scopes allow; anything else returns `403 auth.insufficient_scope`. Give each system the smallest set it needs — for example, a price-display widget backend needs only `flights:read`. A scope is only available when the module behind it is installed for your agency (`flights:*` needs the flight module). ### IP allow-list (recommended) When you create or edit a key you can list the IP addresses or CIDR ranges (`203.0.113.10`, `203.0.113.0/24`, IPv6 too) it may be used from. Requests from other addresses get `403 auth.ip_not_allowed`. ### Expiry, rotation and revocation - **Expiry** — optional; after it the key returns `401 auth.key_expired`. - **Rotate** — creates a new key with the same settings. The old key keeps working for **24 hours**, so you can deploy the new one without downtime. - **Revoke** — stops the key immediately (`401 auth.key_revoked`). Do this at once if a key may have leaked. The panel shows when and from which IP each key was last used. ### Checking a key ```bash curl https://quickota.com/api/v1/account -H "Authorization: Bearer tam_live_..." ``` ```json { "data": { "agencyId": "…", "keyId": "…", "scopes": ["flights:read", "flights:book"] }, "meta": { "requestId": "req_…" } } ``` ### Authentication errors | Code | Status | Meaning | |---|---|---| | `auth.missing_key` | 401 | No `Authorization: Bearer` header. | | `auth.invalid_key` | 401 | Unknown or malformed key. | | `auth.key_expired` / `auth.key_revoked` | 401 | Create or rotate a key in the panel. | | `auth.ip_not_allowed` | 403 | Calling IP is not on the key's allow-list. | | `auth.insufficient_scope` | 403 | The key lacks the scope this endpoint needs. | | `account.module_not_installed` | 403 | Your agency does not have the product installed. | | `account.api_suspended` | 403 | API access is suspended — contact support. | --- ## Flight booking flow ``` search ──► price (optional) ──► hold ──► issue ──► ticketed │ │ │ │ │ │ │ └─► issuing ──(webhook/poll)──► ticketed | failed │ │ └─► cancel (free, before holdExpiresAt) │ └─► priceChanged: show the new price first └─► offers expire (expiresAt): search again ``` | Step | Endpoint | Scope | Money | |---|---|---|---| | Search | `POST /flights/search` | `flights:read` | — | | Re-price one offer | `POST /flights/offers/{offerId}/price` | `flights:read` | — | | Fare rules | `GET /flights/offers/{offerId}/fare-rules` | `flights:read` | — | | Hold | `POST /flights/bookings` | `flights:book` | nothing charged | | Issue | `POST /flights/bookings/{id}/issue` | `flights:book` | `amountDue` taken from wallet | | Cancel a hold | `POST /flights/bookings/{id}/cancel` | `flights:book` | nothing charged | | Read | `GET /flights/bookings/{id}`, `GET /flights/bookings` | `flights:read` | — | ### 1. Search - `slices`: 1 for one-way, 2 for a return, up to 6 for multi-city. Each has `origin`, `destination` (IATA airport or city codes) and `departureDate` (`yyyy-MM-dd`, local). - `passengers`: `adults` (1–9), `children` (2–11 years), `infants` (under 2, not more than adults). - Optional: `cabin` (`economy`, `premium_economy`, `business`, `first`), `directOnly`, `preferredAirline`, `nationality`, `domestic`, `currency`, `maxResults` (1–200, default 50). - Offers are sorted cheapest first. Each fare family (brand) of the same flights is its own offer. - An empty `offers` list is a normal result (no availability), not an error. `warnings` tells you when some airlines did not answer. **Offers expire.** Use an `offerId` before its `expiresAt` (usually 15–30 minutes). An expired or unknown offer returns `flights.offer_expired` / `flights.offer_not_found`: search again. Only live searches count towards your [search quotas and look-to-book ratio](https://quickota.com/developers/guides/rate-limits-and-look-to-book.md). Cache results for identical searches. ### 2. Price (recommended before booking) `POST /flights/offers/{offerId}/price` asks the airline for the current price of one offer. If `priceChanged` is `true`, the offer now carries the new price — show it to your customer. Booking then uses the new price. `GET /flights/offers/{offerId}/fare-rules` returns refund/change/no-show conditions (`penalties`) and, when available, the airline's full rule text. Show at least refundability and baggage before the customer pays. ### 3. Hold `POST /flights/bookings` with the `offerId`, the passengers (same counts and types as the search), a contact and optionally your `agencyReference`. **An `Idempotency-Key` header is required.** Passenger rules: - Names in Latin letters exactly as on the passport. `firstName` may include middle names. - `birthDate` is required for children and infants (and recommended for all). - On international trips a passport (`document`) is required, valid beyond the travel date. - The first adult is the lead passenger. Result: `201` with the booking in status `held`. Common errors: | Code | Meaning | What to do | |---|---|---| | `flights.price_changed` (409) | The airline repriced above tolerance. Nothing was booked. | Price or search again, confirm the new price, hold again. | | `flights.fare_not_available` (409) | Sold out since the search. | Search again. | | `flights.offer_already_booked` (409) | You already held this offer (with a different `Idempotency-Key`). | Use the existing booking: `context.bookingId`. | | `flights.passenger_data_invalid` (422) | Names, dates or documents rejected. | Fix the fields in `errors`. | | `flights.duplicate_booking` (409) | Same passenger on the same flight already. | Use or cancel the existing booking. | | `flights.supplier_timeout` / `supplier_unavailable` | Airline did not answer. `retryable: true`. | Retry with the **same** `Idempotency-Key`. | ### 4. Issue `POST /flights/bookings/{id}/issue` with a new `Idempotency-Key`. Before that, you can check `GET /account/balance`. | Outcome | Status | Meaning | |---|---|---| | `200`, `status: ticketed` | Done | `pnr`, `ticketNumbers`, `passengers[].ticketNumber` filled. | | `200`, `status: issuing` | Waiting on the airline | Wait for the webhook or poll — see [Managing booking states](https://quickota.com/developers/guides/managing-booking-states.md). | | `402 account.insufficient_balance` | Not enough money | `context.shortfall` says how much. **The hold is kept**; top up and issue again before `holdExpiresAt`. | | `409 flights.hold_expired` | Too late | Search again. | | `409 request.invalid_state_transition` | Wrong status | `context.currentStatus` and `context.allowedActions` tell you what is possible. | Calling issue on a booking that is already `issuing` or `ticketed` returns the booking (`200`) and never tickets twice. ### 5. Cancel a hold `POST /flights/bookings/{id}/cancel` releases the seats of a `held` booking; nothing is charged. A **ticketed** booking cannot be cancelled this way (`flights.use_post_booking_request`): request a refund or void instead — see [Refunds, changes & schedule changes](https://quickota.com/developers/guides/servicing-and-schedule-changes.md). ### 6. Read and list - `GET /flights/bookings/{id}` — the full booking with `status`, `nextActions`, `deadlines`, passengers, slices, price and open servicing. - `GET /flights/bookings/{id}/events` — the timeline (held, payment reserved, ticketed, cancellations, servicing, schedule changes). - `GET /flights/bookings` — newest first, filters: `status`, `needsAction=true`, `agencyReference`, `pnr`, `createdFrom`/`createdTo`, `departureFrom`/`departureTo`, `page`, `pageSize` (≤ 100). - `GET /flights/bookings/{id}/documents/e-ticket` — PDF, once ticketed. --- ## Managing booking states Airline bookings are not instant or final. Tickets are sometimes confirmed minutes later, holds expire, airlines change schedules, refunds need a quote. This page explains how to keep your system in step with every booking — without guessing. **The golden rule: every booking response tells you what is happening (`status`, `pendingReason`), what you may do next (`nextActions`), until when (`deadlines`), and how long to wait (`pollAfterSeconds`).** Build your logic on those fields rather than on assumptions. ### Statuses | `status` | `isFinal` | Meaning | Typical `nextActions` | |---|---|---|---| | `held` | no | Seats held, nothing paid yet. | `issue`, `cancel` — both until `deadlines.holdExpiresAt` | | `issuing` | no | Money reserved; the airline is issuing tickets. | `wait` | | `ticketed` | no | Tickets issued. Can still change through servicing. | `download_ticket`, `request_void` (until `deadlines.voidUntil`), `request_refund`, `request_change`, `accept_quote`, `reply_schedule_change` | | `completed` | **yes** | The journey is over. | — | | `cancelled` | **yes** | Hold released or expired, or tickets cancelled/refunded. | — | | `failed` | **yes** | Could not be ticketed. **Any money taken was returned to your wallet.** | `search_again` | ``` held ──issue──► issuing ──► ticketed ──► completed │ │ │ │ └─► failed └─(refund/void)─► cancelled └──cancel / hold expired──► cancelled ``` `statusReason` adds detail when there is some, e.g. `hold_expired`. New statuses or reasons may be added in the future; treat unknown values as "not final, check `nextActions`". ### The booking response, annotated ```json { "id": "7c1e0a52-…", "status": "issuing", "isFinal": false, "pendingReason": "supplier_confirmation_pending", "pollAfterSeconds": 30, "nextActions": [ { "action": "wait", "method": null, "href": null, "deadline": null } ], "deadlines": { "holdExpiresAt": null, "voidUntil": null }, "openServicingRequests": [], "openScheduleChanges": [], "lastEventAt": "2026-11-20T10:31:02Z" } ``` | Field | Use | |---|---| | `nextActions[]` | The only actions allowed now: `action`, the HTTP `method` and `href` (relative to the base URL) to call, and the `deadline` after which it is no longer possible. If an action is not in the list, don't offer it to your user. | | `deadlines.holdExpiresAt` | Issue before this, or the seats are released (`flight.booking.hold_expiring` webhook comes shortly before). | | `deadlines.voidUntil` | Until when a ticket can usually be voided without airline penalty (end of the issue day). | | `pendingReason` | Why a non-final booking is waiting (below). | | `pollAfterSeconds` | When polling, wait at least this long before the next `GET`. Null when there is nothing to wait for. | | `lastEventAt` | Timestamp of the latest change — handy to detect changes cheaply. | ### Pending reasons | `pendingReason` | Status | What is happening | What you do | |---|---|---|---| | `supplier_confirmation_pending` | `issuing` | The airline has not confirmed the tickets yet. Usually seconds to a few minutes. | **Wait.** Webhook `flight.booking.ticketed` / `failed`, or poll every `pollAfterSeconds` (30 s, later 120 s). | | `manual_ticketing_queue` | `issuing` | Still not confirmed after 20 minutes; our ticketing desk took over. You also get `flight.booking.attention_required`. | Keep waiting (poll every 300 s). Tell your customer the ticket is being finalised. Contact support if urgent. | | `awaiting_quote_decision` | `ticketed` | A refund/void/change request has a quote waiting for your answer. | `accept_quote` (see `nextActions`) or decline, before the quote's deadline. | | `servicing_in_progress` | `ticketed` | A servicing request is being handled by the desk. | Wait for `flight.post_booking_request.updated`. | | `awaiting_schedule_change_reply` | `ticketed` | The airline changed the schedule and needs acceptance. | `reply_schedule_change` before its deadline, or request a refund/change. | ### Asynchronous ticketing — the rules Issuing is where money moves, so it is where integrations most often go wrong. Follow these rules and you can never double-ticket or lose a booking: 1. **Send `issue` once, with an `Idempotency-Key`.** If you need to retry (timeout, 5xx), retry with the **same** key. 2. **`200` with `status: issuing` is a success.** Payment is reserved and the airline is working on it. Show "ticket being issued" to your customer. 3. **Never call `issue` again with a new key** to "push" it. (If you do, you simply get the current booking back — but it signals a bug in your flow.) 4. **Wait for the result:** - *Recommended:* the `flight.booking.ticketed` or `flight.booking.failed` [webhook](https://quickota.com/developers/guides/webhooks.md). - *Fallback:* poll `GET /flights/bookings/{id}`, waiting `pollAfterSeconds` between calls. - Best: both — webhooks for speed, a slow poll (e.g. every 10 minutes) of `GET /flights/bookings?needsAction=true` as a safety net. 5. **If your `issue` call timed out with no response,** `GET` the booking: `issuing` or `ticketed` means it went through; `held` means it did not — issue again (same key is fine). 6. **`failed` is final and safe:** the reserved money is back in your wallet. Offer your customer another flight. ### Your work queue: `needsAction` `GET /flights/bookings?needsAction=true` lists the bookings that are waiting on you or on the airline — held bookings to issue or cancel before their deadline, bookings still issuing, quotes to decide and schedule changes to answer. Check it regularly (or build a screen for your staff on it); an empty list means nothing is pending. ### Hold expiry - `flight.booking.hold_expiring` is sent shortly before `holdExpiresAt`. - After `holdExpiresAt` the booking shows `statusReason: hold_expired` and `nextActions: [search_again]`; soon after it turns `cancelled` (`flight.booking.cancelled`). - An expired hold cannot be issued (`flights.hold_expired`). Search again. ### After ticketing A ticketed booking can still change: - **Servicing** — refunds, voids, date changes: see [Refunds, changes & schedule changes](https://quickota.com/developers/guides/servicing-and-schedule-changes.md). `openServicingRequests` lists requests in progress. - **Schedule changes** by the airline: `openScheduleChanges`, webhook `flight.booking.schedule_changed`. - **Partial changes** — `passengers[].state` is `active`, `cancelled` (refunded individually) or `moved` (split to another booking). ### The timeline `GET /flights/bookings/{id}/events` returns everything that happened, oldest first — useful for support and for auditing: ```json [ { "type": "booking.held", "at": "2026-11-20T10:25:14Z", "actor": "agency_api", "note": null }, { "type": "ticketing.started", "at": "2026-11-20T10:26:02Z", "actor": "agency_api", "note": "Payment reserved from the wallet; the airline is issuing." }, { "type": "booking.ticketed", "at": "2026-11-20T10:27:40Z", "actor": "system", "note": "PNR X7K2QP" } ] ``` ### Checklist for a robust integration - [ ] Store `id` and your `agencyReference` together before calling `issue`. - [ ] Use `Idempotency-Key` on every write and reuse it on retries. - [ ] Treat `issuing` as success-in-progress; never re-issue. - [ ] Drive your UI from `nextActions` and `deadlines`. - [ ] Handle `failed` (money returned) and `cancelled` as final. - [ ] Subscribe to webhooks **and** run a slow `needsAction` poll as a safety net. - [ ] Test every path on your platform's test server before going live. --- ## Refunds, changes & schedule changes After ticketing, changes go through **servicing requests**: you ask, our ticketing desk gets the airline's conditions and sends you a **quote**, you accept or decline. Nothing is charged or refunded until you accept. ### Request types | `type` | Use when | Typical cost | |---|---|---| | `void` | Cancel on the **day of issue** (before `deadlines.voidUntil`). | Usually free. | | `refund` | Cancel the whole booking or some passengers/slices. | Airline penalty per the fare rules; non-refundable fares refund nothing (or taxes only). | | `no_show_refund` | The passenger did not fly; recover refundable taxes. | Depends on the airline. | | `change_date` | Move to another date (`requestedDate`). | Change penalty + fare difference. | | `other` | Anything else (name correction, special request) — explain in `note`. | Quoted case by case. | ### Flow ``` requested ──► in_review ──► quoted ──accept──► accepted ──► executing ──► completed │ ├──decline──► declined │ └──(expires)─► quote_expired └──► rejected (the desk/airline refused; see history) (any time before accepting) ──withdraw──► withdrawn executing ──► failed (rare; the desk follows up) ``` #### 1. Create a request ```bash curl -X POST $TAM_URL/flights/bookings/7c1e.../post-booking-requests \ -H "Authorization: Bearer $TAM_KEY" -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \ -d '{ "type": "refund", "passengerIndexes": [0], "note": "Customer cancelled" }' ``` - `passengerIndexes` / `sliceIndexes` — from `passengers[].index` and the slice order; empty means all. - One open request per booking at a time (`flights.servicing_request_open`). - Scope: `flights:post_booking`. #### 2. Receive the quote Wait for the `flight.post_booking_request.updated` webhook (or poll `GET …/post-booking-requests/{requestId}`). When `status` is `quoted`: ```json { "id": "5e2d…", "requestNumber": "PBR-…", "type": "refund", "status": "quoted", "quote": { "airlinePenalty": { "amount": "18.00", "currency": "USD" }, "serviceFee": { "amount": "0.00", "currency": "USD" }, "fareDifference": { "amount": "0.00", "currency": "USD" }, "refundAmount": { "amount": "162.00", "currency": "USD" }, "collectAmount": { "amount": "0.00", "currency": "USD" }, "expiresAt": "2026-11-21T10:00:00Z" }, "nextActions": [ { "action": "accept_quote", "method": "POST", "href": "/flights/bookings/7c1e…/post-booking-requests/5e2d…/accept", "deadline": "2026-11-21T10:00:00Z" }, { "action": "decline_quote", "method": "POST", "href": "/flights/bookings/7c1e…/post-booking-requests/5e2d…/decline", "deadline": "2026-11-21T10:00:00Z" }, { "action": "withdraw", … } ] } ``` - `refundAmount` is credited to your wallet, `collectAmount` is charged to it, when you accept. - The booking shows `pendingReason: awaiting_quote_decision` meanwhile. #### 3. Accept or decline ```bash curl -X POST $TAM_URL/flights/bookings/7c1e.../post-booking-requests/5e2d.../accept \ -H "Authorization: Bearer $TAM_KEY" -H "Idempotency-Key: $(uuidgen)" ``` After acceptance the desk executes the change with the airline. On `completed`, `outcome` shows what was refunded or collected, and for a change the `newPnr` / `newBookingId`. The booking updates accordingly: a full refund makes it `cancelled`; a partial one marks the passengers `cancelled` in `passengers[].state`. Decline with `POST …/decline` (optional `{ "note": "…" }`); withdraw a request you no longer need with `POST …/withdraw`. ### Schedule changes Airlines change flight times, numbers or routings after ticketing. When that happens: 1. You receive `flight.booking.schedule_changed`, and the booking lists the notice in `openScheduleChanges`. 2. `GET /flights/bookings/{id}/schedule-changes` shows the original and the new segments, `requiresAcceptance` and `replyDeadline`. 3. Then: - **No acceptance needed** (information only): `POST …/schedule-changes/{id}/acknowledge`. - **Acceptance needed**: inform the passenger, then `POST …/schedule-changes/{id}/accept` before `replyDeadline`. The booking shows `pendingReason: awaiting_schedule_change_reply` until then. - **Passenger refuses the new schedule**: do not accept; create a `refund` or `change_date` request instead — an involuntary change usually waives penalties. ### Errors you may see | Code | Meaning | |---|---| | `flights.use_post_booking_request` | You called `cancel` on a ticketed booking — create a servicing request instead. | | `flights.servicing_request_open` | Another request is still open on this booking. | | `flights.booking_not_serviceable` | The booking's status does not allow this request (e.g. not ticketed). | | `flights.quote_expired` | The quote's deadline passed; create a new request. | | `flights.schedule_change_not_supported` | The airline does not accept replies through the API for this change; contact support. | --- ## Errors Errors use standard HTTP status codes and an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) `application/problem+json` body: ```json { "type": "https://quickota.com/developers/errors/flights.price_changed", "title": "Price changed", "status": 409, "code": "flights.price_changed", "detail": "The airline repriced the fare above the accepted tolerance between search and booking. Nothing was booked.", "suggestion": "Price the offer again (or search again), show the new price to your customer and book only after they accept it.", "retryable": false, "requestId": "req_7f3a2c9e41d0b8a6c5e21f90", "context": { "offerTotal": { "amount": "180.00", "currency": "USD" } } } ``` | Field | Use it for | |---|---| | `code` | **Branch your code on this.** Stable, never renamed. | | `status` | The HTTP status, repeated. | | `title`, `detail` | Human-readable; wording may change. Safe to log, not to parse. | | `suggestion` | What to do next. | | `retryable` | `true` when the same request may succeed later without changes. | | `requestId` | Quote it to support — we can find the exact request. | | `errors` | For `request.validation_failed`: field → messages, e.g. `"passengers[0].birthDate": ["…"]`. | | `context` | Extra data for some codes (shortfall amounts, current status, allowed actions). | | `type` | Link to this code's documentation page. | ### Handling errors — a checklist 1. **4xx with `retryable: false`** — fix the request or the flow. Retrying the same call will fail the same way. 2. **429** — wait `Retry-After` seconds. See [Rate limits](https://quickota.com/developers/guides/rate-limits-and-look-to-book.md). 3. **5xx or `retryable: true`** — retry with exponential backoff (e.g. 1 s, 2 s, 4 s … max 5 tries). For writes, **send the same `Idempotency-Key`** so a retry can never book or ticket twice. See [Idempotency](https://quickota.com/developers/guides/idempotency.md). 4. **Timeout with no response on `issue`** — do **not** call issue with a new key. `GET` the booking: if it is `issuing` or `ticketed`, the first call worked. See [Managing booking states](https://quickota.com/developers/guides/managing-booking-states.md). 5. Log `requestId` for every error. We never put supplier names, stack traces or internal messages in errors; support can see them from the `requestId`. ### Machine-readable catalog - `https://quickota.com/developers/errors.json` — every code with status, title, description, resolution and `retryable`. - `https://quickota.com/developers/errors/{code}` — one page per code. The full list follows. --- ## Idempotency Networks fail. If your `issue` call times out you cannot know whether the ticket was issued. Idempotency makes retrying safe. Endpoints that create something that must never happen twice **require** an `Idempotency-Key` header: - `POST /flights/bookings` (hold) - `POST /flights/bookings/{id}/issue` - `POST /flights/bookings/{id}/post-booking-requests` - `POST /flights/bookings/{id}/post-booking-requests/{requestId}/accept` ```http POST /flights/bookings/7c1e…/issue Authorization: Bearer tam_live_… Idempotency-Key: 5b0c1f7e-8d53-4bd8-a0f4-4c31d0b5e8a2 ``` ### Rules - Use a **new unique value** (a UUID v4 is ideal) for each new operation, and **the same value** when retrying that operation. - Store the key with your order before you call, so a retry after a crash reuses it. - Keys are remembered for **24 hours**, per agency. | Situation | Result | |---|---| | First request | Runs normally. | | Same key, same body, first request finished | The **stored response is replayed** (same status and body) with header `Idempotent-Replayed: true`. Nothing runs again. | | Same key, same body, first request still running | `409 idempotency.in_progress` — wait a second and retry. | | Same key, **different** body | `422 idempotency.key_reused` — you reused a key for another operation. | | Header missing | `400 idempotency.key_required`. | ### Extra protection on issue Even with a *new* key, calling `issue` on a booking that is already `issuing` or `ticketed` does **not** start a second ticketing attempt: it returns the booking's current state with `200`. Double ticketing is impossible through the API. --- ## Rate limits, quotas & look-to-book Every flight search costs real supplier capacity. The limits below keep the platform fast for everyone. Your current limits and usage are always visible in the panel (**Developers → API usage**) and through `GET /account/usage`. ### 1. Rate limit (per key) | | Limit | |---|---| | Requests per minute | 120 (default) | | Burst | 40 requests at once | Each response carries: ```http X-RateLimit-Limit: 120 X-RateLimit-Remaining: 37 X-RateLimit-Reset: 1 ``` Above the limit you get `429 rate_limit.exceeded` with `Retry-After` (seconds). Wait that long, then continue. Spread traffic evenly instead of firing bursts. ### 2. Search quotas (per agency, live only) | | Default | |---|---| | Searches per day (UTC) | 5,000 | | Searches per month | 100,000 | `X-Quota-Searches-Remaining` shows what is left today. When a quota is used up, searches return `429 quota.searches_daily_exceeded` (or `…_monthly_exceeded`) with `Retry-After` until the next day/month. **Pricing, booking, issuing and reading are never blocked by quotas.** ### 3. Look-to-book ratio (live only) **Look-to-book = live searches ÷ tickets issued** over a rolling window (30 days by default). Integrations that search heavily but rarely book are expensive for everyone, so the ratio is monitored: | Your ratio | What happens | |---|---| | Below 80% of the maximum | Nothing. | | Above 80% of the maximum | Warning e-mail to your agency (at most once a week). | | Above the maximum (default 500 : 1) | The platform may slow down your searches, pause them, or suspend API access, as configured for your account. | Notes: - Only live **searches** count as "looks". Pricing an offer or reading bookings does not. - Tickets issued through the panel and your website count as bookings too, unless your agreement says otherwise. - The ratio is judged only after at least 1,000 searches in the window. - Throttling and pausing lift automatically as soon as the ratio is back under the maximum. A suspension is lifted by the QuickOTA team. - When searching is slowed or paused you get `429 quota.look_to_book_exceeded`; holding, issuing and servicing keep working, so existing customers are not affected. #### How to keep a healthy ratio - **Cache search results** for a few minutes and reuse them for the same route and date. - Do not pre-fetch prices for every route "just in case"; search when a customer asks. - Use `maxResults` to fetch only what you display. - Use the **price** endpoint (not a new search) to re-check one offer before booking. Limits can be adjusted per agency. Talk to us if your business needs different numbers. --- ## 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: ```bash 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 ```http 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 ``` ```json { "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#** ```csharp 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** ```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** ```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** ```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. --- ## Changelog We only make additive changes within `v1`. Your integration must ignore unknown fields and enum values. ### 2026-09 — Sandbox environment removed - The separate sandbox (its own base URL, `tam_test_` keys, virtual balance and simulated ticketing desk) is gone. There is one base URL and keys start with `tam_live_`. - Test integrations on your platform's test server instead: the same API with simulated airlines. - Responses no longer carry `meta.livemode` or the `TAM-Environment` header; webhook payloads no longer carry `livemode`; `GET /account` no longer returns `environment`. ### 2026-09 — v1 released - Flights: search, price, fare rules, hold, issue, cancel, bookings list with `needsAction`, booking timeline, e-ticket PDF. - Flights servicing: refund, void, no-show refund and date-change requests with quotes; airline schedule changes. - Booking states designed for integrators: `status`, `isFinal`, `pendingReason`, `nextActions`, `deadlines`, `pollAfterSeconds`. - Idempotency keys on all booking writes. - Rate limits, search quotas and look-to-book monitoring. - Signed webhooks with retries and a delivery log. - Error catalog with a page per code; `llms.txt`, `llms-full.txt` and OpenAPI for AI tools and code generators. ### Coming next - Ancillaries (extra bags, seats) and fare-brand upsell. - Reference data (airports, airlines, countries) for autocomplete. - Hotels and other QuickOTA products under the same API. --- ## API reference Base URL: `https://quickota.com/api/v1`. Paths below are relative to the base URL. Every request needs `Authorization: Bearer `. Successful bodies are `{ "data": …, "meta": { "requestId", "livemode" } }`; errors are problem+json with a `code`. ### Flight bookings #### `POST /flights/bookings` Holds an offer: the airline reserves the seats and the booking becomes `held` until `deadlines.holdExpiresAt`. Nothing is charged yet. Requires an `Idempotency-Key`; retry with the same key after a timeout to get the same booking back. **Scope:** `flights:book` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `Idempotency-Key` | header | string | yes | Unique value (UUID) per operation; send the same value when retrying. See the Idempotency guide. | Request body: ```json { "offerId": "string", "passengers": [ { "type": "string", "gender": "string", "firstName": "string", "lastName": "string", "birthDate": "2026-12-01", "nationality": "string", "document": { "type": "string", "number": "string", "issuingCountry": "string", "expiryDate": "2026-12-01", "issueDate": "2026-12-01" } } ], "contact": { "name": "string", "email": "ali@example.com", "phone": "string" }, "agencyReference": "string" } ``` | Field | Type | Description | |---|---|---| | `passengers` *(required)* | PassengerV1[] | Passengers in the same counts and types as the search. The first adult is the lead passenger. | | `agencyReference` | string | Your own reference (e.g. your order number), up to 64 characters. Echoed back everywhere and searchable. | Response `201`: ```json { "data": { "id": "string", "bookingNumber": "string", "agencyReference": "string", "status": "string", "isFinal": true, "pendingReason": "string", "statusReason": "string", "nextActions": [ { "action": "string", "method": "string", "href": "string", "deadline": "2026-12-01T08:30:00Z" } ], "deadlines": { "holdExpiresAt": "2026-12-01T08:30:00Z", "voidUntil": "2026-12-01T08:30:00Z" }, "pollAfterSeconds": 0, "pnr": "string", "ticketNumbers": [ "string" ], "price": { "amountDue": { "amount": "string", "currency": "string" }, "suggestedRetail": { "amount": "string", "currency": "string" }, "commission": { "amount": "string", "currency": "string" }, "cancellationFee": { "amount": "string", "currency": "string" }, "serviceFees": [ { "type": "string", "name": "string", "perPassenger": {}, "total": {} } ] }, "slices": [ { "origin": "string", "destination": "string", "departureAt": "2026-12-01T08:30:00Z", "arrivalAt": "2026-12-01T08:30:00Z", "stops": 0, "durationMinutes": 0, "segments": [ { "marketingCarrier": "string", "flightNumber": "string", "operatingCarrier": "string", "origin": "string", "destination": "string", "departureAt": "2026-12-01T08:30:00Z", "arrivalAt": "2026-12-01T08:30:00Z", "cabin": "string", "bookingClass": "string" } ] } ], "passengers": [ { "index": 0, "type": "string", "firstName": "string", "lastName": "string", "birthDate": "2026-12-01", "ticketNumber": "string", "state": "string" } ], "contact": { "name": "string", "email": "ali@example.com", "phone": "string" }, "openServicingRequests": [ { "id": "string", "requestNumber": "string", "type": "string", "status": "string", "quoteExpiresAt": "2026-12-01T08:30:00Z" } ], "openScheduleChanges": [ { "id": "string", "requiresAcceptance": true, "receivedAt": "2026-12-01T08:30:00Z", "replyDeadline": "2026-12-01T08:30:00Z" } ], "createdAt": "2026-12-01T08:30:00Z", "ticketedAt": "2026-12-01T08:30:00Z", "cancelledAt": "2026-12-01T08:30:00Z", "lastEventAt": "2026-12-01T08:30:00Z" }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `GET /flights/bookings` Lists bookings of this environment, newest first. `needsAction=true` returns only bookings waiting on you: held (issue before the deadline), issuing, a servicing quote to decide, or a schedule change to answer. **Scope:** `flights:read` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `status` | query | string | no | | | `needsAction` | query | boolean | no | | | `agencyReference` | query | string | no | | | `pnr` | query | string | no | | | `createdFrom` | query | string (date-time) | no | | | `createdTo` | query | string (date-time) | no | | | `departureFrom` | query | string (date-time) | no | | | `departureTo` | query | string (date-time) | no | | | `page` | query | integer (int32) | no | | | `pageSize` | query | integer (int32) | no | | Response `200`: ```json { "data": [ { "id": "string", "bookingNumber": "string", "agencyReference": "string", "status": "string", "isFinal": true, "pnr": "string", "origin": "string", "destination": "string", "departureAt": "2026-12-01T08:30:00Z", "passengerCount": 0, "leadPassenger": "string", "amountDue": { "amount": "string", "currency": "string" }, "holdExpiresAt": "2026-12-01T08:30:00Z", "createdAt": "2026-12-01T08:30:00Z" } ], "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `GET /flights/bookings/{id}` The booking with its status, next actions, deadlines, passengers, tickets and open servicing. **Scope:** `flights:read` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `id` | path | string (uuid) | yes | | Response `200`: ```json { "data": { "id": "string", "bookingNumber": "string", "agencyReference": "string", "status": "string", "isFinal": true, "pendingReason": "string", "statusReason": "string", "nextActions": [ { "action": "string", "method": "string", "href": "string", "deadline": "2026-12-01T08:30:00Z" } ], "deadlines": { "holdExpiresAt": "2026-12-01T08:30:00Z", "voidUntil": "2026-12-01T08:30:00Z" }, "pollAfterSeconds": 0, "pnr": "string", "ticketNumbers": [ "string" ], "price": { "amountDue": { "amount": "string", "currency": "string" }, "suggestedRetail": { "amount": "string", "currency": "string" }, "commission": { "amount": "string", "currency": "string" }, "cancellationFee": { "amount": "string", "currency": "string" }, "serviceFees": [ { "type": "string", "name": "string", "perPassenger": {}, "total": {} } ] }, "slices": [ { "origin": "string", "destination": "string", "departureAt": "2026-12-01T08:30:00Z", "arrivalAt": "2026-12-01T08:30:00Z", "stops": 0, "durationMinutes": 0, "segments": [ { "marketingCarrier": "string", "flightNumber": "string", "operatingCarrier": "string", "origin": "string", "destination": "string", "departureAt": "2026-12-01T08:30:00Z", "arrivalAt": "2026-12-01T08:30:00Z", "cabin": "string", "bookingClass": "string" } ] } ], "passengers": [ { "index": 0, "type": "string", "firstName": "string", "lastName": "string", "birthDate": "2026-12-01", "ticketNumber": "string", "state": "string" } ], "contact": { "name": "string", "email": "ali@example.com", "phone": "string" }, "openServicingRequests": [ { "id": "string", "requestNumber": "string", "type": "string", "status": "string", "quoteExpiresAt": "2026-12-01T08:30:00Z" } ], "openScheduleChanges": [ { "id": "string", "requiresAcceptance": true, "receivedAt": "2026-12-01T08:30:00Z", "replyDeadline": "2026-12-01T08:30:00Z" } ], "createdAt": "2026-12-01T08:30:00Z", "ticketedAt": "2026-12-01T08:30:00Z", "cancelledAt": "2026-12-01T08:30:00Z", "lastEventAt": "2026-12-01T08:30:00Z" }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `POST /flights/bookings/{id}/cancel` Cancels a booking that has not been issued: the seats are released and nothing is charged. Ticketed bookings are refunded or voided through a post-booking request instead. **Scope:** `flights:book` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `id` | path | string (uuid) | yes | | | `Idempotency-Key` | header | string | no | Unique value (UUID) per operation; send the same value when retrying. See the Idempotency guide. | Response `200`: ```json { "data": { "id": "string", "bookingNumber": "string", "agencyReference": "string", "status": "string", "isFinal": true, "pendingReason": "string", "statusReason": "string", "nextActions": [ { "action": "string", "method": "string", "href": "string", "deadline": "2026-12-01T08:30:00Z" } ], "deadlines": { "holdExpiresAt": "2026-12-01T08:30:00Z", "voidUntil": "2026-12-01T08:30:00Z" }, "pollAfterSeconds": 0, "pnr": "string", "ticketNumbers": [ "string" ], "price": { "amountDue": { "amount": "string", "currency": "string" }, "suggestedRetail": { "amount": "string", "currency": "string" }, "commission": { "amount": "string", "currency": "string" }, "cancellationFee": { "amount": "string", "currency": "string" }, "serviceFees": [ { "type": "string", "name": "string", "perPassenger": {}, "total": {} } ] }, "slices": [ { "origin": "string", "destination": "string", "departureAt": "2026-12-01T08:30:00Z", "arrivalAt": "2026-12-01T08:30:00Z", "stops": 0, "durationMinutes": 0, "segments": [ { "marketingCarrier": "string", "flightNumber": "string", "operatingCarrier": "string", "origin": "string", "destination": "string", "departureAt": "2026-12-01T08:30:00Z", "arrivalAt": "2026-12-01T08:30:00Z", "cabin": "string", "bookingClass": "string" } ] } ], "passengers": [ { "index": 0, "type": "string", "firstName": "string", "lastName": "string", "birthDate": "2026-12-01", "ticketNumber": "string", "state": "string" } ], "contact": { "name": "string", "email": "ali@example.com", "phone": "string" }, "openServicingRequests": [ { "id": "string", "requestNumber": "string", "type": "string", "status": "string", "quoteExpiresAt": "2026-12-01T08:30:00Z" } ], "openScheduleChanges": [ { "id": "string", "requiresAcceptance": true, "receivedAt": "2026-12-01T08:30:00Z", "replyDeadline": "2026-12-01T08:30:00Z" } ], "createdAt": "2026-12-01T08:30:00Z", "ticketedAt": "2026-12-01T08:30:00Z", "cancelledAt": "2026-12-01T08:30:00Z", "lastEventAt": "2026-12-01T08:30:00Z" }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `GET /flights/bookings/{id}/documents/e-ticket` The e-ticket (itinerary receipt) PDF, once the booking is ticketed. **Scope:** `flights:read` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `id` | path | string (uuid) | yes | | Response `200`: OK #### `GET /flights/bookings/{id}/events` Everything that happened to the booking, oldest first. **Scope:** `flights:read` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `id` | path | string (uuid) | yes | | Response `200`: ```json { "data": [ { "type": "string", "at": "2026-12-01T08:30:00Z", "actor": "string", "note": "string" } ], "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `POST /flights/bookings/{id}/issue` Issues a held booking: the amount due is taken from your wallet and the airline issues the tickets. The booking becomes `ticketed`, or `issuing` when the airline confirms later — then wait for the webhook or poll; never call issue again. Calling issue on a booking that is already issuing or ticketed just returns it. **Scope:** `flights:book` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `id` | path | string (uuid) | yes | | | `Idempotency-Key` | header | string | yes | Unique value (UUID) per operation; send the same value when retrying. See the Idempotency guide. | Response `200`: ```json { "data": { "id": "string", "bookingNumber": "string", "agencyReference": "string", "status": "string", "isFinal": true, "pendingReason": "string", "statusReason": "string", "nextActions": [ { "action": "string", "method": "string", "href": "string", "deadline": "2026-12-01T08:30:00Z" } ], "deadlines": { "holdExpiresAt": "2026-12-01T08:30:00Z", "voidUntil": "2026-12-01T08:30:00Z" }, "pollAfterSeconds": 0, "pnr": "string", "ticketNumbers": [ "string" ], "price": { "amountDue": { "amount": "string", "currency": "string" }, "suggestedRetail": { "amount": "string", "currency": "string" }, "commission": { "amount": "string", "currency": "string" }, "cancellationFee": { "amount": "string", "currency": "string" }, "serviceFees": [ { "type": "string", "name": "string", "perPassenger": {}, "total": {} } ] }, "slices": [ { "origin": "string", "destination": "string", "departureAt": "2026-12-01T08:30:00Z", "arrivalAt": "2026-12-01T08:30:00Z", "stops": 0, "durationMinutes": 0, "segments": [ { "marketingCarrier": "string", "flightNumber": "string", "operatingCarrier": "string", "origin": "string", "destination": "string", "departureAt": "2026-12-01T08:30:00Z", "arrivalAt": "2026-12-01T08:30:00Z", "cabin": "string", "bookingClass": "string" } ] } ], "passengers": [ { "index": 0, "type": "string", "firstName": "string", "lastName": "string", "birthDate": "2026-12-01", "ticketNumber": "string", "state": "string" } ], "contact": { "name": "string", "email": "ali@example.com", "phone": "string" }, "openServicingRequests": [ { "id": "string", "requestNumber": "string", "type": "string", "status": "string", "quoteExpiresAt": "2026-12-01T08:30:00Z" } ], "openScheduleChanges": [ { "id": "string", "requiresAcceptance": true, "receivedAt": "2026-12-01T08:30:00Z", "replyDeadline": "2026-12-01T08:30:00Z" } ], "createdAt": "2026-12-01T08:30:00Z", "ticketedAt": "2026-12-01T08:30:00Z", "cancelledAt": "2026-12-01T08:30:00Z", "lastEventAt": "2026-12-01T08:30:00Z" }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` ### Flight servicing #### `POST /flights/bookings/{bookingId}/post-booking-requests` Asks for a refund, void, no-show refund or date change. The request is quoted by the ticketing desk; follow its status (or the `flight.post_booking_request.updated` webhook) and accept or decline the quote before it expires. **Scope:** `flights:post_booking` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `bookingId` | path | string (uuid) | yes | | | `Idempotency-Key` | header | string | yes | Unique value (UUID) per operation; send the same value when retrying. See the Idempotency guide. | Request body: ```json { "type": "string", "passengerIndexes": [ 0 ], "sliceIndexes": [ 0 ], "requestedDate": "2026-12-01", "note": "string" } ``` | Field | Type | Description | |---|---|---| | `type` *(required)* | string | `refund`, `void`, `change_date`, `no_show_refund` or `other`. | | `passengerIndexes` | integer (int32)[] | Indexes of the passengers concerned (from `passengers[].index`). Empty = all active passengers. | | `sliceIndexes` | integer (int32)[] | Indexes of the slices concerned, for a partial refund or change. Empty = all. | | `requestedDate` | string (date) | For `change_date`: the new travel date wanted. | Response `201`: ```json { "data": { "id": "string", "requestNumber": "string", "bookingId": "string", "type": "string", "status": "string", "isFinal": true, "nextActions": [ { "action": "string", "method": "string", "href": "string", "deadline": "2026-12-01T08:30:00Z" } ], "passengerIndexes": [ 0 ], "sliceIndexes": [ 0 ], "quote": { "airlinePenalty": { "amount": "string", "currency": "string" }, "serviceFee": { "amount": "string", "currency": "string" }, "fareDifference": { "amount": "string", "currency": "string" }, "refundAmount": { "amount": "string", "currency": "string" }, "collectAmount": { "amount": "string", "currency": "string" }, "note": "string", "quotedAt": "2026-12-01T08:30:00Z", "expiresAt": "2026-12-01T08:30:00Z" }, "outcome": { "refunded": { "amount": "string", "currency": "string" }, "collected": { "amount": "string", "currency": "string" }, "newPnr": "string", "newBookingId": "string", "executedAt": "2026-12-01T08:30:00Z" }, "createdAt": "2026-12-01T08:30:00Z", "history": [ { "type": "string", "at": "2026-12-01T08:30:00Z", "actor": "string", "note": "string" } ] }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `GET /flights/bookings/{bookingId}/post-booking-requests` All post-booking requests of the booking, newest first. **Scope:** `flights:post_booking` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `bookingId` | path | string (uuid) | yes | | Response `200`: ```json { "data": [ { "id": "string", "requestNumber": "string", "bookingId": "string", "type": "string", "status": "string", "isFinal": true, "nextActions": [ { "action": "string", "method": "string", "href": "string", "deadline": "2026-12-01T08:30:00Z" } ], "passengerIndexes": [ 0 ], "sliceIndexes": [ 0 ], "quote": { "airlinePenalty": { "amount": "string", "currency": "string" }, "serviceFee": { "amount": "string", "currency": "string" }, "fareDifference": { "amount": "string", "currency": "string" }, "refundAmount": { "amount": "string", "currency": "string" }, "collectAmount": { "amount": "string", "currency": "string" }, "note": "string", "quotedAt": "2026-12-01T08:30:00Z", "expiresAt": "2026-12-01T08:30:00Z" }, "outcome": { "refunded": { "amount": "string", "currency": "string" }, "collected": { "amount": "string", "currency": "string" }, "newPnr": "string", "newBookingId": "string", "executedAt": "2026-12-01T08:30:00Z" }, "createdAt": "2026-12-01T08:30:00Z", "history": [ { "type": "string", "at": "2026-12-01T08:30:00Z", "actor": "string", "note": "string" } ] } ], "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `GET /flights/bookings/{bookingId}/post-booking-requests/{requestId}` **Scope:** `flights:post_booking` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `bookingId` | path | string (uuid) | yes | | | `requestId` | path | string (uuid) | yes | | Response `200`: ```json { "data": { "id": "string", "requestNumber": "string", "bookingId": "string", "type": "string", "status": "string", "isFinal": true, "nextActions": [ { "action": "string", "method": "string", "href": "string", "deadline": "2026-12-01T08:30:00Z" } ], "passengerIndexes": [ 0 ], "sliceIndexes": [ 0 ], "quote": { "airlinePenalty": { "amount": "string", "currency": "string" }, "serviceFee": { "amount": "string", "currency": "string" }, "fareDifference": { "amount": "string", "currency": "string" }, "refundAmount": { "amount": "string", "currency": "string" }, "collectAmount": { "amount": "string", "currency": "string" }, "note": "string", "quotedAt": "2026-12-01T08:30:00Z", "expiresAt": "2026-12-01T08:30:00Z" }, "outcome": { "refunded": { "amount": "string", "currency": "string" }, "collected": { "amount": "string", "currency": "string" }, "newPnr": "string", "newBookingId": "string", "executedAt": "2026-12-01T08:30:00Z" }, "createdAt": "2026-12-01T08:30:00Z", "history": [ { "type": "string", "at": "2026-12-01T08:30:00Z", "actor": "string", "note": "string" } ] }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `POST /flights/bookings/{bookingId}/post-booking-requests/{requestId}/accept` Accepts the quote: a refund is credited to your wallet, an amount to collect is charged to it, and the desk executes the change with the airline. **Scope:** `flights:post_booking` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `bookingId` | path | string (uuid) | yes | | | `requestId` | path | string (uuid) | yes | | | `Idempotency-Key` | header | string | yes | Unique value (UUID) per operation; send the same value when retrying. See the Idempotency guide. | Response `200`: ```json { "data": { "id": "string", "requestNumber": "string", "bookingId": "string", "type": "string", "status": "string", "isFinal": true, "nextActions": [ { "action": "string", "method": "string", "href": "string", "deadline": "2026-12-01T08:30:00Z" } ], "passengerIndexes": [ 0 ], "sliceIndexes": [ 0 ], "quote": { "airlinePenalty": { "amount": "string", "currency": "string" }, "serviceFee": { "amount": "string", "currency": "string" }, "fareDifference": { "amount": "string", "currency": "string" }, "refundAmount": { "amount": "string", "currency": "string" }, "collectAmount": { "amount": "string", "currency": "string" }, "note": "string", "quotedAt": "2026-12-01T08:30:00Z", "expiresAt": "2026-12-01T08:30:00Z" }, "outcome": { "refunded": { "amount": "string", "currency": "string" }, "collected": { "amount": "string", "currency": "string" }, "newPnr": "string", "newBookingId": "string", "executedAt": "2026-12-01T08:30:00Z" }, "createdAt": "2026-12-01T08:30:00Z", "history": [ { "type": "string", "at": "2026-12-01T08:30:00Z", "actor": "string", "note": "string" } ] }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `POST /flights/bookings/{bookingId}/post-booking-requests/{requestId}/decline` **Scope:** `flights:post_booking` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `bookingId` | path | string (uuid) | yes | | | `requestId` | path | string (uuid) | yes | | Request body: ```json { "note": "string" } ``` Response `200`: ```json { "data": { "id": "string", "requestNumber": "string", "bookingId": "string", "type": "string", "status": "string", "isFinal": true, "nextActions": [ { "action": "string", "method": "string", "href": "string", "deadline": "2026-12-01T08:30:00Z" } ], "passengerIndexes": [ 0 ], "sliceIndexes": [ 0 ], "quote": { "airlinePenalty": { "amount": "string", "currency": "string" }, "serviceFee": { "amount": "string", "currency": "string" }, "fareDifference": { "amount": "string", "currency": "string" }, "refundAmount": { "amount": "string", "currency": "string" }, "collectAmount": { "amount": "string", "currency": "string" }, "note": "string", "quotedAt": "2026-12-01T08:30:00Z", "expiresAt": "2026-12-01T08:30:00Z" }, "outcome": { "refunded": { "amount": "string", "currency": "string" }, "collected": { "amount": "string", "currency": "string" }, "newPnr": "string", "newBookingId": "string", "executedAt": "2026-12-01T08:30:00Z" }, "createdAt": "2026-12-01T08:30:00Z", "history": [ { "type": "string", "at": "2026-12-01T08:30:00Z", "actor": "string", "note": "string" } ] }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `POST /flights/bookings/{bookingId}/post-booking-requests/{requestId}/withdraw` Withdraws a request that has not been accepted yet. **Scope:** `flights:post_booking` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `bookingId` | path | string (uuid) | yes | | | `requestId` | path | string (uuid) | yes | | Response `200`: ```json { "data": { "id": "string", "requestNumber": "string", "bookingId": "string", "type": "string", "status": "string", "isFinal": true, "nextActions": [ { "action": "string", "method": "string", "href": "string", "deadline": "2026-12-01T08:30:00Z" } ], "passengerIndexes": [ 0 ], "sliceIndexes": [ 0 ], "quote": { "airlinePenalty": { "amount": "string", "currency": "string" }, "serviceFee": { "amount": "string", "currency": "string" }, "fareDifference": { "amount": "string", "currency": "string" }, "refundAmount": { "amount": "string", "currency": "string" }, "collectAmount": { "amount": "string", "currency": "string" }, "note": "string", "quotedAt": "2026-12-01T08:30:00Z", "expiresAt": "2026-12-01T08:30:00Z" }, "outcome": { "refunded": { "amount": "string", "currency": "string" }, "collected": { "amount": "string", "currency": "string" }, "newPnr": "string", "newBookingId": "string", "executedAt": "2026-12-01T08:30:00Z" }, "createdAt": "2026-12-01T08:30:00Z", "history": [ { "type": "string", "at": "2026-12-01T08:30:00Z", "actor": "string", "note": "string" } ] }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `GET /flights/bookings/{bookingId}/schedule-changes` Airline schedule changes on the booking. Changes with `requiresAcceptance` need your reply before `replyDeadline`. **Scope:** `flights:post_booking` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `bookingId` | path | string (uuid) | yes | | Response `200`: ```json { "data": [ { "id": "string", "status": "string", "requiresAcceptance": true, "receivedAt": "2026-12-01T08:30:00Z", "replyDeadline": "2026-12-01T08:30:00Z", "original": [ { "sliceIndex": 0, "airline": "string", "flightNumber": "string", "origin": "string", "destination": "string", "departureLocal": "2026-12-01T08:30:00Z", "arrivalLocal": "2026-12-01T08:30:00Z" } ], "changed": [ { "sliceIndex": 0, "airline": "string", "flightNumber": "string", "origin": "string", "destination": "string", "departureLocal": "2026-12-01T08:30:00Z", "arrivalLocal": "2026-12-01T08:30:00Z" } ], "remark": "string", "repliedAt": "2026-12-01T08:30:00Z" } ], "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `POST /flights/bookings/{bookingId}/schedule-changes/{noticeId}/accept` Accepts the new schedule on behalf of the passengers. To refuse it, create a post-booking request (refund or date change) instead. **Scope:** `flights:post_booking` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `bookingId` | path | string (uuid) | yes | | | `noticeId` | path | string (uuid) | yes | | Response `200`: ```json { "data": { "id": "string", "status": "string", "requiresAcceptance": true, "receivedAt": "2026-12-01T08:30:00Z", "replyDeadline": "2026-12-01T08:30:00Z", "original": [ { "sliceIndex": 0, "airline": "string", "flightNumber": "string", "origin": "string", "destination": "string", "departureLocal": "2026-12-01T08:30:00Z", "arrivalLocal": "2026-12-01T08:30:00Z" } ], "changed": [ { "sliceIndex": 0, "airline": "string", "flightNumber": "string", "origin": "string", "destination": "string", "departureLocal": "2026-12-01T08:30:00Z", "arrivalLocal": "2026-12-01T08:30:00Z" } ], "remark": "string", "repliedAt": "2026-12-01T08:30:00Z" }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `POST /flights/bookings/{bookingId}/schedule-changes/{noticeId}/acknowledge` Tells the airline you have seen a change that needs no acceptance. **Scope:** `flights:post_booking` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `bookingId` | path | string (uuid) | yes | | | `noticeId` | path | string (uuid) | yes | | Response `200`: ```json { "data": { "id": "string", "status": "string", "requiresAcceptance": true, "receivedAt": "2026-12-01T08:30:00Z", "replyDeadline": "2026-12-01T08:30:00Z", "original": [ { "sliceIndex": 0, "airline": "string", "flightNumber": "string", "origin": "string", "destination": "string", "departureLocal": "2026-12-01T08:30:00Z", "arrivalLocal": "2026-12-01T08:30:00Z" } ], "changed": [ { "sliceIndex": 0, "airline": "string", "flightNumber": "string", "origin": "string", "destination": "string", "departureLocal": "2026-12-01T08:30:00Z", "arrivalLocal": "2026-12-01T08:30:00Z" } ], "remark": "string", "repliedAt": "2026-12-01T08:30:00Z" }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` ### Flight shopping #### `GET /flights/offers/{offerId}/fare-rules` The fare's refund, change and no-show conditions, plus the airline's full rule text when available. **Scope:** `flights:read` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `offerId` | path | string | yes | | Response `200`: ```json { "data": { "offerId": "string", "refundable": true, "changeable": true, "cancelPenalty": { "amount": "string", "currency": "string" }, "changePenalty": { "amount": "string", "currency": "string" }, "penalties": [ { "kind": "string", "condition": "string", "permitted": true, "amount": { "amount": "string", "currency": "string" }, "percentOfFare": 0 } ], "text": "string" }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `POST /flights/offers/{offerId}/price` Re-prices an offer with the airline right now. Call it before holding when the search is more than a few minutes old or before showing your customer the final price. When `priceChanged` is true the new price replaces the old one for the hold. **Scope:** `flights:read` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `offerId` | path | string | yes | | Response `200`: ```json { "data": { "offerId": "string", "priceChanged": true, "previousTotal": { "amount": "string", "currency": "string" }, "price": { "total": { "amount": "string", "currency": "string" }, "suggestedRetail": { "amount": "string", "currency": "string" }, "commission": { "amount": "string", "currency": "string" } }, "taxes": { "amount": "string", "currency": "string" }, "refundable": true, "changeable": true, "holdAvailableUntil": "2026-12-01T08:30:00Z", "baggage": [ { "category": "string", "included": true, "pieces": 0, "weightKg": 0, "summary": "string" } ], "serviceFees": [ { "type": "string", "name": "string", "perPassenger": { "amount": "string", "currency": "string" }, "total": { "amount": "string", "currency": "string" } } ], "hasFlightHold": true }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `POST /flights/search` Searches every airline source available to your agency and returns bookable offers, cheapest first. Each fare family of the same flights is a separate offer. Offers expire (see `expiresAt`) — hold soon. **Scope:** `flights:read` Request body: ```json { "slices": [ { "origin": "string", "destination": "string", "departureDate": "2026-12-01" } ], "passengers": { "adults": 0, "children": 0, "infants": 0 }, "cabin": "string", "currency": "string", "nationality": "string", "domestic": true, "directOnly": true, "preferredAirline": "string", "maxResults": 0 } ``` | Field | Type | Description | |---|---|---| | `slices` *(required)* | SearchSliceV1[] | 1 slice for one-way, 2 for a return, up to 6 for multi-city. | | `cabin` | string | `economy` (default), `premium_economy`, `business` or `first`. | | `currency` | string | Currency to price in. Defaults to your wallet currency, else your agency's default currency; when neither is configured the search is refused and you must send one. | | `nationality` | string | ISO 3166-1 alpha-2 nationality of the travellers, when fares depend on it. | | `domestic` | boolean | True for a domestic trip (national IDs are then accepted as travel documents). | | `preferredAirline` | string | Optional 2-letter airline code to prefer. | | `maxResults` | integer (int32) | Most offers to return (1–200, default 50), cheapest first. | Response `200`: ```json { "data": { "offers": [ { "offerId": "string", "expiresAt": "2026-12-01T08:30:00Z", "slices": [ { "origin": "string", "destination": "string", "departureAt": "2026-12-01T08:30:00Z", "arrivalAt": "2026-12-01T08:30:00Z", "stops": 0, "durationMinutes": 0, "segments": [ {} ] } ], "price": { "total": {}, "suggestedRetail": {}, "commission": {} }, "fare": { "brandName": "string", "brandCode": "string", "cabin": "string", "refundable": true, "changeable": true, "baggage": [ {} ] }, "seatsLeft": 0 } ], "warnings": [ { "code": "string", "message": "string" } ] }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` ### Webhooks #### `GET /webhook-deliveries` Delivery attempts, newest first. Filter by `endpointId`, `eventId` or `status` (pending, succeeded, failed, cancelled). **Scope:** `webhooks:manage` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `endpointId` | query | string (uuid) | no | | | `eventId` | query | string (uuid) | no | | | `status` | query | string | no | | | `page` | query | integer (int32) | no | | | `pageSize` | query | integer (int32) | no | | Response `200`: ```json { "data": [ { "id": "7c1e0a52-3d4f-4c1b-9a51-2f7d8e6b1c90", "eventId": "7c1e0a52-3d4f-4c1b-9a51-2f7d8e6b1c90", "endpointId": "7c1e0a52-3d4f-4c1b-9a51-2f7d8e6b1c90", "eventType": "string", "status": "string", "attempts": 0, "nextAttemptAt": "2026-12-01T08:30:00Z", "lastAttemptAt": "2026-12-01T08:30:00Z", "lastStatusCode": 0, "lastError": "string", "lastResponseExcerpt": "string", "lastDurationMs": 0, "createdAt": "2026-12-01T08:30:00Z", "completedAt": "2026-12-01T08:30:00Z" } ], "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `POST /webhook-deliveries/{id}/retry` Sends a delivery again now (e.g. after fixing your receiver). A failure starts a fresh retry schedule. **Scope:** `webhooks:manage` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `id` | path | string (uuid) | yes | | Response `200`: ```json { "data": { "id": "7c1e0a52-3d4f-4c1b-9a51-2f7d8e6b1c90", "eventId": "7c1e0a52-3d4f-4c1b-9a51-2f7d8e6b1c90", "endpointId": "7c1e0a52-3d4f-4c1b-9a51-2f7d8e6b1c90", "eventType": "string", "status": "string", "attempts": 0, "nextAttemptAt": "2026-12-01T08:30:00Z", "lastAttemptAt": "2026-12-01T08:30:00Z", "lastStatusCode": 0, "lastError": "string", "lastResponseExcerpt": "string", "lastDurationMs": 0, "createdAt": "2026-12-01T08:30:00Z", "completedAt": "2026-12-01T08:30:00Z" }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `GET /webhook-endpoints` Your webhook endpoints in this environment. **Scope:** `webhooks:manage` Response `200`: ```json { "data": [ { "id": "7c1e0a52-3d4f-4c1b-9a51-2f7d8e6b1c90", "url": "string", "description": "string", "eventTypes": [ "string" ], "status": "string", "disabledReason": "string", "failingSince": "2026-12-01T08:30:00Z", "lastSuccessAt": "2026-12-01T08:30:00Z", "lastFailureAt": "2026-12-01T08:30:00Z", "secretRotating": true, "createdAt": "2026-12-01T08:30:00Z" } ], "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `POST /webhook-endpoints` Registers an endpoint. The response contains the signing `secret` — store it; it is used to verify the `TAM-Signature` header. **Scope:** `webhooks:manage` Request body: ```json { "url": "string", "description": "string", "eventTypes": [ "string" ], "enabled": true } ``` | Field | Type | Description | |---|---|---| | `url` | string | Public `https://` URL that receives the events. | | `description` | string | Optional note for yourself, e.g. "Production booking sync". | | `eventTypes` | string[] | Event types to receive, or `["*"]` for all (including ones added later). | | `enabled` | boolean | On update only: false pauses deliveries, true resumes them (also after an automatic disable). | Response `201`: ```json { "data": { "endpoint": { "id": "7c1e0a52-3d4f-4c1b-9a51-2f7d8e6b1c90", "url": "string", "description": "string", "eventTypes": [ "string" ], "status": "string", "disabledReason": "string", "failingSince": "2026-12-01T08:30:00Z", "lastSuccessAt": "2026-12-01T08:30:00Z", "lastFailureAt": "2026-12-01T08:30:00Z", "secretRotating": true, "createdAt": "2026-12-01T08:30:00Z" }, "secret": "string" }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `GET /webhook-endpoints/{id}` **Scope:** `webhooks:manage` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `id` | path | string (uuid) | yes | | Response `200`: ```json { "data": { "id": "7c1e0a52-3d4f-4c1b-9a51-2f7d8e6b1c90", "url": "string", "description": "string", "eventTypes": [ "string" ], "status": "string", "disabledReason": "string", "failingSince": "2026-12-01T08:30:00Z", "lastSuccessAt": "2026-12-01T08:30:00Z", "lastFailureAt": "2026-12-01T08:30:00Z", "secretRotating": true, "createdAt": "2026-12-01T08:30:00Z" }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `PATCH /webhook-endpoints/{id}` Changes the URL, description or event types (omitted fields keep their value), or pauses/resumes the endpoint with `enabled`. **Scope:** `webhooks:manage` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `id` | path | string (uuid) | yes | | Request body: ```json { "url": "string", "description": "string", "eventTypes": [ "string" ], "enabled": true } ``` | Field | Type | Description | |---|---|---| | `url` | string | Public `https://` URL that receives the events. | | `description` | string | Optional note for yourself, e.g. "Production booking sync". | | `eventTypes` | string[] | Event types to receive, or `["*"]` for all (including ones added later). | | `enabled` | boolean | On update only: false pauses deliveries, true resumes them (also after an automatic disable). | Response `200`: ```json { "data": { "id": "7c1e0a52-3d4f-4c1b-9a51-2f7d8e6b1c90", "url": "string", "description": "string", "eventTypes": [ "string" ], "status": "string", "disabledReason": "string", "failingSince": "2026-12-01T08:30:00Z", "lastSuccessAt": "2026-12-01T08:30:00Z", "lastFailureAt": "2026-12-01T08:30:00Z", "secretRotating": true, "createdAt": "2026-12-01T08:30:00Z" }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `DELETE /webhook-endpoints/{id}` Deletes the endpoint; deliveries still waiting for it are cancelled. **Scope:** `webhooks:manage` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `id` | path | string (uuid) | yes | | Response `204`: No Content #### `POST /webhook-endpoints/{id}/rotate-secret` Issues a new signing secret. For 24 hours deliveries carry two `v1` signatures (new and old secret), so you can deploy the new one without losing events. **Scope:** `webhooks:manage` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `id` | path | string (uuid) | yes | | Response `200`: ```json { "data": { "endpoint": { "id": "7c1e0a52-3d4f-4c1b-9a51-2f7d8e6b1c90", "url": "string", "description": "string", "eventTypes": [ "string" ], "status": "string", "disabledReason": "string", "failingSince": "2026-12-01T08:30:00Z", "lastSuccessAt": "2026-12-01T08:30:00Z", "lastFailureAt": "2026-12-01T08:30:00Z", "secretRotating": true, "createdAt": "2026-12-01T08:30:00Z" }, "secret": "string" }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `POST /webhook-endpoints/{id}/test` Sends a `ping` event to this endpoint within a few seconds. Check the result with `GET /webhook-deliveries?eventId=…`. **Scope:** `webhooks:manage` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `id` | path | string (uuid) | yes | | Response `202`: ```json { "data": { "id": "7c1e0a52-3d4f-4c1b-9a51-2f7d8e6b1c90", "type": "string", "objectType": "string", "objectId": "7c1e0a52-3d4f-4c1b-9a51-2f7d8e6b1c90", "createdAt": "2026-12-01T08:30:00Z", "status": "string", "payload": null }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `GET /webhook-event-types` Every event type you can subscribe to, with when it is sent. **Scope:** `webhooks:manage` Response `200`: ```json { "data": [ { "name": "string", "module": "string", "description": "string" } ], "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `GET /webhook-events` Events raised in this environment, newest first (kept 30 days). Use it to catch up after downtime: filter by `objectId` (a booking id) or `type`. **Scope:** `webhooks:manage` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `type` | query | string | no | | | `objectId` | query | string (uuid) | no | | | `page` | query | integer (int32) | no | | | `pageSize` | query | integer (int32) | no | | Response `200`: ```json { "data": [ { "id": "7c1e0a52-3d4f-4c1b-9a51-2f7d8e6b1c90", "type": "string", "objectType": "string", "objectId": "7c1e0a52-3d4f-4c1b-9a51-2f7d8e6b1c90", "createdAt": "2026-12-01T08:30:00Z", "status": "string", "payload": null } ], "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `GET /webhook-events/{id}` **Scope:** `webhooks:manage` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `id` | path | string (uuid) | yes | | Response `200`: ```json { "data": { "id": "7c1e0a52-3d4f-4c1b-9a51-2f7d8e6b1c90", "type": "string", "objectType": "string", "objectId": "7c1e0a52-3d4f-4c1b-9a51-2f7d8e6b1c90", "createdAt": "2026-12-01T08:30:00Z", "status": "string", "payload": null }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` ### Account #### `GET /account` Returns the calling API key's id and scopes. Use it to check a key works. **Scope:** `account:read` Response `200`: ```json { "data": { "agencyId": "7c1e0a52-3d4f-4c1b-9a51-2f7d8e6b1c90", "keyId": "7c1e0a52-3d4f-4c1b-9a51-2f7d8e6b1c90", "scopes": [ "string" ] }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `GET /account/balance` Your wallet balance — what issuing a booking can spend. Check it before issuing to avoid `account.insufficient_balance`. **Scope:** `account:read` Response `200`: ```json { "data": { "balance": "string", "available": "string", "currency": "string", "active": true }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` #### `GET /account/usage` Your API usage per day, your limits (rate, daily and monthly search quotas) and your look-to-book standing. **Scope:** `account:read` | Parameter | In | Type | Required | Description | |---|---|---|---|---| | `days` | query | integer (int32) | no | | Response `200`: ```json { "data": { "from": "2026-12-01", "to": "2026-12-01", "days": [ { "date": "2026-12-01", "requests": 0, "searches": 0, "holds": 0, "booked": 0, "bookedOtherChannels": 0, "throttled": 0, "clientErrors": 0, "serverErrors": 0 } ], "totalRequests": 0, "totalSearches": 0, "totalBooked": 0, "lookToBookRatio": 0, "maxLookToBookRatio": 0, "ratioWindowDays": 0, "breachStatus": 0, "restriction": 0, "isSuspended": true, "suspendedReason": "string", "requestsPerMinute": 0, "searchesPerDay": 0, "searchesToday": 0, "searchesPerMonth": 0, "searchesThisMonth": 0 }, "meta": { "requestId": "string" }, "pagination": { "page": 0, "pageSize": 0, "total": 0, "hasMore": true } } ``` --- # Error codes ## All error codes ### `auth.*` | Code | HTTP | When it happens | What to do | Retry | |---|---|---|---|---| | [`auth.endpoint_not_allowed`](https://quickota.com/developers/errors/auth.endpoint_not_allowed) | 403 | API keys can only call documented public endpoints under the public base URL. | Check the path against the API reference. | no | | [`auth.insufficient_scope`](https://quickota.com/developers/errors/auth.insufficient_scope) | 403 | The key does not carry the scope this endpoint needs (for example `flights:book` to create bookings). | Edit the key in the panel and add the scope named in `context.requiredScope`, or use a key that has it. | no | | [`auth.invalid_key`](https://quickota.com/developers/errors/auth.invalid_key) | 401 | The key is malformed, was mistyped, or never existed. | Copy the key again from the panel. A key is shown only once when it is created — if it is lost, create a new one and revoke the old one. | no | | [`auth.ip_not_allowed`](https://quickota.com/developers/errors/auth.ip_not_allowed) | 403 | The key has an IP allow-list and the request came from an address that is not on it. | Call from an allowed server, or add this server's public IP (or CIDR range) to the key's allow-list in the panel. | no | | [`auth.key_expired`](https://quickota.com/developers/errors/auth.key_expired) | 401 | The key had an expiry date and it has passed. | Create a new key (or rotate the expired one) in the panel. | no | | [`auth.key_revoked`](https://quickota.com/developers/errors/auth.key_revoked) | 401 | The key was revoked by your agency or by platform support, or it was replaced by a rotation whose grace period ended. | Use the current key from the panel, or create a new one. | no | | [`auth.missing_key`](https://quickota.com/developers/errors/auth.missing_key) | 401 | The request has no `Authorization: Bearer ` header, or it carries a panel login token instead of an API key. The public API only accepts API keys. | Create a key in your panel under Developers → API keys and send it as `Authorization: Bearer tam_live_…`. | no | ### `account.*` | Code | HTTP | When it happens | What to do | Retry | |---|---|---|---|---| | [`account.agency_inactive`](https://quickota.com/developers/errors/account.agency_inactive) | 403 | Your agency account is deactivated on the platform. | Contact platform support. | no | | [`account.api_suspended`](https://quickota.com/developers/errors/account.api_suspended) | 403 | API access for your agency was suspended — usually because usage limits or the look-to-book ratio were breached, or on request of platform support. | Contact platform support. Your panel stays available; only API access is paused. | no | | [`account.insufficient_balance`](https://quickota.com/developers/errors/account.insufficient_balance) | 402 | The wallet balance does not cover the amount due. `context` carries `required`, `available` and `shortfall`. The booking hold is kept until it expires. | Top up the wallet (panel → Finance) and call the same endpoint again before the hold expires. Check `GET /account/balance` before issuing to avoid this. | no | | [`account.module_not_installed`](https://quickota.com/developers/errors/account.module_not_installed) | 403 | Your agency has not activated the module this endpoint belongs to, or the Developer API module itself. | Activate the module from your panel's module store, or contact your account manager. | no | | [`account.wallet_currency_mismatch`](https://quickota.com/developers/errors/account.wallet_currency_mismatch) | 409 | The booking currency differs from your wallet currency. | Search with `currency` set to your wallet currency (see `GET /account/balance`). | no | | [`account.wallet_unavailable`](https://quickota.com/developers/errors/account.wallet_unavailable) | 503 | The payment could not be taken from your wallet for a reason other than balance (wallet missing or temporarily unavailable). Nothing was charged. | Retry later; contact support if it persists. | yes | ### `request.*` | Code | HTTP | When it happens | What to do | Retry | |---|---|---|---|---| | [`request.conflict`](https://quickota.com/developers/errors/request.conflict) | 409 | Another request changed the same resource at the same moment. | Fetch the resource again and retry if the action is still needed. | yes | | [`request.invalid_state_transition`](https://quickota.com/developers/errors/request.invalid_state_transition) | 409 | The action does not apply to the resource's current status (for example issuing a booking that is already cancelled). `context.currentStatus` and `context.allowedActions` describe what is possible now. | Fetch the resource and follow its `nextActions`. | no | | [`request.malformed_json`](https://quickota.com/developers/errors/request.malformed_json) | 400 | The body is not valid JSON or does not match the expected shape. | Send `Content-Type: application/json` with a body matching the API reference. | no | | [`request.not_found`](https://quickota.com/developers/errors/request.not_found) | 404 | The id does not exist, or it belongs to another agency. | Check the id, and that it was created with a key of the same agency. | no | | [`request.validation_failed`](https://quickota.com/developers/errors/request.validation_failed) | 422 | One or more fields are missing or invalid. `errors` maps each field path to its messages. | Fix the listed fields and send the request again. | no | ### `idempotency.*` | Code | HTTP | When it happens | What to do | Retry | |---|---|---|---|---| | [`idempotency.in_progress`](https://quickota.com/developers/errors/idempotency.in_progress) | 409 | The first request with this key has not finished yet. | Wait a few seconds and retry with the same key — you will receive the original result. | yes | | [`idempotency.key_required`](https://quickota.com/developers/errors/idempotency.key_required) | 400 | This endpoint creates something that must never happen twice (a booking, a ticket), so it requires an `Idempotency-Key` header. | Send a unique value (a UUID is ideal) in `Idempotency-Key`. Reuse the same value when you retry the same operation. | no | | [`idempotency.key_reused`](https://quickota.com/developers/errors/idempotency.key_reused) | 422 | The key was already used for a request with a different method, path or body. | Generate a new key for every new operation; reuse a key only to retry exactly the same request. | no | ### `rate_limit.*` | Code | HTTP | When it happens | What to do | Retry | |---|---|---|---|---| | [`rate_limit.exceeded`](https://quickota.com/developers/errors/rate_limit.exceeded) | 429 | Too many requests in a short time for this key. `X-RateLimit-*` headers show the limit and when it resets. | Wait for the number of seconds in `Retry-After`, then retry. Spread calls out and cache search results. | yes | ### `quota.*` | Code | HTTP | When it happens | What to do | Retry | |---|---|---|---|---| | [`quota.look_to_book_exceeded`](https://quickota.com/developers/errors/quota.look_to_book_exceeded) | 429 | Your agency searches far more than it books. Searching is paused until the ratio recovers; booking endpoints keep working. | Reduce repeated or automated searches (cache results, avoid price scraping) and convert more searches into tickets. Contact support if you believe this is wrong. | yes | | [`quota.searches_daily_exceeded`](https://quickota.com/developers/errors/quota.searches_daily_exceeded) | 429 | Your agency used all searches allowed for today. | Wait until the quota resets (00:00 UTC) or ask your account manager for a higher quota. Cache results instead of repeating identical searches. | yes | | [`quota.searches_monthly_exceeded`](https://quickota.com/developers/errors/quota.searches_monthly_exceeded) | 429 | Your agency used all searches allowed for this month. | Wait until the next month or ask your account manager for a higher quota. | yes | ### `flights.*` | Code | HTTP | When it happens | What to do | Retry | |---|---|---|---|---| | [`flights.booking_not_serviceable`](https://quickota.com/developers/errors/flights.booking_not_serviceable) | 409 | Post-booking requests need a ticketed booking that has not been fully cancelled or flown. | Check the booking's `status` and `nextActions`. | no | | [`flights.cancellation_not_allowed`](https://quickota.com/developers/errors/flights.cancellation_not_allowed) | 409 | The fare rules do not allow cancelling or refunding at this time. | Check the fare rules. A no-show refund of taxes may still be possible through a post-booking request. | no | | [`flights.duplicate_booking`](https://quickota.com/developers/errors/flights.duplicate_booking) | 409 | An identical booking already exists (same offer or same request). | Look the existing booking up with `GET /flights/bookings?agencyReference=…` before booking again. | no | | [`flights.exchange_rate_unavailable`](https://quickota.com/developers/errors/flights.exchange_rate_unavailable) | 503 | No exchange rate is configured between the fare currency and your wallet currency, so the fare cannot be priced safely. | Search in your wallet currency, or contact support to configure the rate. | no | | [`flights.fare_not_available`](https://quickota.com/developers/errors/flights.fare_not_available) | 409 | The seats at this fare were sold between search and booking. | Search again and pick another flight or fare. | no | | [`flights.hold_expired`](https://quickota.com/developers/errors/flights.hold_expired) | 409 | The seats were released because the booking was not issued before `deadlines.holdExpiresAt`. The booking is now `cancelled`. | Search and hold again, then issue before the hold expires. | no | | [`flights.hold_not_supported`](https://quickota.com/developers/errors/flights.hold_not_supported) | 409 | The airline issues this fare the moment it is booked (typical of low-cost carriers), so it cannot be held and paid later. The price check shows it as `hasFlightHold: false`. Nothing was booked. | Pick a fare with `hasFlightHold: true`, or book this one through the agency panel, which books and pays in one step. | no | | [`flights.invalid_search`](https://quickota.com/developers/errors/flights.invalid_search) | 422 | The search criteria are incomplete or impossible: missing airports, a date in the past or more than a year ahead, no adult, more infants than adults, more than 9 seated passengers, or more than 6 legs. | Fix the fields listed in `errors` and search again. | no | | [`flights.itinerary_not_bookable`](https://quickota.com/developers/errors/flights.itinerary_not_bookable) | 409 | The airline refused the itinerary (schedule changed, flight closed or cancelled). | Search again. | no | | [`flights.offer_already_booked`](https://quickota.com/developers/errors/flights.offer_already_booked) | 409 | Each offer can be held once. This one already produced a booking (see `context.bookingId`). A retry of the same hold should reuse the same `Idempotency-Key`, which returns the original booking instead of this error. | Use the existing booking, or search again for a new offer. | no | | [`flights.offer_expired`](https://quickota.com/developers/errors/flights.offer_expired) | 410 | The offer's validity window (`expiresAt`) has passed, or the airline no longer recognises it. Fares change constantly, so offers are short-lived. | Search again. Price (`POST /flights/offers/{offerId}/price`) and hold soon after searching. | no | | [`flights.offer_not_found`](https://quickota.com/developers/errors/flights.offer_not_found) | 404 | The `offerId` is unknown. Offers belong to the agency that searched them and are kept for at most 30 minutes. | Search again and use an `offerId` from the new results. | no | | [`flights.passenger_data_invalid`](https://quickota.com/developers/errors/flights.passenger_data_invalid) | 422 | A passenger is invalid: no adult on the booking, a child or infant without birth date, a passenger type that does not match the age, a missing or expired passport on an international trip (a national ID is accepted on domestic trips only), or a placeholder name such as TEST. Names must be in Latin letters exactly as on the travel document. | Correct the passenger listed in `detail` and hold again with a new `Idempotency-Key`. | no | | [`flights.price_changed`](https://quickota.com/developers/errors/flights.price_changed) | 409 | The airline repriced the fare above the accepted tolerance between search and booking. Nothing was booked. | Price the offer again (or search again), show the new price to your customer and book only after they accept it. | no | | [`flights.quote_expired`](https://quickota.com/developers/errors/flights.quote_expired) | 409 | The post-booking quote was not accepted before its `expiresAt`. | Wait for a new quote (support may re-quote) or create a new request. | no | | [`flights.schedule_change_not_supported`](https://quickota.com/developers/errors/flights.schedule_change_not_supported) | 409 | The airline for this booking does not accept schedule-change replies through the API. | Contact support to handle this schedule change. | no | | [`flights.servicing_request_open`](https://quickota.com/developers/errors/flights.servicing_request_open) | 409 | This booking already has an open post-booking request for these passengers. | Follow the open request (`openServicingRequests` on the booking) to its end, or withdraw it first. | no | | [`flights.supplier_timeout`](https://quickota.com/developers/errors/flights.supplier_timeout) | 504 | The airline's reservation system was too slow. For a hold, nothing was booked unless the booking appears in `GET /flights/bookings?agencyReference=…`. | Retry with the same `Idempotency-Key` after a few seconds. | yes | | [`flights.supplier_unavailable`](https://quickota.com/developers/errors/flights.supplier_unavailable) | 503 | The airline connection is temporarily unavailable or refused the request for a reason on its side. | Retry later, or search again for other options. | yes | | [`flights.ticket_not_issued`](https://quickota.com/developers/errors/flights.ticket_not_issued) | 409 | The e-ticket document exists only once the booking is `ticketed`. | Wait for the booking to become `ticketed`, then download the ticket. | no | | [`flights.ticketing_pending`](https://quickota.com/developers/errors/flights.ticketing_pending) | 409 | The airline is still issuing the tickets. This is normal for some airlines and can take up to several minutes. | Do not call `issue` again. Wait for the `flight.booking.ticketed` webhook, or poll `GET /flights/bookings/{id}` every `pollAfterSeconds`. | yes | | [`flights.use_post_booking_request`](https://quickota.com/developers/errors/flights.use_post_booking_request) | 409 | A ticketed booking cannot be cancelled directly: the airline's refund or void conditions apply and must be quoted first. | Create a post-booking request (`POST /flights/bookings/{id}/post-booking-requests`) of type `refund` or `void`, wait for its quote, then accept it. | no | ### `webhooks.*` | Code | HTTP | When it happens | What to do | Retry | |---|---|---|---|---| | [`webhooks.endpoint_not_found`](https://quickota.com/developers/errors/webhooks.endpoint_not_found) | 404 | The webhook endpoint id does not exist in this environment. | List your endpoints with `GET /webhook-endpoints`. | no | | [`webhooks.url_not_allowed`](https://quickota.com/developers/errors/webhooks.url_not_allowed) | 422 | Webhook URLs must be public `https://` addresses. Private, loopback and internal network addresses are refused. | Use a publicly reachable HTTPS URL. | no | ### `server.*` | Code | HTTP | When it happens | What to do | Retry | |---|---|---|---|---| | [`server.internal_error`](https://quickota.com/developers/errors/server.internal_error) | 500 | Something failed on our side. Nothing about your request is wrong. | Retry later. If it persists, contact support and quote the `requestId`. | yes | | [`server.maintenance`](https://quickota.com/developers/errors/server.maintenance) | 503 | The API is in maintenance or overloaded. | Retry after the number of seconds in `Retry-After`. | yes |