# 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.
