{"openapi":"3.1.0","info":{"title":"Routespring Crew Travel API","version":"1.0.0","x-logo":{"url":"https://routespring.com/Routespring-logo.png","altText":"Routespring","href":"https://www.routespring.com","backgroundColor":"#FFFFFF"},"description":"Routespring Crew Travel API lets airlines automate hotel and flight bookings\nfor their crew — directly from their crew management system, without manual\nintervention.\n\nSubmit your roster. Routespring compares it against existing bookings,\ndetermines what actions are required, and books hotels for layovers and\npositioning flights for deadhead legs — automatically, based on the rules\nand preferences your airline has configured.\n\n---\n\n## What you can do\n\n- **Automate hotel layover bookings** — submit a roster and Routespring\n  books, modifies, and cancels hotel stays as your schedule changes.\n- **Book deadhead positioning flights** — when crew need repositioning,\n  Routespring searches and books on your behalf.\n- **Handle IROPs in near real-time** — submit out-of-band hotel or flight\n  requirements directly, bypassing the roster cycle.\n- **Monitor every booking** — track state transitions, retry failures, and\n  export full audit trails per submission.\n\n---\n\n## How it works\n\n```\nSchedule-driven                  IROP / ad-hoc\nPOST /crew/schedules             POST /crew/action-items\n(diff against prior              POST /crew/bookings/request\n submission)\n        │                               │\n        └───────────────┬───────────────┘\n                        ▼\n                Booking engine\n                (config · policy)\n                        │\n               ┌────────┴────────┐\n               │                 │\n            Hotels          Deadhead flights\n               │                 │\n               ▼                 ▼\n          Auto-booked        Auto-booked\n          ──────────────────────────────\n          or NEEDS_REVIEW if price ceiling\n          exceeded or policy blocks\n               │                 │\n               └────────┬────────┘\n                        ▼\n               Action Items → Bookings\n```\n\nProcessing is asynchronous. Every `POST /crew/schedules` returns a\n`schedule_id` immediately — poll `/processing-status` to track progress,\nthen retrieve action items and bookings once processing completes.\n\n---\n\n## Before you start\n\n- **Sandbox** — all examples in this documentation run against the sandbox.\n  Get credentials from your Routespring dashboard under\n  Settings → Integrations → REST API.\n- **Interactive walkthrough** — the fastest way to see the full flow in\n  action without writing any code.\n- **Quickstart** — a complete curl-based guide covering every step from\n  authentication to booking retrieval.\n\n---\n\n## DEMO\n\n<a href=\"walkthrough/index.html\" target=\"_blank\" rel=\"noopener\"><b>▶ Launch the crew lifecycle walkthrough ↗</b></a>\n\nA guided, click-through demo of the full crew lifecycle against the **sandbox**.\nPaste your sandbox `client_id` / `client_secret` (from the dashboard) and a\ntraveler email, then step through it live: get a token → configure → submit a\nschedule → poll → action items → bookings → select and confirm a deadhead flight →\nfull trip details. This reference page (everything below) is the contract; the\ndemo is the same flow, executed for you one step at a time.\n\n## Quickstart\n\nA copy-paste guide for integrators. Walks the lifecycle in order: one-time configuration → schedule-driven booking → IROP / airline-injected booking → ops & retries → error responses you should handle.\n\nEvery block is a real `curl` invocation. Bodies are inline so each one is self-contained. Variables (`$BASE`, `$BEARER`) are set once in §0.\n\n---\n\n### 0. Get an access token\n\nThe crew API authenticates with an **OAuth2 `client_credentials`** flow served by the Routespring auth service. You exchange a long‑lived `client_id` / `client_secret` for a short‑lived **access token (JWT)**, and send that token as the Bearer on every crew API call. (The gateway validates the access token and resolves it to your company — there is no airline id in the URL.)\n\n```bash\n# Base of the Routespring OAuth service.\nexport AUTH_BASE=\"https://oauth-sandbox.routespring.com\"   # sandbox\n# export AUTH_BASE=\"https://oauth.routespring.com\"          # production (host confirmed at onboarding)\n```\n\n**Step 1 — get client credentials (one-time).** Register an OAuth2 client, or receive `client_id`/`client_secret` directly during onboarding. Registration authenticates as your Routespring user (`Bearer <email>:<sToken>`):\n\n```bash\ncurl -sS -X POST \"$AUTH_BASE/oauth/register?name=crew-integration&scopes=events:read%20events:write\" \\\n  -H \"Authorization: Bearer your-user@airline.com:YOUR_STOKEN\"\n# -> { \"client_id\": \"0687e8bd-…\", \"client_secret\": \"vwWDwBbC5QJ…\" }\n```\n\n**Step 2 — exchange them for an access token.** HTTP Basic (`client_id:client_secret`) + `grant_type=client_credentials`:\n\n```bash\ncurl -sS -X POST \"$AUTH_BASE/oauth/token?grant_type=client_credentials\" \\\n  -u \"CLIENT_ID:CLIENT_SECRET\"\n# -> { \"access_token\": \"<JWT>\", \"refresh_token\": \"<UUID>\", \"token_type\": \"Bearer\",\n#      \"expires_in\": 899, \"scope\": \"events:read events:write\" }\n```\n\n**Step 3 — refresh when it expires (~15 min).** No credentials needed, just the refresh token:\n\n```bash\ncurl -sS -X POST \"$AUTH_BASE/oauth/refresh?refresh_token=YOUR_REFRESH_TOKEN\"\n# -> a fresh access_token (+ expires_in)\n```\n\nNow set the variables the rest of this guide uses — `$BEARER` is the `access_token` from Step 2:\n\n```bash\nexport BASE=\"https://sandbox.routespring.com/api/v1\"   # sandbox: gateway host + /api/v1\n# export BASE=\"https://api.routespring.com/api/v1\"      # production (host confirmed at onboarding)\nexport BEARER=\"<access_token from /oauth/token>\"  # short-lived; refresh via Step 3\n```\n\n> **Why a Bearer token?** Every endpoint under `/crew/…` requires `Authorization: Bearer $BEARER`. Your airline (tenant) is derived from the token itself — there is no airline id in the URL. The hotel directory at `/hotels` also requires it. Missing, malformed, or **expired** bearer → `401 UNAUTHORIZED` (refresh and retry).\n\n---\n\n### 0.1 Before your first end-to-end — behaviors that will trip you up\n\nThese aren't obvious from the endpoint reference, but each one will cost you a debugging session if you don't know it up front:\n\n- **Everything off `POST /schedules` is asynchronous.** The submit returns `202` with `processing_status: QUEUED`. Poll `…/processing-status` until it is terminal (`COMPLETED` / `PARTIAL` / `FAILED`) **before** trusting `…/action-items`. An empty `action_items` array immediately after submit usually just means processing hasn't finished — not that nothing was derived.\n\n- **Deadhead diffing is stateful.** Deadhead (`FLIGHT`) action items are derived by diffing your submission against the **previous** submission's persisted legs, keyed on `employee_id + pairing_number + duty_date + leg_sequence`. Consequences:\n  - Re-POSTing a byte-identical roster is a **no-op** — no new action items.\n  - Changing any key field emits a `DEADHEAD_CANCELLED` for the old leg **and** a `NEW_DEADHEAD` for the new one.\n  - When testing, use a **fresh `employee_id`** to get a clean `NEW_DEADHEAD` with no cancellation noise.\n\n- **Keep dates consistent within a duty period.** A leg's `duty_date` must agree with the date of its `dep_time_local` / `expected_dep_time_local`. If they disagree, the leg is dropped during grouping and nothing derives (you'll see an empty `action_items` with no error).\n\n- **A deadhead books the positioning flight you submit.** A `DH` leg books a flight on its explicit `from_airport` → `to_airport`, exactly as you submit them — the route is taken as-is, not derived from the crew's home base.\n\n- **Deadheads and hotels both book automatically.** `/schedules` *derives* the need and books it — a hotel layover, or a `NEW_DEADHEAD` positioning flight on the DH leg's explicit `from_airport` → `to_airport`, is auto-booked through the supplier path. Auto-booking is gated by policy: a deviation (e.g. an early check-in, or a route/date the supplier can't fulfill) or a strict price mismatch surfaces the item as `NEEDS_REVIEW` for an agent to resolve instead.\n\n- **Flight booking requires a date of birth.** The crew member's traveler profile must have a DOB (airline/GDS requirement). Without one, a flight `POST /bookings/request` returns `422`.\n\n- **Flight options are short-lived and opaque.** `flight_option_id` values from `GET /flight-options` expire (~20 min) and must be passed back **verbatim** to `POST /bookings/request` — never parse or construct them. Search fresh and commit promptly; an expired id fails the commit.\n\n- **Read a booking in two steps.** `GET /bookings/{id}/status` refreshes the live supplier state (PNR / ticket / transaction / trip-session ids) and persists it; then `GET /bookings/{id}` returns the full record with those ids on the `flight` block (`pnr`, `ticket_id`, `transaction_id`, `trip_session_id`).\n\n- **Raw supplier itinerary (optional deep dive).** Beyond the crew booking record, you can fetch the full supplier itinerary — complete PNR, fare breakdown, segments — from the flight API at `GET $BASE/bookings/trips/{trip_session_id}` (see the **Flights** tag), using the `trip_session_id` returned by `GET /crew/bookings/{id}`.\n\n---\n\n### 1. One-time airline setup\n\nRun these once when onboarding a new airline. They are **idempotent** — re-running a PUT replaces the config (creating a new version under the hood).\n\n#### 1.0 Crew booking account + automation flags\n\n`crew_booker_email` is the Routespring account under which **engine-driven** crew bookings — hotel layovers the diff engine books and later amends/cancels — are placed; set it **first**, before any roster flows. (A manual flight commit via `POST /crew/bookings/request` is booked under the user whose bearer token makes that call, *not* this account — so for flights the actor is the API caller.) The other setting exposed here is `cancellation_lookback_days`; omit a field on `PUT` to leave it unchanged. The automation execution levers (cancellation / modification / swap / deadhead-flight execution, strict price matching, NOC stats) are Routespring-managed and not part of this API. Deadhead positioning flights are booked automatically by the engine, the same as hotel layovers.\n\n```bash\ncurl -sS -X PUT \"$BASE/crew/config/company\" \\\n  -H \"Authorization: Bearer $BEARER\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"crew_booker_email\": \"crew-ops@flymx.com\"\n  }'\n\n# Read the current config back any time:\ncurl -sS \"$BASE/crew/config/company\" -H \"Authorization: Bearer $BEARER\"\n```\n\n#### 1.1 Hotel inventory + contract rates\n\nTell Routespring which preferred hotel to book at each crew base (one hotel per base). The booking engine prefers it over open-market searches.\n\n```bash\ncurl -sS -X PUT \"$BASE/crew/config/hotels\" \\\n  -H \"Authorization: Bearer $BEARER\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"bases\": [\n      {\n        \"base_station\": \"SLC\",\n        \"hotels\": [\n          {\n            \"property_id\": \"hilton-slc-airport\",\n            \"chain\": \"HILTON\",\n            \"policy\": \"STANDARD\",\n            \"standard_check_in_time\": \"15:00\",\n            \"standard_check_out_time\": \"11:00\",\n            \"buffer_minutes\": 60,\n            \"contract_rate\": { \"amount\": 149.00, \"currency\": \"USD\" },\n            \"rate_periods\": [\n              { \"effective_from\": \"2026-01-01\", \"effective_to\": \"2026-06-30\", \"rate\": { \"amount\": 149.00, \"currency\": \"USD\" } },\n              { \"effective_from\": \"2026-07-01\", \"effective_to\": \"2026-12-31\", \"rate\": { \"amount\": 169.00, \"currency\": \"USD\" } }\n            ]\n          }\n        ]\n      },\n      {\n        \"base_station\": \"JFK\",\n        \"hotels\": [\n          {\n            \"property_id\": \"TVP-JFK-9001\",\n            \"policy\": \"FLEXIBLE\",\n            \"flex_window_minutes\": 120,\n            \"buffer_minutes\": 30\n          }\n        ]\n      }\n    ]\n  }' | jq .\n```\n\n> **GDS vs DIRECT.** `GDS` hotels are looked up against the GDS global inventory by `property_id`; an unknown id returns `422 UNKNOWN_HOTEL_PROPERTY` at PUT time. `DIRECT` hotels are managed by you and not validated against any external system.\n\n#### 1.2 Flight rules (deadhead thresholds)\n\nThe price ceiling for auto-booking a positioning flight:\n\n```bash\ncurl -sS -X PUT \"$BASE/crew/config/flight-rules\" \\\n  -H \"Authorization: Bearer $BEARER\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"auto_book_max_price\": { \"amount\": 800.00, \"currency\": \"USD\" }\n  }' | jq .\n```\n\n#### 1.3 Flight preferences\n\nPreferred / blacklisted carriers and cabin:\n\n```bash\ncurl -sS -X PUT \"$BASE/crew/config/flights\" \\\n  -H \"Authorization: Bearer $BEARER\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"preferred_airlines\": [\"DL\", \"UA\", \"AA\"],\n    \"blacklisted_airlines\": [\"F9\"],\n    \"preferred_cabin\": \"ECONOMY\",\n    \"same_carrier_preference\": true\n  }' | jq .\n```\n\n#### 1.4 Verify the config landed\n\n```bash\ncurl -sS \"$BASE/crew/config/hotels\"        -H \"Authorization: Bearer $BEARER\" | jq '.bases | length'\ncurl -sS \"$BASE/crew/config/flight-rules\"  -H \"Authorization: Bearer $BEARER\" | jq .\n```\n\n---\n\n### 2. Schedule-driven flow (the normal happy path)\n\nThis is the standard loop: you POST a roster and Routespring diffs it, derives action\nitems, and books against your config. Hotel layovers are diffed against existing booking\nactuals; deadhead positioning against the prior submission's persisted legs.\n\n#### 2.1 Submit a roster (JSON)\n\n```bash\nSUBMIT_RESPONSE=$(curl -sS -X POST \"$BASE/crew/schedules\" \\\n  -H \"Authorization: Bearer $BEARER\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"external_ref\": \"MX-20260527-4821\",\n    \"as_of\": \"2026-05-27T08:00:00Z\",\n    \"schedule_start_date\": \"2026-05-16\",\n    \"schedule_end_date\": \"2026-05-31\",\n    \"pairings\": [\n      {\n        \"pairing_number\": \"MX-20260527-4821\",\n        \"origin_base\": \"SLC\",\n        \"crew\": [\n          { \"employee_id\": \"EMP-1042\", \"name\": \"Jordan Reyes\", \"rank\": \"CA\", \"home_base\": \"SLC\", \"traveller_email\": \"jreyes@flymx.com\" },\n          { \"employee_id\": \"EMP-2317\", \"name\": \"Alex Pham\", \"rank\": \"FO\", \"home_base\": \"SLC\", \"traveller_email\": \"apham@flymx.com\" }\n        ],\n        \"duty_periods\": [\n          {\n            \"duty_period_num\": 1,\n            \"duty_date\": \"2026-05-27\",\n            \"fdp_start_local\": \"2026-05-26T23:30:00-06:00\",\n            \"fdp_end_local\": \"2026-05-27T12:15:00-04:00\",\n            \"legs\": [\n              { \"leg_sequence\": 1, \"activity_code\": \"FLT\", \"flight_number\": \"MX401\", \"from_airport\": \"SLC\", \"to_airport\": \"LAX\", \"dep_time_local\": \"2026-05-27T01:00:00-06:00\", \"arr_time_local\": \"2026-05-27T01:30:00-07:00\", \"aircraft_type\": \"A220-300\", \"aircraft_tail\": \"N412MX\" },\n              { \"leg_sequence\": 2, \"activity_code\": \"FLT\", \"flight_number\": \"MX588\", \"from_airport\": \"LAX\", \"to_airport\": \"IAD\", \"dep_time_local\": \"2026-05-27T03:00:00-07:00\", \"arr_time_local\": \"2026-05-27T14:15:00-04:00\", \"aircraft_type\": \"A220-300\", \"aircraft_tail\": \"N412MX\" }\n            ],\n            \"layover\": { \"station\": \"IAD\", \"booking_required\": true, \"checkin_time_local\": \"2026-05-27T14:45:00-04:00\", \"checkout_time_local\": \"2026-05-28T01:00:00-04:00\" }\n          },\n          {\n            \"duty_period_num\": 2,\n            \"duty_date\": \"2026-05-28\",\n            \"fdp_start_local\": \"2026-05-28T02:00:00-04:00\",\n            \"fdp_end_local\": \"2026-05-28T09:30:00-05:00\",\n            \"legs\": [\n              { \"leg_sequence\": 3, \"activity_code\": \"FLT\", \"flight_number\": \"MX210\", \"from_airport\": \"IAD\", \"to_airport\": \"TUL\", \"dep_time_local\": \"2026-05-28T04:00:00-04:00\", \"arr_time_local\": \"2026-05-28T05:45:00-05:00\", \"aircraft_type\": \"A220-300\", \"aircraft_tail\": \"N388MX\" },\n              { \"leg_sequence\": 4, \"activity_code\": \"DH\", \"from_airport\": \"TUL\", \"to_airport\": \"DAL\", \"expected_dep_time_local\": \"2026-05-28T07:00:00-05:00\", \"expected_arr_time_local\": \"2026-05-28T08:15:00-05:00\", \"booking_required\": true }\n            ],\n            \"layover\": { \"station\": \"DAL\", \"booking_required\": true, \"checkin_time_local\": \"2026-05-28T08:45:00-05:00\", \"checkout_time_local\": \"2026-05-29T00:30:00-05:00\" }\n          },\n          {\n            \"duty_period_num\": 3,\n            \"duty_date\": \"2026-05-29\",\n            \"fdp_start_local\": \"2026-05-29T01:30:00-05:00\",\n            \"fdp_end_local\": \"2026-05-29T04:00:00-06:00\",\n            \"legs\": [\n              { \"leg_sequence\": 5, \"activity_code\": \"FLT\", \"flight_number\": \"MX305\", \"from_airport\": \"DAL\", \"to_airport\": \"SLC\", \"dep_time_local\": \"2026-05-29T03:00:00-05:00\", \"arr_time_local\": \"2026-05-29T03:45:00-06:00\", \"aircraft_type\": \"A220-300\", \"aircraft_tail\": \"N401MX\" }\n            ]\n          }\n        ]\n      }\n    ]\n  }')\necho \"$SUBMIT_RESPONSE\" | jq .\n\n# Capture the minted schedule_id for the next steps.\nexport SCHEDULE_ID=$(echo \"$SUBMIT_RESPONSE\" | jq -r .schedule_id)\necho \"SCHEDULE_ID=$SCHEDULE_ID\"\n```\n\nExpected: `202 Accepted` with `processing_status: QUEUED` and a generated `schedule_id` like `UPL-35-1779633752601`.\n\n> **Deadhead in v1.** The JSON `POST /schedules` path derives both HOTEL and FLIGHT (deadhead) action items from the same submission. JSON is the only ingestion format — there is no file-upload endpoint.\n\n#### 2.2 Poll processing status\n\n```bash\ncurl -sS \"$BASE/crew/schedules/$SCHEDULE_ID/processing-status\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq .\n```\n\n`status` walks: `QUEUED → PROCESSING → BOOKING → COMPLETED` (or `FAILED`).\n\n> **Why status can revert from COMPLETED to BOOKING.** The hotel branch can flip the upload to COMPLETED before the deadhead (flight) branch finishes its supplier calls. If any FLIGHT task is still non-terminal, this endpoint reports `BOOKING` rather than `COMPLETED` so you don't stop polling early. Once every flight task is `BOOKED` / `FAILED` / `CANCELLED`, status reports `COMPLETED`.\n\n#### 2.3 List derived action items\n\n```bash\n# All action items for the submission\ncurl -sS \"$BASE/crew/schedules/$SCHEDULE_ID/action-items\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq '.action_items[] | {action_id, type, classification, priority, disposition}'\n\n# Just FLIGHT (deadhead) items\ncurl -sS \"$BASE/crew/schedules/$SCHEDULE_ID/action-items?type=FLIGHT\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq .\n\n# Just items needing human review\ncurl -sS \"$BASE/crew/schedules/$SCHEDULE_ID/action-items?disposition=PENDING&priority=P1\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq .\n```\n\nAction-item classifications you'll see: `NEW_LAYOVER`, `LAYOVER_DATE_CHANGE`, `LAYOVER_CITY_CHANGE`, `LAYOVER_CANCELLED`, `NEW_DEADHEAD`, `DEADHEAD_TIME_CHANGE`, `DEADHEAD_CANCELLED`, and the data-quality / IROP groups documented in `openapi.yaml`.\n\n#### 2.4 List the resulting bookings\n\n```bash\n# By employee\ncurl -sS \"$BASE/crew/bookings?employee_id=EMP-12047\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq '.bookings[] | {booking_id, type, status, hotel, flight}'\n\n# By pairing\ncurl -sS \"$BASE/crew/bookings?pairing_number=P-23845\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq .\n\n# By IROP reference\ncurl -sS \"$BASE/crew/bookings?irop_ref=IROP-2026-06-15-EWR-001\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq .\n\n# Only flight bookings in a date range\ncurl -sS \"$BASE/crew/bookings?type=FLIGHT&from_date=2026-06-01&to_date=2026-06-30\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq .\n```\n\n> **Filter is required.** `GET /bookings` with no filter returns `400 INVALID_REQUEST`. Pass any anchor key: `employee_id`, `pairing_number`, `flight_number`, `irop_ref`, `schedule_id`. The booking index is denormalised so any one anchor is a single-indexed lookup.\n\n#### 2.5 Get a specific booking + its status history\n\n```bash\nexport BOOKING_ID=\"BKG-...\"  # from §2.4\n\ncurl -sS \"$BASE/crew/bookings/$BOOKING_ID\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq .\n\ncurl -sS \"$BASE/crew/bookings/$BOOKING_ID/status\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq .\n# returns current_status + the full transition history\n```\n\n#### 2.6 Modify a booking (date change / hotel swap / fare upgrade)\n\n```bash\n# In-place hotel date-window change (preserves booking_id + supplier confirmation)\ncurl -sS -X PATCH \"$BASE/crew/bookings/$BOOKING_ID\" \\\n  -H \"Authorization: Bearer $BEARER\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"reason\": \"PAIRING_EXTENDED\",\n    \"hotel\": { \"check_out_date\": \"2026-06-16\" }\n  }' | jq .\n```\n\n> **Side effect: `manual_override` becomes `true`.** After a successful PATCH, the diff engine will not modify this booking on subsequent schedule submissions. Release it back to auto-management by PATCHing `{ \"manual_override\": false }` (alone or alongside other changes).\n\n#### 2.7 Cancel a booking\n\n```bash\ncurl -sS -X DELETE \"$BASE/crew/bookings/$BOOKING_ID?reason=roster_change\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq .\n```\n\nA cancelled booking whose pairing later reappears in a schedule **will not auto-recreate** — re-issue explicitly with `POST /bookings/request`.\n\n---\n\n### 3. IROP / airline-injected flow\n\nWhen ops already knows what they need and the roster system hasn't caught up.\n\n#### 3.1 Inject an IROP hotel requirement (engine still picks the property)\n\nUse `POST /action-items` when the airline has identified the requirement but wants Routespring to auto-select against the active hotel config.\n\n```bash\ncurl -sS -X POST \"$BASE/crew/action-items\" \\\n  -H \"Authorization: Bearer $BEARER\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"type\": \"HOTEL\",\n    \"classification\": \"IROP_HOTEL_EXTEND\",\n    \"priority\": \"P1\",\n    \"source\": \"IROP\",\n    \"irop_ref\": \"IROP-2026-06-15-EWR-001\",\n    \"anchors\": {\n      \"traveller_email\": \"jordan.reyes@flymx.com\",\n      \"employee_id\": \"EMP-12047\",\n      \"pairing_number\": \"P-23845\"\n    },\n    \"hotel\": {\n      \"city\": \"EWR\",\n      \"check_in_date\": \"2026-06-15\",\n      \"check_out_date\": \"2026-06-16\",\n      \"num_nights\": 1\n    },\n    \"notes\": \"Crew weather-diverted to EWR; extend layover.\"\n  }' | jq .\n```\n\n#### 3.2 Commit a specific hotel\n\n```bash\ncurl -sS -X POST \"$BASE/crew/bookings/request\" \\\n  -H \"Authorization: Bearer $BEARER\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"type\": \"HOTEL\",\n    \"classification\": \"IROP_HOTEL_EXTEND\",\n    \"source\": \"IROP\",\n    \"irop_ref\": \"IROP-2026-06-15-EWR-001\",\n    \"anchors\": {\n      \"traveller_email\": \"jordan.reyes@flymx.com\",\n      \"employee_id\": \"EMP-12047\",\n      \"pairing_number\": \"P-23845\"\n    },\n    \"hotel\": {\n      \"city\": \"EWR\",\n      \"property_id\": \"24355\",\n      \"check_in_date\": \"2026-06-15\",\n      \"check_out_date\": \"2026-06-16\"\n    }\n  }' | jq .\n```\n\nOmit `property_id` to let Routespring auto-select per priority order.\n\n#### 3.3 Search flight options (manual deadhead picker)\n\n```bash\ncurl -sS \"$BASE/crew/flight-options?from_airport=EWR&to_airport=SLC&date=2026-06-15&arrive_by=2026-06-15T20:00:00Z\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq '.options[] | {flight_option_id, airline_code, flight_number, dep_time, arr_time, price}'\n```\n\nReturns options each carrying a `flight_option_id` (a supplier offer id) — **held for 20 minutes**. To commit one, POST it back with the crew member's `traveller_email` on `anchors`.\n\n#### 3.4 Commit a specific flight from §3.3\n\n```bash\ncurl -sS -X POST \"$BASE/crew/bookings/request\" \\\n  -H \"Authorization: Bearer $BEARER\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"type\": \"FLIGHT\",\n    \"classification\": \"IROP_NEW_DEADHEAD\",\n    \"source\": \"IROP\",\n    \"irop_ref\": \"IROP-2026-06-15-EWR-001\",\n    \"anchors\": {\n      \"traveller_email\": \"jordan.reyes@flymx.com\",\n      \"employee_id\": \"EMP-12047\",\n      \"pairing_number\": \"P-23845\"\n    },\n    \"flight\": {\n      \"flight_option_id\": \"REPLACE_ME_FROM_3.3\"\n    },\n    \"notes\": \"Manager-selected from /flight-options\"\n  }' | jq .\n```\n\nOr commit a flight without a prior `/flight-options` search by passing the route + date:\n\n```bash\ncurl -sS -X POST \"$BASE/crew/bookings/request\" \\\n  -H \"Authorization: Bearer $BEARER\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"type\": \"FLIGHT\",\n    \"classification\": \"IROP_NEW_DEADHEAD\",\n    \"source\": \"IROP\",\n    \"irop_ref\": \"IROP-2026-06-15-EWR-002\",\n    \"anchors\": { \"traveller_email\": \"jordan.reyes@flymx.com\", \"employee_id\": \"EMP-12047\" },\n    \"flight\": {\n      \"from_airport\": \"EWR\",\n      \"to_airport\": \"SLC\",\n      \"depart_date\": \"2026-06-15\",\n      \"depart_after\": \"2026-06-15T09:00:00Z\",\n      \"arrive_by\":    \"2026-06-15T20:00:00Z\",\n      \"cabin_class\": \"ECONOMY\"\n    }\n  }' | jq .\n```\n\n---\n\n### 4. Ops & retries\n\n#### 4.1 Hotel directory (GDS global search, airline-independent)\n\nUse to look up `property_id`s before adding hotels to your config.\nAny combination of `name`, `city`, `airport_code` (at least one required).\n\n```bash\n# By partial name\ncurl -sS \"$BASE/hotels?name=Hilton%20Newark&limit=10\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq '.hotels[] | {property_id, name, city}'\n\n# By city\ncurl -sS \"$BASE/hotels?city=Newark&limit=10\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq '.hotels[] | {property_id, name, city}'\n\n# By IATA airport code (3 letters) - matches hotels that name the airport\n# AND hotels in the airport's city per Routespring's airport directory\ncurl -sS \"$BASE/hotels?airport_code=EWR&limit=10\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq '.hotels[] | {property_id, name, city}'\n\n# Combine filters - results are unioned and deduplicated on property_id\ncurl -sS \"$BASE/hotels?airport_code=EWR&name=Hilton&limit=10\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq '.hotels[] | {property_id, name, city}'\n```\n\n#### 4.2 Audit events for a schedule\n\nData-quality flags and duplicate-row detections from the diff engine:\n\n```bash\ncurl -sS \"$BASE/crew/schedules/$SCHEDULE_ID/audit-events\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq .\n```\n\n#### 4.3 Exports\n\n```bash\n# Action items as CSV\ncurl -sS -o actions.csv \"$BASE/crew/schedules/$SCHEDULE_ID/actions.csv\" \\\n  -H \"Authorization: Bearer $BEARER\"\n```\n\n#### 4.4 Retry a failed booking\n\n```bash\n# Single retry\ncurl -sS -X POST \"$BASE/crew/bookings/$BOOKING_ID/retry\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq .\n\n# What is queued\ncurl -sS \"$BASE/crew/bookings/retry-queue?limit=20\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq .\n\n# Recent bulk-retry jobs\ncurl -sS \"$BASE/crew/bookings/retry-jobs?limit=10\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq '.retry_jobs[] | {retry_job_id, status, total, succeeded, failed}'\n\n# Single job detail\ncurl -sS \"$BASE/crew/bookings/retry-jobs/JOB-...\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq .\n```\n\n---\n\n### 5. Error responses you should handle\n\nThe same envelope is used everywhere:\n\n```json\n{ \"error\": { \"code\": \"<UPPER_SNAKE>\", \"message\": \"<human>\", \"details\": [...] }, \"status\": <int> }\n```\n\n#### 5.1 `401 UNAUTHORIZED` — missing / malformed bearer\n\n```bash\ncurl -sS \"$BASE/crew/schedules\" | jq .\n# => { \"error\": { \"code\": \"UNAUTHORIZED\", ... }, \"status\": 401 }\n```\n\n#### 5.2 `404 NOT_FOUND` — unknown id, or cross-tenant access\n\n```bash\ncurl -sS \"$BASE/crew/bookings/BKG-DOES-NOT-EXIST\" \\\n  -H \"Authorization: Bearer $BEARER\" | jq .\n```\n\n> **Cross-tenant requests return 404, not 403.** A resource owned by a different airline looks identical to a non-existent resource — we don't leak existence across tenants.\n\n#### 5.3 `409 CONFLICT` — duplicate booking on `/bookings/request`\n\nA live booking already exists for the same anchors and booking type. Use `PATCH /bookings/{id}` to modify the existing one, or release it with `manual_override: false` first.\n\n```bash\n# Run the §3.2 hotel commit twice. The second call should return 409:\n# { \"error\": { \"code\": \"CONFLICT\", \"message\": \"a live HOTEL booking already exists for ...\" }, \"status\": 409 }\n```\n\n#### 5.4 Expired flight option on `/bookings/request`\n\nA `flight_option_id` is a supplier offer held for 20 minutes after the originating `/flight-options` query. Committing an expired offer doesn't pre-fail — the request is accepted (`202`) and the booking comes back `NEEDS_REVIEW` (the commit failed at the supplier). Re-search and pick a fresh option.\n\n#### 5.5 `422 VALIDATION_FAILED` — structural validation\n\n```bash\ncurl -sS -X POST \"$BASE/crew/schedules\" \\\n  -H \"Authorization: Bearer $BEARER\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{ \"pairings\": [] }' | jq .\n# => { \"error\": { \"code\": \"VALIDATION_FAILED\", ... }, \"status\": 422 }\n```\n\n---\n\n### 6. Full setup-and-smoke-test in one block\n\nFor an automated airline-onboarding script:\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\n: \"${BASE:?set BASE first}\"\n: \"${BEARER:?set BEARER first}\"\n\ncall() {\n  curl -fsS -H \"Authorization: Bearer $BEARER\" -H \"Content-Type: application/json\" \"$@\"\n}\n\necho \"1/4  Push hotel config…\"\ncall -X PUT \"$BASE/crew/config/hotels\" \\\n  -d '{\"bases\":[{\"base_station\":\"SLC\",\"hotels\":[{\"property_id\":\"hilton-slc-airport\",\"policy\":\"STANDARD\",\"standard_check_in_time\":\"15:00\",\"standard_check_out_time\":\"11:00\",\"buffer_minutes\":60}]}]}' >/dev/null\n\necho \"2/4  Push flight rules…\"\ncall -X PUT \"$BASE/crew/config/flight-rules\" \\\n  -d '{\"auto_book_max_price\":{\"amount\":800.00,\"currency\":\"USD\"}}' >/dev/null\n\necho \"3/4  Submit a smoke-test roster…\"\nSCHEDULE_ID=$(call -X POST \"$BASE/crew/schedules\" \\\n  -d '{\"external_ref\":\"SMOKE-TEST\",\"as_of\":\"2026-06-15T08:00:00Z\",\"schedule_start_date\":\"2026-06-14\",\"schedule_end_date\":\"2026-06-15\",\"pairings\":[{\"pairing_number\":\"P-SMOKE\",\"origin_base\":\"SLC\",\"crew\":[{\"employee_id\":\"EMP-SMOKE\",\"name\":\"Smoke Test\",\"home_base\":\"SLC\",\"rank\":\"CA\",\"traveller_email\":\"smoke@example.invalid\"}],\"duty_periods\":[{\"duty_period_num\":1,\"duty_date\":\"2026-06-14\",\"fdp_start_local\":\"2026-06-14T16:00:00-06:00\",\"fdp_end_local\":\"2026-06-14T20:45:00-04:00\",\"legs\":[{\"leg_sequence\":1,\"activity_code\":\"FLT\",\"from_airport\":\"SLC\",\"to_airport\":\"JFK\",\"dep_time_local\":\"2026-06-14T17:00:00-06:00\",\"arr_time_local\":\"2026-06-14T20:45:00-04:00\"}],\"layover\":{\"station\":\"JFK\",\"booking_required\":true,\"checkin_time_local\":\"2026-06-14T21:15:00-04:00\",\"checkout_time_local\":\"2026-06-15T11:00:00-04:00\"}},{\"duty_period_num\":2,\"duty_date\":\"2026-06-15\",\"fdp_start_local\":\"2026-06-15T13:15:00-04:00\",\"fdp_end_local\":\"2026-06-15T16:30:00-06:00\",\"legs\":[{\"leg_sequence\":2,\"activity_code\":\"FLT\",\"from_airport\":\"JFK\",\"to_airport\":\"SLC\",\"dep_time_local\":\"2026-06-15T14:15:00-04:00\",\"arr_time_local\":\"2026-06-15T16:30:00-06:00\"}]}]}]}' \\\n  | jq -r .schedule_id)\necho \"    schedule_id=$SCHEDULE_ID\"\n\necho \"4/4  Poll until COMPLETED (or BOOKING with all flight tasks terminal)…\"\nfor i in 1 2 3 4 5 6 7 8 9 10; do\n  STATUS=$(call \"$BASE/crew/schedules/$SCHEDULE_ID/processing-status\" | jq -r .status)\n  echo \"    [$i] status=$STATUS\"\n  case \"$STATUS\" in\n    COMPLETED|FAILED) break ;;\n    *) sleep 2 ;;\n  esac\ndone\n\necho \"Done. Bookings:\"\ncall \"$BASE/crew/bookings?schedule_id=$SCHEDULE_ID\" | jq '.bookings[] | {booking_id, type, status}'\n```\n\n## Glossary\n\nAirline-industry terminology used in this API. Only terms that actually\nappear in the spec are listed here. Many field descriptions cross-reference\nthese definitions.\n\n### Crew\n\n- **Crew Member** — A pilot or flight attendant assigned to operate or accompany a flight in a working capacity. Maps to `CrewMember`.\n- **Rank** — Role / seniority code: `CA` Captain, `FO` First Officer, `FA` Flight Attendant, `PU` Purser, `SO` Second Officer. Airline-conventional. Maps to `CrewMember.rank`.\n- **Crew Base / Home Base / Domicile** — The airport a crew member is contractually based at; pairings begin and end here, and TAFB is measured against it. The three terms are interchangeable. Maps to `CrewMember.home_base` (IATA code).\n- **Union Code** — The crew member's union (e.g. ALPA, APFA, AFA-CWA, APA). Can affect hotel-tier entitlements and per-diem rules at some carriers. Maps to `CrewMember.union_code`.\n\n### Schedule structure\n\nHierarchy, largest to smallest: **Roster** (a crew member's monthly schedule) → **Pairings** (multi-day trips) → **Duty Periods** (one operational day each) → **Legs** (individual flight segments).\n\n- **Schedule / Roster** — The published assignment of work to crew over a planning horizon (typically a month). Maps to what you submit at `POST /schedules` and the `Schedule` resource Routespring builds from it.\n- **Pairing** — The atomic unit of crew scheduling: a sequence of duty periods that originates and terminates at the same domicile, typically 1–4 days, separated by layovers. Maps to `Pairing`; identified by `pairing_number`.\n- **Duty Period (FDP — Flight Duty Period)** — Roughly one working day: begins at sign-in (~60–90 min before the first flight) and ends after the last flight. Numbered sequentially within a pairing. Maps to `DutyPeriod` (`duty_period_num`, `duty_date`, `fdp_start_local`, `fdp_end_local`).\n- **Leg (also: Segment)** — One movement of one aircraft from one airport to another. Numbered within a duty period (`leg_sequence`). Maps to `Leg`.\n\n### Activity codes\n\nA leg's `activity_code` says what the crew is doing during that leg (industry-conventional, not regulated):\n\n| Code | Meaning | Counts as duty? |\n|------|---------|-----------------|\n| `FLT` | Revenue flight — crew is operating | Yes |\n| `DH`  | Deadhead — see below | Yes (duty, not rest) |\n| `TRG` | Training (sim, ground school, line check) | Yes |\n| `OFF` | Scheduled day off | No |\n| `RSV` | Reserve — crew on call for assignment | Yes |\n| `SBY` | Standby — available for immediate call-out | Yes |\n| `SL`  | Sick leave | No |\n| `RST` | Rest period at layover hotel | No |\n\n- **Deadhead (DH)** — Transportation of a crew member as a passenger to position them for an upcoming duty. Deadhead time counts as **duty, not rest** — it eats into the crew's daily duty budget, which is what makes positioning expensive and worth automating. **This is the FLIGHT booking category in this API.**\n\n### Time / duty / rest\n\n- **Block Time (also: Block Hours)** — Ramp-blocks-out to ramp-blocks-in; most pilots' pay clock, always less than the surrounding FDP. Maps to `Leg.block_hours`.\n- **FDP Start / End** — The times that bound a duty period; used by the engine to tell on-duty from scheduled rest (hotel check-in timing, rest minimums). Maps to `DutyPeriod.fdp_start_local` / `fdp_end_local`.\n- **Layover** — A scheduled rest period at an out-station between two duty periods of the same pairing; the crew is off duty and entitled to hotel accommodation. Maps to `DutyPeriod.layover`.\n- **TAFB — Time Away From Base** — Total wall-clock duration of a pairing, measured from pairing check-in to pairing check-out — including all duty periods, layovers, and deadhead positioning. Used for per-diem and pairing-bid preferences. Maps to `Pairing.tafb_hours`.\n\n### Aircraft\n\n- **Aircraft Type / Tail** — *Type* is the model designator (`B777`, `A350`, `B737`; ICAO/IATA codes). *Tail* is the unique registration of one airframe (`A6-EBA`). Maps to `Leg.aircraft_type` / `Leg.aircraft_tail`.\n\n### Operations\n\n- **IROPS — Irregular Operations** — Disruptions to planned schedules caused by weather, mechanical issues, ATC delays, or other unforeseen events. Reach the engine via the airline-injection path and are treated as high-priority (`P1`/`P2`) — typically only hours to react.\n### Booking & supplier\n\n- **GDS — Global Distribution System** — The intermediary inventory and booking platform (e.g. Amadeus, Sabre, Travelport) through which Routespring searches and books hotels and flights. GDS hotels are validated against Routespring's inventory at config time — an unknown `property_id` returns `422`. `DIRECT` hotels bypass GDS and are managed by the airline directly.\n- **PNR — Passenger Name Record** — The airline reservation record created when a flight is ticketed. Routespring returns the `pnr`, `ticket_id`, `transaction_id`, and `trip_session_id` on a `FLIGHT` booking after the booking is confirmed and the status is refreshed via `GET /crew/bookings/{id}/status`.\n\n## Conceptual model\n\nFour conceptual domains:\n\n1. **Schedule ingestion** — the airline POSTs the *complete current state*\n   of all open pairings to `POST /schedules` (JSON). Routespring computes\n   the diff server-side against the prior submission and derives booking\n   action items.\n   **There is no delta mode** — always send everything you have open.\n2. **Booking configuration** — the airline defines per-base hotel\n   preferences, flight preferences, and flight rules. PUTs\n   are idempotent and *versioned*; every booking snapshots the active\n   config version (see `Booking.config_version_used`) so historical\n   bookings can be replayed against the exact rules that were in force.\n3. **Booking engine** — Routespring processes action items asynchronously,\n   applying configuration to decide between auto-booking and human review.\n4. **Retrieval & management** — the airline queries bookings by any\n   anchor key, and modifies / cancels bookings out-of-band.\n\n## Schedule-driven vs. IROP booking\n\nAction items reach the booking engine via **two distinct paths**. Both\nproduce the same downstream artifacts: action items with the same\nclassification taxonomy, bookings with the same lifecycle, the same\nretry semantics.\n\n| Path | When to use | Endpoint(s) |\n|---|---|---|\n| **Schedule diff** | Normal flow — airline publishes the roster, Routespring derives every needed booking from the diff against the prior submission | `POST /schedules` |\n| **Airline injection** | IROPs and ad-hoc requirements where the airline already knows what's needed and the roster system hasn't caught up | `POST /action-items` (let engine search + auto-book) or `POST /bookings/request` (airline already picked the option) |\n\nUse `Booking.source` and `ActionItem.source` (`SCHEDULE_DIFF` /\n`AIRLINE_INJECTED` / `IROP`) to distinguish the origin downstream.\n\n## Anchor keys & ID model\n\nCrew, pairing, and leg identity all come from **the airline's own\nidentifiers** — Routespring does not mint a parallel `crew_id` /\n`pairing_id` / `duty_id` / `leg_id`. Every booking record is denormalized\nwith these keys so retrieval by any of them is a single indexed lookup:\n\n| Anchor | Issued by | Notes |\n|---|---|---|\n| `employee_id` | Airline | Your HR system's employee id — the crew anchor |\n| `pairing_number` | Airline | e.g. `MX-20260527-4821` |\n| `flight_number` | Airline | e.g. `MX401` |\n| `irop_ref` | Airline | IROP correlation id for IROP-injected artifacts |\n| `schedule_id` | Routespring | The submission id returned by `POST /schedules` |\n\nQuery `GET /bookings?<any-key>=<value>` and get the same record set\nregardless of which anchor you pass.\n\n## Tracking async outcomes\n\n`POST /schedules` returns `202 Accepted` immediately. Outcomes surface\nasynchronously at **three levels** — track all three:\n\n```\n┌──────────────────────────────────────────────────────────────────┐\n│  1. Submission level — ProcessingStatus                          │\n│     QUEUED → PROCESSING → BOOKING → COMPLETED                    │\n│                                  └─ or FAILED (file unparseable) │\n│                                                                  │\n│  2. Action item level — ActionItem.disposition                   │\n│     AUTO_BOOKED | AUTO_CANCELLED | PENDING_REVIEW | FAILED        │\n│                                                                  │\n│  3. Booking level — Booking.status                               │\n│     PENDING → CONFIRMED → MODIFIED → CANCELLED                   │\n│             └─ or FAILED / NEEDS_REVIEW                          │\n└──────────────────────────────────────────────────────────────────┘\n```\n\n`ProcessingStatus: COMPLETED` does **not** imply every action succeeded —\nindividual bookings can still be `FAILED`. Always check the booking\nlevel too. The schedule-level `summary.action_items_*` counts give you\nthe breakdown at a glance.\n\n### Disposition vs status vs auto-eligibility\n\nThree closely-related fields that confuse first-time readers:\n\n- `ActionItem.disposition` (`AUTO_BOOKED` / `AUTO_CANCELLED` / `PENDING_REVIEW`\n  / `FAILED`) — what the engine **decided** to do with the action.\n- `ActionItem.auto_eligible` (boolean) — whether the action passed all\n  auto-book *eligibility* checks (preferred vendor available, within\n  `auto_book_max_price`, etc.). **Independent of\n  supplier success.** An action can be `auto_eligible: true` but still\n  end up `disposition: FAILED` if the supplier call failed — a retry\n  could succeed. Use this to distinguish \"ineligible, don't retry\" from\n  \"eligible but supplier failed, retry might work\".\n- `Booking.status` — the booking's **lifecycle state** at the supplier.\n\n## Manual bookings & override rules\n\nAny of these actions sets `Booking.manual_override = true` automatically:\n`PATCH /bookings/{id}`, `DELETE /bookings/{id}`, `POST /bookings/request`,\n`POST /action-items`. Once `manual_override` is true, **the diff engine\nwill NOT modify or cancel this booking on subsequent schedule\nsubmissions**, even if the upstream pairing changes.\n\nThis prevents the diff engine from silently reverting a deliberate\nmanual change (e.g. an IROP rebooking made before the catch-up roster\narrives). To release a booking back to automatic management, `PATCH`\nwith `{\"manual_override\": false}` — the diff engine resumes ownership\non the next submission.\n\nA cancelled booking whose pairing later reappears in a schedule will\n**not** auto-recreate — re-issue explicitly.\n\n## Action item classifications\n\n`ActionItem.classification` describes **why** an action exists. The\ntaxonomy (new values may be added in future versions — handle unknown values gracefully):\n\n| Group | Values |\n|---|---|\n| **Hotel — diff-derived** | `NEW_LAYOVER`, `LAYOVER_DATE_CHANGE`, `LAYOVER_CITY_CHANGE`, `LAYOVER_CANCELLED`, `LAYOVER_EXTENDED` |\n| **Flight — diff-derived** | `NEW_DEADHEAD`, `DEADHEAD_TIME_CHANGE`, `DEADHEAD_CANCELLED` |\n| **Pairing-level** | `PAIRING_NEW`, `PAIRING_EXTENDED`, `PAIRING_SHORTENED`, `PAIRING_CANCELLED`, `SWAP_OUT`, `SWAP_IN`, `STATUS_TRANSITION` |\n| **IROP — airline-injected** | `IROP_NEW_DEADHEAD`, `IROP_REROUTE`, `IROP_HOTEL_EXTEND`, `IROP_HOTEL_CANCEL` |\n| **Data quality** | `DATA_QUALITY_FLAG`, `DATA_QUALITY_QUARANTINE`, `DUPLICATE_ROW` |\n| **No-op** | `UNCHANGED` (only when the Routespring-managed `emit_nop_tasks` toggle is on — off by default in v1, not set via this API) |\n\n**v1 diff-derived subset.** A `POST /schedules` diff in v1 derives the\nhotel, deadhead, swap, and data-quality classifications — `NEW_LAYOVER`,\n`LAYOVER_DATE_CHANGE`, `LAYOVER_CITY_CHANGE`, `LAYOVER_EXTENDED`,\n`LAYOVER_CANCELLED`, `NEW_DEADHEAD`, `DEADHEAD_TIME_CHANGE`,\n`DEADHEAD_CANCELLED`, `SWAP_OUT`, `SWAP_IN`, `DUPLICATE_ROW`,\n`DATA_QUALITY_FLAG`, and `UNCHANGED`. Pairing-level\nclassifications are not emitted by the diff path. The `IROP_*`\nvalues are airline-supplied on\n`POST /action-items` / `POST /bookings/request` and are accepted in v1.\n\n## Priority\n\n`ActionItem.priority` drives queue ordering and human-review SLAs:\n\n| Priority | Typical use | Target time to disposition |\n|---|---|---|\n| `P1` | IROPs requiring immediate action — flights departing in hours | ~5 minutes |\n| `P2` | Same-day pairing changes; default for airline injections | ~30 minutes |\n| `P3` | Routine layover bookings several days out | within the hour |\n| `P4` | Backfills, no-op annotations, low-urgency cleanup | next batch window |\n\nAirline injections (`POST /action-items`) default to `P2`. Bump to `P1`\nfor IROPs with tight SLAs.\n\n**v1:** `priority` is recorded on every action item, but review-SLA\nenforcement is not active — the target times above are advisory.\n\n## Authentication\n\nAll endpoints require `Authorization: Bearer <access_token>`. The token is\nobtained via an OAuth2 `client_credentials` flow on the Routespring auth\nservice — see Quickstart §0 (\"Get an access token\") for the full sequence:\n\n- **One-time:** register an OAuth2 client (or receive credentials at\n  onboarding) → `client_id` + `client_secret`.\n- **`POST /oauth/token`** — Basic `client_id:client_secret`,\n  `grant_type=client_credentials` → a short-lived JWT `access_token`\n  (`expires_in` ≈ 15 min) plus a `refresh_token`.\n- **`POST /oauth/refresh`** — exchange the `refresh_token` for a new\n  access token when it expires.\n\nThe access token resolves to the calling airline (tenant); there is no\nairline id in the URL path — every request is scoped to the company the\ntoken belongs to. An expired token returns `401 UNAUTHORIZED`.\n","contact":{"name":"Routespring Engineering","email":"api@routespring.com"}},"servers":[{"url":"https://sandbox.routespring.com/api/v1","description":"Sandbox (gateway host + /api/v1). Crew endpoints are under /crew, the hotel directory under /hotels."}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Schedules","description":"The Schedules APIs are where every booking workflow begins. Submit your\nairline's current roster as a complete snapshot — Routespring compares it\nagainst the bookings that already exist in the system and derives everything\nthat needs to happen: new bookings, cancellations, modifications, and crew\nswaps.\n\nYou do not send deltas. Every submission is the full picture of your open\npairings for a given date window. Routespring handles the reconciliation\nserver-side, so your integration stays simple — re-export from your crew\nmanagement system and POST.\n\nProcessing is asynchronous. The submit returns immediately with a\n`schedule_id`; use the processing-status endpoint to track progress. The\nbooking decisions Routespring derives are surfaced as Action Items — see the\nAction Items section for how to retrieve and act on them.\n"},{"name":"Exports","description":"Downloadable artifacts (output workbook, actions CSV, record mapping, summary, source file)"},{"name":"Configuration","description":"Booking configuration (hotels, flights, rules)"},{"name":"Bookings","description":"Booking retrieval and lifecycle management"},{"name":"Retries","description":"Manual and bulk retries for failed bookings"},{"name":"Action Items","description":"An action item is the engine's decision record — what Routespring determined\nneeds to happen after comparing your roster against existing bookings. Before\nany supplier call is made, the engine creates an action item for each required\nchange: book a new hotel, cancel a layover, extend a stay, position a crew\nmember on a deadhead flight.\n\nAction items are created two ways:\n\n- **From a schedule submission** — automatically derived when you POST a\n  roster. The engine classifies each change (NEW_LAYOVER, DEADHEAD_CANCELLED,\n  LAYOVER_EXTENDED, etc.) and decides whether to auto-book or hold for review.\n- **Injected outside the roster** — for IROPs and requirements that exist\n  operationally but are not yet reflected in your roster system. Use\n  `POST /crew/action-items` to tell Routespring what's needed and let the\n  engine handle supplier selection.\n\nEvery action item has a disposition:\n\n- `AUTO_BOOKED` — the engine booked it automatically. A booking record exists.\n- `AUTO_CANCELLED` — a `subtype: CANCEL` action the engine cancelled\n  automatically with the supplier; no human action needed.\n- `PENDING_REVIEW` — human action required. Check `recommended_action` for\n  what to do next.\n- `FAILED` — the supplier call was attempted but failed. Use the retry\n  endpoints to recover.\n\nBoth paths produce the same downstream artifact — action items flow through\nthe same booking engine, apply the same configuration, and produce the same\nbooking records.\n"},{"name":"Audit","description":"Data quality audit events"},{"name":"Hotels","description":"Global lookup of Routespring's GDS hotel inventory"}],"paths":{"/crew/schedules":{"post":{"tags":["Schedules"],"summary":"Submit a crew schedule","description":"Push the airline's roster to Routespring as a JSON payload. Each\nsubmission represents the **complete current state** of all open\npairings — Routespring computes the diff against the prior\nsubmission server-side and derives action items accordingly.\n\nReturns `202` immediately with a `schedule_id`; diffing,\nclassification, and booking run asynchronously. Track outcomes\nat the three async levels described in the **Async outcome\nmodel** section of the API overview. Briefly: a submission of\n200 duty periods where 3 individual hotel bookings fail surfaces\nas `ProcessingStatus: COMPLETED` plus 197 `CONFIRMED` and 3\n`FAILED` bookings, each retrievable individually.\n\nJSON is the only ingestion format: the airline-workflow anchors\n(`pairing_number`, `employee_id`, …) flow through as structured\nfields tied to your operational vocabulary.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleSubmission"},"examples":{"full":{"$ref":"#/components/examples/ScheduleSubmissionFull"}}}}},"responses":{"202":{"description":"Accepted for processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleAccepted"},"example":{"schedule_id":"sch_01HMWX9N1Q4ZJ8P7AKDQRT4F9M","external_ref":"NB-EXPORT-2026-05-15T14:30Z","processing_status":"QUEUED","received_at":"2026-05-15T14:32:11Z","links":{"status":"/v1/crew/schedules/sch_01HMWX9N1Q4ZJ8P7AKDQRT4F9M/processing-status","action_items":"/v1/crew/schedules/sch_01HMWX9N1Q4ZJ8P7AKDQRT4F9M/action-items"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/Unprocessable"}}},"get":{"tags":["Schedules"],"summary":"List schedule submissions","operationId":"listSchedules","description":"Returns a paginated list of schedule submissions for your airline,\nordered by submission time descending (most recent first). Use this\nto look up a `schedule_id` when you don't have it from the original\nsubmit response, or to audit submission history.\n\nNo filter is required — omitting all parameters returns the most\nrecent 50 submissions.\n","parameters":[{"in":"query","name":"from","schema":{"type":"string","format":"date"},"description":"Filter schedules received on/after this date."},{"in":"query","name":"to","schema":{"type":"string","format":"date"},"description":"Filter schedules received on/before this date."},{"in":"query","name":"status","schema":{"$ref":"#/components/schemas/ProcessingStatusEnum"},"description":"Filter by processing status. Useful for finding submissions still\nin progress (`QUEUED`, `PROCESSING`, `BOOKING`) or those that\nfailed (`FAILED`).\n"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleListResponse"},"example":{"schedules":[{"schedule_id":"sch_01HMWX9N1Q4ZJ8P7AKDQRT4F9M","external_ref":"NB-EXPORT-2026-05-15T14:30Z","processing_status":"COMPLETED","received_at":"2026-05-15T14:32:11Z","started_at":"2026-05-15T14:32:13Z","completed_at":"2026-05-15T14:32:48Z","links":{"status":"/v1/crew/schedules/sch_01HMWX9N1Q4ZJ8P7AKDQRT4F9M/processing-status","action_items":"/v1/crew/schedules/sch_01HMWX9N1Q4ZJ8P7AKDQRT4F9M/action-items"},"summary":{"action_items_total":67,"action_items_auto_booked":54,"action_items_auto_cancelled":0,"action_items_pending_review":13,"action_items_failed":0,"audit_events":4}},{"schedule_id":"sch_01HMWX8K2P3TL7N4BKDQRS3E8L","external_ref":"NB-EXPORT-2026-05-14T09:15Z","processing_status":"COMPLETED","received_at":"2026-05-14T09:18:42Z","started_at":"2026-05-14T09:18:44Z","completed_at":"2026-05-14T09:19:21Z","links":{"status":"/v1/crew/schedules/sch_01HMWX8K2P3TL7N4BKDQRS3E8L/processing-status","action_items":"/v1/crew/schedules/sch_01HMWX8K2P3TL7N4BKDQRS3E8L/action-items"},"summary":{"action_items_total":12,"action_items_auto_booked":10,"action_items_auto_cancelled":0,"action_items_pending_review":2,"action_items_failed":0,"audit_events":1}}],"next_cursor":null,"total_results":2}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/crew/schedules/{schedule_id}":{"get":{"tags":["Schedules"],"summary":"Get schedule metadata + summary","description":"Returns the full metadata and processing summary for a single schedule\nsubmission. Use this to check the outcome of a submission — how action\nitems were distributed across auto-booked, pending review, and failed.\n","parameters":[{"$ref":"#/components/parameters/ScheduleIdPath"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleDetail"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/crew/schedules/{schedule_id}/processing-status":{"get":{"tags":["Schedules"],"summary":"Poll processing status","parameters":[{"$ref":"#/components/parameters/ScheduleIdPath"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProcessingStatus"},"example":{"schedule_id":"sch_01HMWX9N1Q4ZJ8P7AKDQRT4F9M","status":"COMPLETED","received_at":"2026-05-15T14:32:11Z","started_at":"2026-05-15T14:32:13Z","completed_at":"2026-05-15T14:32:48Z","summary":{"action_items_total":67,"action_items_auto_booked":54,"action_items_auto_cancelled":0,"action_items_pending_review":13,"audit_events":4}}}}}}}},"/crew/schedules/{schedule_id}/action-items":{"get":{"tags":["Action Items"],"summary":"List action items derived from a schedule","description":"Returns the action items Routespring derived from this schedule\nsubmission. Each item represents one booking decision — what the engine\ndetermined needed to happen and whether it acted automatically or held\nit for review.\n\nCall this after polling `processing-status` until `poll_recommended`\nis `false`. Filter by `disposition: PENDING_REVIEW` to find items\nrequiring human action, or by `type: FLIGHT` to see all deadhead\npositioning needs.\n","parameters":[{"$ref":"#/components/parameters/ScheduleIdPath"},{"in":"query","name":"type","schema":{"type":"string","enum":["HOTEL","FLIGHT"]},"description":"Filter by booking type — `HOTEL` for layover bookings, `FLIGHT` for deadhead positioning."},{"in":"query","name":"subtype","schema":{"type":"string","enum":["NEW_BOOKING","MODIFY","CANCEL"]},"description":"Filter by the nature of the change — `NEW_BOOKING`, `MODIFY`, or `CANCEL`."},{"in":"query","name":"priority","schema":{"type":"string","enum":["P1","P2","P3","P4"]},"description":"Filter by urgency — `P1` for IROPs requiring immediate action through to `P4` for low-urgency backfills."},{"in":"query","name":"disposition","schema":{"type":"string","enum":["AUTO_BOOKED","AUTO_CANCELLED","PENDING_REVIEW","FAILED"]},"description":"Filter by outcome — `AUTO_BOOKED` / `AUTO_CANCELLED` for items the engine handled automatically, `PENDING_REVIEW` to find items requiring human action, `FAILED` to find items needing retry."},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionItemListResponse"},"example":{"action_items":[{"action_id":"act_01HMWX9TG3FJ8K9V2N1QS7HRPM","schedule_id":"sch_01HMWX9N1Q4ZJ8P7AKDQRT4F9M","source":"SCHEDULE_DIFF","type":"HOTEL","subtype":"NEW_BOOKING","classification":"NEW_LAYOVER","priority":"P3","disposition":"AUTO_BOOKED","auto_eligible":true,"anchors":{"employee_id":"EMP-1042","pairing_number":"MX-20260527-4821","duty_date":"2026-05-27"},"source_rows":{"day1_row":null,"day2_row":3},"booking_id":"bkg_01HMWXA52N0SQ8FRPDPK7RZ44V","created_at":"2026-05-27T08:00:45Z"},{"action_id":"act_01HMWX9TG3FJ8K9V2N1QS8HRPN","schedule_id":"sch_01HMWX9N1Q4ZJ8P7AKDQRT4F9M","source":"SCHEDULE_DIFF","type":"FLIGHT","subtype":"NEW_BOOKING","classification":"NEW_DEADHEAD","priority":"P3","disposition":"AUTO_BOOKED","auto_eligible":true,"anchors":{"employee_id":"EMP-1042","pairing_number":"MX-20260527-4821","duty_date":"2026-05-28"},"source_rows":{"day1_row":null,"day2_row":7},"booking_id":"bkg_01HMWXA6F3PK9V2N1QS8HRPN","flight_options_url":"/flight-options?from_airport=TUL&to_airport=DAL&date=2026-05-28&employee_id=EMP-1042","created_at":"2026-05-27T08:00:46Z"}],"next_cursor":null}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/crew/schedules/{schedule_id}/audit-events":{"get":{"tags":["Audit"],"summary":"List data-quality audit events","parameters":[{"$ref":"#/components/parameters/ScheduleIdPath"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditEventListResponse"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/crew/schedules/{schedule_id}/actions.csv":{"get":{"tags":["Exports"],"summary":"Download action items as CSV","description":"Flat CSV of every action item from this schedule submission. One\nrow per action. Machine-readable; suitable for ingestion into\nairline-side BI / analytics systems.\n","parameters":[{"$ref":"#/components/parameters/ScheduleIdPath"}],"responses":{"200":{"description":"OK","content":{"text/csv":{"schema":{"type":"string"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/crew/action-items":{"post":{"tags":["Action Items"],"summary":"Inject an action item (IROP / out-of-schedule requirement)","operationId":"injectActionItem","description":"The airline already knows what booking is required and asks\nRoutespring to run it through the booking engine — searching the\nsupplier pool, applying the active configuration, and either\nauto-booking or flagging for review per the same rules that\ngovern diff-derived action items.\n\n**Primary use case — IROPs.** When operations diverges from the\npublished roster (weather diversion, sick crew, cascading delay),\nthe airline computes the new booking requirement before your crew\nmanagement system catches up. Calling this endpoint\nwith `source: IROP` and an `irop_ref` correlates every action\nitem, booking, and audit event for that IROP for after-action\nanalysis.\n\n**When to use vs alternatives:**\n* Diff-derived (normal flow): just `POST /schedules`. Routespring\n  computes the action items.\n* Airline has *not* selected a specific flight, wants Routespring\n  to search + auto-book per config: **this endpoint**.\n* Airline *has* selected a specific flight (via `GET /flight-options`)\n  and wants to commit it: `POST /bookings/request` instead — that\n  path skips the search step and goes straight to the supplier.\n\nReturns the created `ActionItem` immediately (`202`).\n\n**Execution (v1).** Both **HOTEL** and **FLIGHT** injections are\nauto-booked asynchronously through the same engine the schedule flow\nuses (supplier search + selection + rate/fare from config); poll\n`GET /bookings/{booking_id}/status` for the outcome. `POST /action-items`\ntakes no `flight_option_id` — the engine searches and selects the\npositioning flight for you. (If you have already chosen a specific\nflight via `GET /flight-options` and want to commit that exact one,\nuse `POST /bookings/request` instead.) Resulting `Booking.manual_override`\ndefaults to `true` so a subsequent schedule submission's diff engine does\nnot revert the injected booking.\n\n**Idempotency (optional).** Send an `Idempotency-Key` header to make\nretries safe: a repeated key for the same airline replays the original\naction item instead of injecting (and booking) a second time.\n","parameters":[{"in":"header","name":"Idempotency-Key","required":false,"schema":{"type":"string"},"description":"Optional. A repeated key for the same airline replays the original action item instead of injecting a second time."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionItemInjection"},"examples":{"irop-deadhead":{"summary":"IROP — crew needs new deadhead positioning","value":{"type":"FLIGHT","classification":"IROP_NEW_DEADHEAD","priority":"P1","source":"IROP","irop_ref":"IROP-2026-05-18-EWR-001","anchors":{"traveller_email":"jordan.reyes@flymx.com","employee_id":"EMP-12047","pairing_number":"P-23845"},"flight":{"from_airport":"EWR","to_airport":"SLC","depart_date":"2026-05-18","arrive_by":"2026-05-18T14:00:00Z"},"notes":"Original positioning leg cancelled due to ATC ground stop at EWR."}},"irop-hotel-extension":{"summary":"IROP — extend layover hotel by one night","value":{"type":"HOTEL","classification":"IROP_HOTEL_EXTEND","priority":"P2","source":"IROP","irop_ref":"IROP-2026-05-18-EWR-001","anchors":{"traveller_email":"jordan.reyes@flymx.com","employee_id":"EMP-12047","pairing_number":"P-23845"},"hotel":{"city":"SLC","check_in_date":"2026-05-18","check_out_date":"2026-05-19"},"notes":"Pairing extended; existing hotel needs additional night."}}}}}},"responses":{"202":{"description":"Accepted — action item created; booking pipeline running asynchronously","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionItem"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/Unprocessable"}}}},"/crew/config/hotels":{"get":{"tags":["Configuration"],"summary":"Get hotel configuration (per-base preferred hotels + contract rates)","description":"Returns your airline's active hotel configuration — the preferred hotel per\ncrew base, with its contract rate and check-in/out policy. The booking engine\nuses the hotel configured for the crew member's layover city, matching against\nthe contracted rate.\n\nTo retrieve a historical snapshot, use\n`GET /crew/config/hotels/versions/{version}`.\n","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HotelConfig"},"example":{"bases":[{"base_station":"SLC","hotels":[{"property_id":"hilton-slc-airport","chain":"HILTON","policy":"STANDARD","standard_check_in_time":"15:00","standard_check_out_time":"11:00","buffer_minutes":60,"contract_rate":{"amount":145,"currency":"USD"},"rate_periods":[{"effective_from":"2026-01-01","effective_to":"2026-06-30","rate":{"amount":149,"currency":"USD"}},{"effective_from":"2026-07-01","effective_to":"2026-12-31","rate":{"amount":169,"currency":"USD"}}]}]},{"base_station":"DXB","hotels":[{"property_id":"jw-marriott-dxb","chain":"MARRIOTT","policy":"GUARANTEED","guaranteed_early_check_in_time":"10:00","guaranteed_late_check_out_time":"14:00","buffer_minutes":30,"contract_rate":{"amount":220,"currency":"USD"}}]}],"version":"v-2026-05-15-001","active":true,"updated_at":"2026-05-15T10:32:00Z"}}}}}},"put":{"tags":["Configuration"],"summary":"Replace hotel configuration (idempotent, creates new version)","description":"Replaces your airline's hotel configuration. The full list of bases and\nhotels must be sent — this is a full replacement, not a merge. **One hotel\nper base.** The previous config is archived as an immutable version; every\nbooking records the config version active at the time it was made.\n\nThe booking channel is **derived** from each `property_id`: a match in\nRoutespring's GDS inventory (look it up via `GET /hotels`) books via `GDS`;\na `property_id` that is **not** in the inventory is a `DIRECT` hotel only\nwhen it supplies its own `chain`. A non-inventory `property_id` with no\n`chain` is treated as a mistyped GDS id and rejected with\n`422 UNKNOWN_HOTEL_PROPERTY`. `handler_type` is therefore not sent on\ninput — it is returned (derived) on the config response.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HotelConfigInput"},"example":{"bases":[{"base_station":"SLC","hotels":[{"property_id":"hilton-slc-airport","policy":"STANDARD","standard_check_in_time":"15:00","standard_check_out_time":"11:00","buffer_minutes":60,"contract_rate":{"amount":145,"currency":"USD"},"rate_periods":[{"effective_from":"2026-01-01","effective_to":"2026-06-30","rate":{"amount":149,"currency":"USD"}},{"effective_from":"2026-07-01","effective_to":"2026-12-31","rate":{"amount":169,"currency":"USD"}}]}]},{"base_station":"DXB","hotels":[{"property_id":"jw-marriott-dxb","chain":"MARRIOTT","policy":"GUARANTEED","guaranteed_early_check_in_time":"10:00","guaranteed_late_check_out_time":"14:00","buffer_minutes":30,"contract_rate":{"amount":220,"currency":"USD"}}]}]}}}},"responses":{"200":{"description":"Replaced","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HotelConfig"},"example":{"bases":[{"base_station":"SLC","hotels":[{"property_id":"hilton-slc-airport","chain":"HILTON","policy":"STANDARD","standard_check_in_time":"15:00","standard_check_out_time":"11:00","buffer_minutes":60,"contract_rate":{"amount":145,"currency":"USD"},"rate_periods":[{"effective_from":"2026-01-01","effective_to":"2026-06-30","rate":{"amount":149,"currency":"USD"}},{"effective_from":"2026-07-01","effective_to":"2026-12-31","rate":{"amount":169,"currency":"USD"}}]}]},{"base_station":"DXB","hotels":[{"property_id":"jw-marriott-dxb","chain":"MARRIOTT","policy":"GUARANTEED","guaranteed_early_check_in_time":"10:00","guaranteed_late_check_out_time":"14:00","buffer_minutes":30,"contract_rate":{"amount":220,"currency":"USD"}}]}],"version":"v-2026-05-16-001","active":true,"updated_at":"2026-05-16T09:15:00Z"}}}}}}},"/crew/config/flights":{"get":{"tags":["Configuration"],"summary":"Get flight configuration (preferred / blacklisted airlines, cabin)","description":"Returns your airline's carrier and cabin preferences for deadhead\npositioning flights. The booking engine applies these when searching\nfor and selecting flights on behalf of crew.\n","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlightConfig"},"example":{"preferred_airlines":["AA","DL","UA","EK"],"blacklisted_airlines":["F9"],"preferred_cabin":"ECONOMY","same_carrier_preference":true,"version":"v-2026-05-15-001","updated_at":"2026-05-15T10:36:00Z"}}}}}},"put":{"tags":["Configuration"],"summary":"Replace flight configuration","description":"Sets your airline's carrier and cabin preferences for deadhead positioning\nflights. Blacklisted carriers are never booked. Preferred carriers are tried\nfirst; if none have availability, the engine considers any non-blacklisted\noption.\n\nSet `same_carrier_preference: true` to prioritize your own airline's flights\nwhen repositioning crew.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlightConfigInput"},"example":{"preferred_airlines":["AA","DL","UA","EK"],"blacklisted_airlines":["F9"],"preferred_cabin":"ECONOMY","same_carrier_preference":true}}}},"responses":{"200":{"description":"Replaced","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlightConfig"},"example":{"preferred_airlines":["AA","DL","UA","EK"],"blacklisted_airlines":["F9"],"preferred_cabin":"ECONOMY","same_carrier_preference":true,"version":"v-2026-05-16-001","updated_at":"2026-05-16T09:20:00Z"}}}}}}},"/crew/config/flight-rules":{"get":{"tags":["Configuration"],"summary":"Get flight rules (auto-book price ceiling)","description":"Returns the price ceiling for auto-booking positioning (deadhead) flights.\n\nIf a booking lands in `NEEDS_REVIEW` unexpectedly, check `auto_book_max_price`\n— fares above this amount are held for travel manager approval rather than\nbooked automatically.\n","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlightRules"}}}}}},"put":{"tags":["Configuration"],"summary":"Replace flight rules","description":"`auto_book_max_price` is the per-ticket ceiling for auto-booking a\npositioning (deadhead) flight — fares above this are held as\n`NEEDS_REVIEW` for travel manager approval rather than booked automatically.\n\nOnly fields included in the request are updated; omitted fields retain their\ncurrent values.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlightRulesInput"},"example":{"auto_book_max_price":{"amount":800,"currency":"USD"}}}}},"responses":{"200":{"description":"Replaced","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlightRules"},"example":{"auto_book_max_price":{"amount":800,"currency":"USD"},"version":"v-2026-05-16-001","updated_at":"2026-05-16T09:22:00Z"}}}}}}},"/crew/config/company":{"get":{"tags":["Configuration"],"summary":"Get company crew-automation settings","description":"Returns your airline's automation settings — the flags that control what\nRoutespring executes automatically versus holds for human review, the account\nunder which engine-driven bookings are placed, and the rate matching mode that\ndetermines how strictly contracted rates are enforced at booking time.\n\nWhen an automation flag is disabled, Routespring creates an action item for\nyour team to handle instead of executing automatically. See the **Action Items**\nsection for how these are surfaced and managed.\n\nIf bookings are landing in `NEEDS_REVIEW` unexpectedly, or automated\ncancellations and modifications are not executing, check these settings first.\n","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrewCompanyConfig"},"example":{"crew_booker_email":"crew-ops@flymx.com","cancellation_lookback_days":0}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"put":{"tags":["Configuration"],"summary":"Set company crew-automation settings","description":"Sets the crew booker account and the booking preferences exposed here.\n`crew_booker_email` is required — all engine-driven bookings (hotel\nlayovers, diff-engine amendments and cancellations) are placed under this\naccount. Manual flight commits via `POST /bookings/request` are booked\nunder the bearer token making the call.\n\nOmit a field to keep its current value. The automation execution levers\n(cancellation / modification / swap / deadhead-flight execution, NOC stats)\nare Routespring-managed and not set through this API.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrewCompanyConfigInput"},"example":{"crew_booker_email":"crew-ops@flymx.com","cancellation_lookback_days":0}}}},"responses":{"200":{"description":"Saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrewCompanyConfig"},"example":{"crew_booker_email":"crew-ops@flymx.com","cancellation_lookback_days":0}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/Unprocessable"}}}},"/crew/config/{config_type}/versions/{version}":{"get":{"tags":["Configuration"],"summary":"Retrieve a specific historical config version","description":"Returns the immutable snapshot of the named config at the given\nversion. Use this when a booking's `config_version_used` references\na non-current version and you need to know exactly which rules\napplied.\n\nThe response shape depends on `config_type`: `hotels` returns a\n`HotelConfig`, `flights` returns a `FlightConfig`, etc.\n\n**Note:** only `version = v1` resolves; any other version\nreturns `404`.\n","parameters":[{"in":"path","name":"config_type","required":true,"schema":{"type":"string","enum":["hotels","flights","flight-rules"]}},{"in":"path","name":"version","required":true,"schema":{"type":"string"},"example":"v-2026-05-15-001"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/HotelConfig"},{"$ref":"#/components/schemas/FlightConfig"},{"$ref":"#/components/schemas/FlightRules"}]}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/crew/bookings":{"get":{"tags":["Bookings"],"summary":"Search bookings by any anchor key","description":"Bookings are denormalized with all anchor keys, so any combination of\nthe query parameters below returns the same booking record set.\n\nAt least one filter is required.\n","parameters":[{"in":"query","name":"schedule_id","schema":{"type":"string"}},{"in":"query","name":"employee_id","schema":{"type":"string"},"description":"Airline's HR employee id — the crew anchor."},{"in":"query","name":"pairing_number","schema":{"type":"string"}},{"in":"query","name":"flight_number","schema":{"type":"string"}},{"in":"query","name":"irop_ref","schema":{"type":"string"},"description":"Airline IROP correlation id. Returns every booking tied to a\nsingle IROP — useful for after-action cost reporting and\naudit. Set on bookings created via `POST /bookings/request`\nor `POST /action-items` with an `irop_ref`.\n"},{"in":"query","name":"source","schema":{"type":"string","enum":["SCHEDULE_DIFF","AIRLINE_INJECTED","IROP"]},"description":"How the booking originated. Filter for `AIRLINE_INJECTED` or `IROP` to find airline-injected bookings."},{"in":"query","name":"type","schema":{"type":"string","enum":["HOTEL","FLIGHT"]}},{"in":"query","name":"status","schema":{"$ref":"#/components/schemas/BookingStatusEnum"}},{"in":"query","name":"from_date","schema":{"type":"string","format":"date"}},{"in":"query","name":"to_date","schema":{"type":"string","format":"date"}},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingListResponse"}}}}}}},"/crew/flight-options":{"get":{"tags":["Bookings"],"summary":"Search available flight options for a deadhead leg","operationId":"searchFlightOptions","description":"Returns paginated flight options for a given origin/destination/date.\nUse this when you want to manually select a specific flight rather than\nletting Routespring auto-book via its built-in logic.\n\n**Default behavior (no call needed):** Routespring auto-selects and\nbooks the best available flight based on your booking configuration\n(preferred airlines, cabin, `auto_book_max_price`, etc.). You\nonly need this endpoint if you want a human to review and pick.\n\n**Typical IROP flow:**\n1. Call this endpoint to fetch options (10 per page by default).\n2. Present pages to the travel manager.\n3. POST the chosen `flight_option_id` to\n   `PATCH /bookings/{booking_id}` or to `POST /bookings/request`\n   to lock in that specific flight.\n\nResults are sourced from the same supplier pool Routespring uses for\nauto-booking. Prices and availability are live at query time.\n","parameters":[{"in":"query","name":"from_airport","schema":{"type":"string"},"description":"IATA departure airport code. Required unless `action_id` is supplied."},{"in":"query","name":"to_airport","schema":{"type":"string"},"description":"IATA arrival airport code. Required unless `action_id` is supplied."},{"in":"query","name":"date","schema":{"type":"string","format":"date"},"description":"Desired travel date (local at origin). Required unless `action_id` is supplied."},{"in":"query","name":"arrive_by","schema":{"type":"string","format":"date-time"},"description":"Latest acceptable arrival time (UTC). Filters out options that\narrive too late for the crew's next duty period start.\n"},{"in":"query","name":"action_id","schema":{"type":"string"},"description":"If provided, the route and date (`from_airport`, `to_airport`,\n`date`, `arrive_by`) are pre-filled from the named action item, so\nyou may omit those params. An unknown `action_id` returns `404`.\n"},{"in":"query","name":"employee_id","schema":{"type":"string"},"description":"Airline HR employee id. Accepted but not currently applied to\nresult filtering.\n"},{"in":"query","name":"limit","schema":{"type":"integer","default":10,"minimum":1,"maximum":50},"description":"Results per page. Defaults to 10."},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlightOptionsResponse"},"example":{"options":[{"flight_option_id":"of_01HN2X9N1Q4ZJ8P7AKDQRT4F9A","airline_code":"DL","flight_number":"DL1234","from_airport":"SLC","to_airport":"JFK","dep_time":"2026-05-16T06:00:00Z","arr_time":"2026-05-16T13:30:00Z","cabin_class":"ECONOMY","fare_name":"Main Cabin","price":{"amount":342,"currency":"USD"},"stops":0,"available_seats":4,"preferred_carrier":true},{"flight_option_id":"of_01HN2X9N1Q4ZJ8P7AKDQRT4F9B","airline_code":"UA","flight_number":"UA887","from_airport":"SLC","to_airport":"JFK","dep_time":"2026-05-16T08:15:00Z","arr_time":"2026-05-16T15:55:00Z","cabin_class":"ECONOMY","fare_name":"Economy Basic","price":{"amount":289,"currency":"USD"},"stops":1,"available_seats":2,"preferred_carrier":false}],"next_cursor":"eyJwYWdlIjoyfQ","total_results":23}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/crew/bookings/request":{"post":{"tags":["Bookings"],"summary":"Create a booking from an airline-supplied selection (IROP commit)","operationId":"requestBooking","description":"\"I've already decided what to book — just book it.\" Pairs with\n`GET /flight-options`: airline searches, picks an option, posts\nthe `flight_option_id` here.\n\n**Execution (v1).** A **HOTEL** request is booked for real,\nasynchronously, through the same engine the schedule flow uses\n(duplicate check → supplier selection in priority order → rate from\nconfig). The call returns `202`; poll `GET /bookings/{booking_id}/status`\nfor the outcome. A **FLIGHT** commit that carries a `flight_option_id` is\nbooked for real through Routespring's internal Flights API — it tickets and yields a PNR,\nticket number, and trip-session id; poll `GET /bookings/{booking_id}/status`,\nthen read those off the `flight` block via `GET /bookings/{booking_id}`.\nA **FLIGHT** commit that omits `flight_option_id` and instead carries a\nroute + date (`from_airport` + `to_airport` + `depart_date`) auto-selects\nand books the best matching flight — the same way a hotel commit\nauto-selects when `property_id` is omitted (see `BookingRequestFlight`).\n(Deadhead positioning flights derived from `POST /schedules` are booked\nautomatically by the engine and don't need this call at all.)\n\n**When to use vs alternatives:**\n* The airline wants Routespring to search + auto-book against its\n  config: use `POST /action-items` instead.\n* The booking already exists and needs to change: use\n  `PATCH /bookings/{booking_id}`.\n* Diff-derived (normal flow): just `POST /schedules`.\n\n**Typical IROP flow end-to-end:**\n1. Detect IROP in airline ops system.\n2. `GET /flight-options?from_airport=EWR&to_airport=SLC&arrive_by=...&employee_id=...`\n3. Travel manager picks an option (or your automation does).\n4. POST that `flight_option_id` here with the anchors and a\n   classification (`IROP_NEW_DEADHEAD`, `IROP_REROUTE`, …).\n\nBookings created here default to `manual_override: true` so the\neventual catch-up schedule submission's diff engine does not\nrevert this booking. Set `manual_override: false` to release\nimmediately to auto-management.\n\n**Idempotency (optional).** Send an `Idempotency-Key` header to make\nretries safe: a repeated key for the same airline returns the booking\nthe first call created instead of committing a second time.\n\nFor HOTEL bookings (where there is no `flight-options` analogue),\nthe airline supplies the `city` + `check_in_date` + `check_out_date`\nplus an optional `property_id` to pin a specific hotel from its\npreferred-hotels config. If `property_id` is omitted Routespring\nauto-selects per priority order — useful for hotel IROPs where\nthe airline just needs *any* preferred hotel locked in fast.\n","parameters":[{"in":"header","name":"Idempotency-Key","required":false,"schema":{"type":"string"},"description":"Optional. A repeated key for the same airline replays the original booking instead of committing a second time."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingRequestInput"},"examples":{"flight-from-option":{"summary":"Commit a flight selected from /flight-options","value":{"type":"FLIGHT","classification":"IROP_NEW_DEADHEAD","source":"IROP","irop_ref":"IROP-2026-05-18-EWR-001","anchors":{"traveller_email":"jordan.reyes@flymx.com","employee_id":"EMP-12047","pairing_number":"P-23845"},"flight":{"flight_option_id":"of_01HN2X9N1Q4ZJ8P7AKDQRT4F9A"}}},"hotel-pinned":{"summary":"Commit a hotel at a specific property","value":{"type":"HOTEL","classification":"IROP_HOTEL_EXTEND","source":"IROP","irop_ref":"IROP-2026-05-18-EWR-001","anchors":{"traveller_email":"jordan.reyes@flymx.com","employee_id":"EMP-12047","pairing_number":"P-23845"},"hotel":{"city":"SLC","property_id":"hilton-slc-airport","check_in_date":"2026-05-18","check_out_date":"2026-05-19"}}}}}}},"responses":{"202":{"description":"Accepted — booking created in `PENDING`; supplier call in flight","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"description":"Conflict — a booking already exists for the same anchors and\nbooking type (e.g. a hotel for the same crew + duty_date).\nUse `PATCH /bookings/{id}` to modify the existing booking\ninstead, or release it first with `manual_override: false`.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"422":{"$ref":"#/components/responses/Unprocessable"}}}},"/crew/bookings/{booking_id}":{"get":{"tags":["Bookings"],"summary":"Get booking detail","parameters":[{"$ref":"#/components/parameters/BookingIdPath"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Bookings"],"summary":"Modify a booking (date change, hotel swap, fare upgrade)","description":"Modify an existing booking. Routespring performs the supplier change\nand returns `202` with the booking record reflecting the outcome\n(`MODIFIED` on a successful amend, or `NEEDS_REVIEW` if the supplier\nrejected it).\n\n**How the change is applied.** A hotel **date-window** change\n(check-in / check-out) is applied as a true in-place supplier amend —\nthe `booking_id` and supplier confirmation are preserved and the\nbooking ends in `MODIFIED`. A hotel **property swap** or any\n**flight** change is applied as a cancel-and-rebook: the original\nbooking moves to `CANCELLED` and a new `CONFIRMED` booking is created.\n\n**Company levers (Routespring-managed, not set via this API in v1).**\nAn explicit PATCH always performs the requested amend — it is a\ndeliberate operator action and is **not** gated by the per-company\nbooking-modification execution lever (that lever governs only the\nautomatic schedule-diff path). The strict-price-match lever\nstill controls how strictly the amended rate must match the\ncontracted rate (a mismatch fails the amend in strict mode, or is\nflagged in loose mode). These are configured by Routespring during\nonboarding; there is no self-serve config surface for them. Valid\nfield combinations: send `hotel`\nfor HOTEL bookings and `flight` for FLIGHT bookings (matching the\nbooking's type); `manual_override` may be sent alone or alongside.\n\n**Side effect — `manual_override` becomes `true`** by default\nafter a successful PATCH. Once set, the diff engine will no\nlonger touch this booking on subsequent schedule submissions.\nTo explicitly release the booking back to automatic management,\nPATCH with `{ \"manual_override\": false }` (alone or alongside\nother changes).\n","parameters":[{"$ref":"#/components/parameters/BookingIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingModification"},"examples":{"hotel-date-change":{"value":{"reason":"PAIRING_EXTENDED","hotel":{"check_out_date":"2026-05-19"}}},"flight-time-change":{"value":{"reason":"CREW_PAIRING_CHANGE","flight":{"desired_arrival_by":"2026-05-19T07:00:00Z"}}},"release-to-auto":{"summary":"Release a previously-overridden booking back to auto","value":{"reason":"Manual override no longer needed","manual_override":false}}}}}},"responses":{"202":{"description":"Accepted for processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["Bookings"],"summary":"Cancel a booking","description":"Cancel a booking. Routespring resolves the underlying supplier\ntransaction and issues the cancellation, then returns `202` with the\nbooking reflecting the outcome: `CANCELLED` on success, or\n`NEEDS_REVIEW` if the supplier rejected the cancellation (the live\nticket/room is still out there and an operator must reconcile it).\nRe-issuing `DELETE` on a `NEEDS_REVIEW` booking retries the cancel;\na booking already `CANCELLED` is idempotent (no second supplier call).\n\n**Side effect — `manual_override` becomes `true`.** A cancelled\nbooking is locked from the diff engine regardless of the supplier\noutcome; if the underlying pairing later reappears, Routespring will\nNOT auto-create a replacement. The airline must explicitly re-issue a\nnew booking (e.g. by re-uploading the schedule with that pairing\nre-included).\n","parameters":[{"$ref":"#/components/parameters/BookingIdPath"},{"in":"query","name":"reason","schema":{"type":"string"},"description":"Free-text cancellation reason (recorded in audit log)"}],"responses":{"202":{"description":"Cancellation accepted (async fee-checking may apply)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/crew/bookings/{booking_id}/status":{"get":{"tags":["Bookings"],"summary":"Get booking status + state-transition history","parameters":[{"$ref":"#/components/parameters/BookingIdPath"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingStatusResponse"}}}}}}},"/crew/bookings/{booking_id}/retry":{"post":{"tags":["Retries"],"summary":"Manually retry a failed booking","description":"Re-attempt supplier booking for a single booking. The booking must\nbe in `FAILED` state — once a retry starts the booking moves to\n`PENDING`, so a re-POST while a retry is already in progress returns\n`409`. Each accepted retry increments `retry_count` on the booking.\nThe supplier call runs asynchronously; the response returns\nimmediately with the booking in `PENDING` state.\n","parameters":[{"$ref":"#/components/parameters/BookingIdPath"}],"responses":{"202":{"description":"Retry accepted; supplier call in flight","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"409":{"description":"Retry not applicable (booking not in FAILED state, or max retries already reached)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/crew/bookings/retry-queue":{"get":{"tags":["Retries"],"summary":"List bookings currently queued for retry","description":"Returns bookings that have failed and are scheduled for an\nautomatic retry attempt. Each entry shows the next scheduled\nattempt time and the prior error.\n","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetryQueueResponse"}}}}}}},"/crew/bookings/retry-jobs":{"get":{"tags":["Retries"],"summary":"List recent retry jobs","description":"Returns recent retry jobs plus their status (RUNNING / COMPLETED /\nFAILED) and aggregate counts.\n","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetryJobListResponse"}}}}}}},"/crew/bookings/retry-jobs/{retry_job_id}":{"get":{"tags":["Retries"],"summary":"Get a retry job's status and progress","parameters":[{"in":"path","name":"retry_job_id","required":true,"schema":{"type":"string"},"example":"rtj_01HMXX9N1Q4ZJ8P7AKDQRT4F9M"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetryJob"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/hotels":{"get":{"tags":["Hotels"],"summary":"Search the GDS hotel inventory by name, city, or airport code","operationId":"searchHotels","description":"Search Routespring's GDS hotel inventory by any combination\nof `name`, `city`, and `airport_code`. At least one filter must be\nsupplied.\n\nUse this to discover a hotel's `property_id` before registering it\nvia `PUT /crew/config/hotels`. A `property_id` found in this GDS\ninventory is booked via `GDS`; one that is not is treated as `DIRECT`.\n\n**How the filters compose.** `name` and `city` each run an\nindependent LIKE-search against `CONCAT(hotel_name, address, city)`;\n`airport_code` instead searches the hotels located nearby the given\nairport (a proximity search around the airport's location). Results\nare unioned, deduplicated on `property_id`, and sorted.\n\nThis is a **global** lookup: results are the same regardless of the\ncalling airline, so the path is not airline-scoped. Only hotels\nbookable through GDS are returned. A bearer credential is\nstill required.\n","parameters":[{"in":"query","name":"name","required":false,"schema":{"type":"string","minLength":2},"description":"Partial, case-insensitive hotel-name match. Min 2 chars when supplied."},{"in":"query","name":"city","required":false,"schema":{"type":"string","minLength":2},"description":"Partial, case-insensitive city-name match. Min 2 chars when supplied."},{"in":"query","name":"airport_code","required":false,"schema":{"type":"string","minLength":3,"maxLength":3},"description":"3-letter IATA airport code (e.g. `JFK`, `LAX`, `LHR`). Searches\nthe hotels located nearby the given airport.\n"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HotelSearchResponse"},"example":{"hotels":[{"property_id":"TVP-JFK-9001","name":"JFK GDS Grand","chain":"HILTON","city":"JFK","address":"144-02 135th Ave, Jamaica, NY"}],"next_cursor":null,"total_results":1}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"API-Key"}},"parameters":{"ScheduleIdPath":{"in":"path","name":"schedule_id","required":true,"schema":{"type":"string"},"example":"sch_01HMWX9N1Q4ZJ8P7AKDQRT4F9M"},"BookingIdPath":{"in":"path","name":"booking_id","required":true,"schema":{"type":"string"},"example":"bkg_01HMWXA52N0SQ8FRPDPK7RZ44V"},"Limit":{"in":"query","name":"limit","schema":{"type":"integer","default":50,"minimum":1,"maximum":200}},"Cursor":{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Opaque pagination cursor. Echo from previous response."},"offerId":{"name":"offerId","in":"path","required":true,"description":"Flight offer ID from search results","schema":{"type":"string","example":"off_c34680d61c37"}},"searchId":{"name":"searchId","in":"query","required":false,"description":"Search session ID (recommended for accurate pricing)","schema":{"type":"string","example":"srch_535f163a1d96"}},"bookingRequestId":{"name":"bookingRequestId","in":"path","required":true,"description":"Temporary booking request ID from the POST /bookings response.\nExpires after 24 hours — use tripSessionId for permanent lookups.\n","schema":{"type":"string","example":"bkreq_9f8e7d6c5b4a"}}},"responses":{"BadRequest":{"description":"Malformed request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"Unauthorized":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"Unprocessable":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"InternalError":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}},"schemas":{"ApiErrorResponse":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","example":"INVALID_REQUEST"},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"issue":{"type":"string"}}}}}},"status":{"type":"integer"}}},"Money":{"type":"object","properties":{"amount":{"type":"number","example":145},"currency":{"type":"string","example":"USD"}}},"ProcessingStatusEnum":{"type":"string","enum":["QUEUED","PROCESSING","BOOKING","COMPLETED","FAILED"],"description":"Lifecycle of a schedule submission:\n* `QUEUED`     — accepted, waiting for the diff engine to pick up.\n* `PROCESSING` — diffing + classifying (fast — seconds).\n* `BOOKING`    — diff complete; supplier calls in flight (slower — minutes).\n* `COMPLETED`  — all supplier calls have terminated (success or failure).\n* `FAILED`     — the submission could not be processed at all (e.g. malformed file).\nNote: `COMPLETED` does not imply every action succeeded — individual\nbookings can still be in `FAILED` state. See `summary.action_items_*`\nfor the breakdown and the retry endpoints for follow-up.\n"},"BookingStatusEnum":{"type":"string","enum":["PENDING","CONFIRMED","MODIFIED","CANCELLED","FAILED","NEEDS_REVIEW"],"description":"Lifecycle of a single booking:\n* `PENDING`      — supplier call in flight; awaiting confirmation.\n* `CONFIRMED`    — supplier confirmed; PNR / voucher issued.\n* `MODIFIED`     — booking amended in place after confirmation. Still effectively confirmed; the status reflects that an amend has been applied since the original `CONFIRMED` transition. v1 reaches `MODIFIED` for **hotel date-window changes** (check-in / check-out) and requires the airline's hotel-modification feature; property swaps and flight changes are applied as cancel-and-rebook and surface as `CANCELLED` plus a new `CONFIRMED` booking rather than `MODIFIED`.\n* `CANCELLED`    — booking cancelled (by `DELETE /bookings/{id}`, by retry abandonment after `max_retries`, or by supplier-side action).\n* `FAILED`       — supplier call returned an error and the booking is **not in place**. See `Booking.error_message` and the `/retry` endpoints. Distinct from `NEEDS_REVIEW`: a `FAILED` booking *was attempted*; a `NEEDS_REVIEW` booking has *not been attempted yet*.\n* `NEEDS_REVIEW` — engine held the booking for human action (no preferred vendor in config, price exceeds `auto_book_max_price`, etc.). The supplier call has not been made; a travel manager must intervene — typically via `PATCH /bookings/{id}` after resolving the underlying issue.\n\nBookings created via `POST /bookings/request` typically start at\n`PENDING` and transition to `CONFIRMED` (or `FAILED`). Bookings\nborn from `POST /action-items` or `POST /schedules` may also\nstart at `NEEDS_REVIEW` if the engine's eligibility checks\nrequire human disposition.\n"},"ScheduleSubmission":{"type":"object","required":["schedule_start_date","schedule_end_date","pairings"],"description":"Each submission represents the **complete current state** of all\nopen pairings **within the declared schedule window**. Routespring\ncomputes the diff against the prior submission server-side; the client\ndoes not declare what changed.\n\n`schedule_start_date` and `schedule_end_date` (both **required**) bound\nthat window. Routespring interprets the file *relative to what is\nalready in the system only inside this window*: bookings whose date\nfalls within `[schedule_start_date, schedule_end_date]` are reconciled\nagainst the submission (booked, amended, or cancelled to match), while\nbookings outside the window are left untouched. This lets an airline\nsubmit a partial roster (e.g. just next week) without Routespring\ntreating every booking outside that range as removed.\n\nAll timestamps are full ISO-8601 with an explicit UTC offset (e.g.\n`2026-05-27T01:00:00-06:00`); `as_of` is UTC (`Z`). There is a single\ntime representation — no parallel `*_utc` fields.\n\nThe booking diff derives both **hotel** layovers (from each duty\nperiod's `layover`) and **deadhead** positioning flights (from `DH`\nlegs) against the prior submission.\n","properties":{"external_ref":{"type":"string","description":"Airline-side identifier for this submission (echoed back)."},"as_of":{"type":"string","format":"date-time","example":"2026-05-27T08:00:00Z","description":"Airline timestamp (UTC) when this snapshot was captured."},"schedule_start_date":{"type":"string","format":"date","example":"2026-05-16","description":"Inclusive start of the schedule window this submission covers\n(`YYYY-MM-DD`). Routespring only reconciles bookings on or after\nthis date against the submission.\n"},"schedule_end_date":{"type":"string","format":"date","example":"2026-05-31","description":"Inclusive end of the schedule window this submission covers\n(`YYYY-MM-DD`). Must be on or after `schedule_start_date`.\nRoutespring only reconciles bookings on or before this date.\n"},"pairings":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/Pairing"}}}},"Pairing":{"type":"object","required":["pairing_number"],"description":"Supply what your system exports. Routespring uses whatever is present;\nmissing optional fields reduce interpretability but do not fail\ningestion. At minimum, a pairing needs its `pairing_number`, at least\none `crew[].employee_id`, and enough leg data to determine layover\nstations and deadhead legs.\n","properties":{"pairing_number":{"type":"string","example":"MX-20260527-4821","description":"The airline's identifier for this trip, assigned during crew pairing\noptimization. Used as an anchor key on all derived bookings — every\nhotel and deadhead booking is indexed by this value for retrieval via\n`GET /crew/bookings`.\n"},"origin_base":{"type":"string","example":"SLC","description":"IATA of the base where the pairing begins (sign-in). Almost\nalways equal to the crew's `home_base`; differs only for unusual\nbase-changes mid-pairing.\n"},"terminus_base":{"type":"string","example":"SLC","description":"IATA of the base where the pairing ends (sign-off). Almost always\nequal to `origin_base`; pairings that originate and terminate at\ndifferent bases are exceptional (\"one-way\" pairings).\n"},"check_in_local":{"type":"string","format":"date-time","example":"2026-05-16T05:30:00-06:00","description":"Local timestamp (with UTC offset) at `origin_base` when the crew\nsigns in. Equal to `fdp_start_local` of the first duty period.\n"},"check_out_local":{"type":"string","format":"date-time","example":"2026-05-17T19:45:00-06:00","description":"Local timestamp (with UTC offset) at `terminus_base` when the crew\nsigns off. Equal to `fdp_end_local` of the last duty period.\n"},"tafb_hours":{"type":"number","example":38.25,"description":"Time Away From Base — elapsed hours from `check_in` to\n`check_out` of this pairing\n([NAVBLUE definition](https://n-crewplanning.support.navblue.aero/support/solutions/articles/35000210162-tafb)).\nPairing-level by definition, not duty-level. Used by union per-diem\nand bid-preference rules. See the **Glossary** (TAFB).\n"},"crew":{"type":"array","description":"Crew members assigned to this pairing. Include all crew operating\ntogether — each member gets their own hotel and deadhead flight\nbookings derived independently based on their `home_base` and\ntraveller profile.\n","items":{"$ref":"#/components/schemas/CrewMember"}},"duty_periods":{"type":"array","description":"The ordered sequence of duty periods that make up this pairing. Each\nduty period represents one working day, bounded by sign-in and\nsign-off times, and may carry a layover if the crew overnights away\nfrom base.\n","items":{"$ref":"#/components/schemas/DutyPeriod"}}}},"CrewMember":{"type":"object","required":["traveller_email"],"description":"A crew member assigned to the pairing. All identifiers are\nairline-provided; Routespring does not mint its own crew id.\n`traveller_email` is the single crew identity — the same anchor used by\n`anchors.traveller_email` on the booking endpoints — so the schedule and\nbooking surfaces resolve a crew member the same way.\n","properties":{"traveller_email":{"type":"string","format":"email","example":"jreyes@flymx.com","description":"The crew member's Routespring account email — the **required** crew\nanchor. Routespring resolves the traveller's bookable profile and\nbooking eligibility from it, so it must be a registered user of the\nairline's company. (Replaces the former `user_id`.)\n"},"employee_id":{"type":"string","example":"EMP-1042","description":"The airline's HR employee identifier — an optional secondary anchor.\nFlows into the booking's denormalized index for retrieval; it is not\nused to resolve the crew member (that is `traveller_email`).\n"},"name":{"type":"string","example":"Jordan Reyes","description":"Display name of the crew member. Appears in booking records and exports."},"rank":{"type":"string","example":"CA","description":"Role code — CA (Captain), FO (First Officer), FA (Flight\nAttendant), PU (Purser), SO (Second Officer). See the\n**Glossary** (Rank).\n"},"home_base":{"type":"string","example":"SLC","description":"IATA of the crew member's contractual base / domicile.\nSee the **Glossary** (Crew Base).\n"},"union_code":{"type":"string","example":"ALPA","description":"Union the crew member belongs to (ALPA, APFA, AFA-CWA, APA, …).\nAffects per-airline hotel tier at some carriers.\nSee the **Glossary** (Union Code).\n"}}},"DutyPeriod":{"type":"object","description":"One duty period within a pairing. `fdp_*_local` are full ISO-8601\ntimestamps with an explicit UTC offset.\n","properties":{"duty_period_num":{"type":"integer","example":1,"description":"Sequential number of this duty period within the pairing, starting at 1. Must be unique within a pairing."},"duty_date":{"type":"string","format":"date","example":"2026-05-27","description":"Calendar date the duty starts on (local to the first leg's departure)."},"fdp_start_local":{"type":"string","format":"date-time","example":"2026-05-26T23:30:00-06:00","description":"FDP start instant, local to the first leg's `from_airport` (with UTC offset)."},"fdp_end_local":{"type":"string","format":"date-time","example":"2026-05-27T12:15:00-04:00","description":"FDP end instant, local to the last leg's `to_airport` of this duty\nperiod (with UTC offset). May carry a different offset than\n`fdp_start_local` for long-haul flying.\n"},"legs":{"type":"array","description":"The ordered sequence of legs within this duty period. Each leg is one\naircraft movement. Legs with `activity_code: DH` and\n`booking_required: true` will trigger a deadhead positioning flight\nbooking.\n","items":{"$ref":"#/components/schemas/Leg"}},"layover":{"$ref":"#/components/schemas/Layover","description":"The rest period and hotel layover following this duty period, if one is required."}}},"Layover":{"type":"object","description":"The overnight rest following this duty period, when one is required.\nAbsent on the final duty period of a pairing (crew returns to base).\nDrives hotel booking for the layover station.\n","properties":{"station":{"type":"string","example":"IAD","description":"IATA code of the layover city where the crew overnights."},"booking_required":{"type":"boolean","description":"When `true`, Routespring books a hotel for this layover. When\n`false`, the layover is recorded but no hotel booking is attempted\n— use this when the airline is handling accommodation directly.\n"},"checkin_time_local":{"type":"string","format":"date-time","example":"2026-05-27T14:45:00-04:00","description":"The crew's expected hotel check-in time, local to the layover station."},"checkout_time_local":{"type":"string","format":"date-time","example":"2026-05-28T01:00:00-04:00","description":"The crew's expected hotel check-out time, local to the layover station."}}},"Leg":{"type":"object","required":["leg_sequence","activity_code"],"description":"One leg of a duty period. Revenue (`FLT`) legs carry actual\n`dep_time_local` / `arr_time_local`; deadhead (`DH`) legs that\nRoutespring must book carry `expected_dep_time_local` /\n`expected_arr_time_local` and `booking_required: true`. All timestamps\nare full ISO-8601 with an explicit UTC offset.\n","properties":{"leg_sequence":{"type":"integer","example":1,"description":"Sequential position of this leg within the duty period, starting\nat 1. Used as part of the deadhead diff key — changing this value\non an existing `DH` leg will trigger a cancellation of the old\nbooking and a new one.\n"},"activity_code":{"type":"string","enum":["FLT","DH","TRG","OFF","RSV","SBY","SL","RST"],"description":"Type of activity for this leg. Only `DH` legs with\n`booking_required: true` trigger a booking action — all other\ncodes are recorded for operational context but do not produce\nbookings.\n\n`FLT` — revenue flight (crew is operating); `DH` — deadhead\npositioning; `TRG` — training; `OFF` — day off; `RSV` — reserve;\n`SBY` — standby; `SL` — sick leave; `RST` — rest.\n"},"flight_number":{"type":"string","example":"MX401","description":"IATA carrier code and flight number (e.g. `BZ401`). Present on\noperated `FLT` legs. Typically absent on `DH` legs since the\npositioning flight has not yet been booked.\n"},"from_airport":{"type":"string","example":"SLC","description":"IATA 3-letter departure airport code. For a `DH` (deadhead) leg this\nis the departure airport of the positioning flight Routespring books\n— taken as submitted, not derived.\n"},"to_airport":{"type":"string","example":"LAX","description":"IATA 3-letter arrival airport code. For a `DH` (deadhead) leg this is\nthe arrival airport of the positioning flight Routespring books —\ntaken as submitted, not derived.\n"},"dep_time_local":{"type":"string","format":"date-time","example":"2026-05-27T01:00:00-06:00","description":"Scheduled departure (with UTC offset). Present on operated (`FLT`) legs."},"arr_time_local":{"type":"string","format":"date-time","example":"2026-05-27T01:30:00-07:00","description":"Scheduled arrival (with UTC offset). Present on operated (`FLT`) legs."},"expected_dep_time_local":{"type":"string","format":"date-time","example":"2026-05-28T07:00:00-05:00","description":"Desired departure window for a `DH` leg Routespring must book (with UTC offset)."},"expected_arr_time_local":{"type":"string","format":"date-time","example":"2026-05-28T08:15:00-05:00","description":"Desired arrival window for a `DH` leg Routespring must book (with UTC offset)."},"booking_required":{"type":"boolean","description":"When `true` on a `DH` leg, Routespring books a positioning flight\nfor this deadhead. When `false`, the leg is recorded but no flight\nbooking is attempted, even if all other eligibility criteria are met.\n"},"block_hours":{"type":"number","example":13.75,"description":"Gate-to-gate flight time in hours. Used for crew pay calculations.\nNot used by the booking engine.\n"},"cabin_class":{"type":"string","enum":["ECONOMY","PREMIUM_ECONOMY","BUSINESS","FIRST"],"description":"Cabin booked / requested for this flight hop."},"fare_name":{"type":"string","example":"Main Cabin","description":"GDS fare-product name for this flight hop."},"aircraft_type":{"type":"string","example":"A220-300","description":"ICAO/IATA aircraft type (operational context, used internally)."},"aircraft_tail":{"type":"string","example":"N412MX","description":"Aircraft tail registration."},"hotel_station_override":{"type":"string","example":"JFK","description":"IATA code that should be used as the layover city for hotel\nplacement, overriding `to_airport`. Set when the crew overnights\nat a different airport than the arrival airport (e.g. ferry,\nsplit crew, contract hotel in a nearby city).\n"},"remarks":{"type":"string","description":"Free-text notes exported from your crew management system. IROP-related\nkeywords in this field (`IROP`, `IROP-WX`, `IROP-ATC`, `DELAY`, `DVT`,\n`CASCADE`) cause the derived action items to be classified at elevated\npriority (P1/P2) for faster handling.\n"}}},"ScheduleAccepted":{"type":"object","properties":{"schedule_id":{"type":"string"},"external_ref":{"type":"string"},"processing_status":{"$ref":"#/components/schemas/ProcessingStatusEnum"},"received_at":{"type":"string","format":"date-time"},"links":{"type":"object","properties":{"status":{"type":"string"},"action_items":{"type":"string"}}}}},"ScheduleDetail":{"allOf":[{"$ref":"#/components/schemas/ScheduleAccepted"},{"type":"object","properties":{"started_at":{"type":"string","format":"date-time"},"completed_at":{"type":"string","format":"date-time"},"summary":{"$ref":"#/components/schemas/ScheduleSummary"}}}]},"ScheduleSummary":{"type":"object","properties":{"action_items_total":{"type":"integer"},"action_items_auto_booked":{"type":"integer"},"action_items_auto_cancelled":{"type":"integer","description":"Action items the engine cancelled automatically with the supplier\n(`disposition: AUTO_CANCELLED`). Counted separately from\n`action_items_auto_booked` so a successful auto-cancellation is not\nmislabelled as pending review.\n"},"action_items_pending_review":{"type":"integer"},"action_items_failed":{"type":"integer","description":"Action items whose booking attempt terminally failed\n(`bookingStatus` = `FAILED` / `CANCELLATION_FAILED`). Together with\n`action_items_auto_booked`, `action_items_auto_cancelled` and\n`action_items_pending_review` this covers every disposition.\n"},"audit_events":{"type":"integer"}}},"ScheduleListResponse":{"type":"object","properties":{"schedules":{"type":"array","items":{"$ref":"#/components/schemas/ScheduleDetail"}},"next_cursor":{"type":["string","null"]},"total_results":{"type":"integer","description":"Total schedules matching the filter across all pages."}}},"ProcessingStatus":{"type":"object","properties":{"schedule_id":{"type":"string"},"status":{"$ref":"#/components/schemas/ProcessingStatusEnum"},"received_at":{"type":"string","format":"date-time"},"started_at":{"type":["string","null"],"format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"},"error":{"type":["string","null"]},"flight_tasks_pending":{"type":"integer","description":"Count of FLIGHT (deadhead) booking tasks not yet terminal. The\nsubmission stays in `BOOKING` until this reaches 0, then flips to\n`COMPLETED`.\n"},"poll_recommended":{"type":"boolean","description":"Whether the client should keep polling this endpoint. **Use this as\nthe loop condition** instead of hard-coding status checks.\n\n`true`  — the schedule is still progressing on its own (`QUEUED` /\n`PROCESSING`, or `BOOKING` with a supplier call in flight); poll again\nafter a short delay.\n\n`false` — nothing more will change automatically: either a terminal\nstatus (`COMPLETED` / `FAILED` / `PARTIAL`), **or** `BOOKING` pinned only\nby items left for manual action (e.g. a booking surfaced as\n`NEEDS_REVIEW` for an agent to resolve). In that case the automatic\nbookings are already done — stop polling and read `…/action-items`\nand `…/bookings`.\n"},"summary":{"$ref":"#/components/schemas/ScheduleSummary"}}},"ActionItem":{"type":"object","properties":{"action_id":{"type":"string","example":"act_01HMWX9TG3FJ8K9V2N1QS7HRPM"},"schedule_id":{"type":["string","null"],"description":"Populated for diff-derived action items (the schedule submission\nthat produced this item). Null for airline-injected items\ncreated via `POST /action-items`.\n"},"source":{"type":"string","enum":["SCHEDULE_DIFF","AIRLINE_INJECTED","IROP"],"default":"SCHEDULE_DIFF","description":"How this action item entered the engine. Output-only: on input\n(`POST /action-items` / `POST /bookings/request`) only\n`AIRLINE_INJECTED` and `IROP` are accepted — `SCHEDULE_DIFF` is\nassigned by Routespring for diff-derived items and cannot be set.\n* `SCHEDULE_DIFF` — derived from a `POST /schedules` submission.\n* `AIRLINE_INJECTED` — created via `POST /action-items` outside any IROP context.\n* `IROP` — created via `POST /action-items` with `source: IROP`.\n"},"type":{"type":"string","enum":["HOTEL","FLIGHT"],"description":"`FLIGHT` action items are produced from two paths in v1: the\n`POST /schedules` diff emits them for **deadhead positioning** —\na `Leg` with `activity_code: DH` (classified `NEW_DEADHEAD` /\n`DEADHEAD_TIME_CHANGE` / `DEADHEAD_CANCELLED`); airline\ninjection (`POST /action-items` / `POST /bookings/request`)\ncovers the out-of-schedule cases.\n"},"subtype":{"type":"string","enum":["NEW_BOOKING","MODIFY","CANCEL"],"description":"The booking operation this action item represents — whether the\nengine needs to create a new booking, modify an existing one, or\ncancel. For example, a `LAYOVER_DATE_CHANGE` produces\n`subtype: MODIFY`; a `LAYOVER_CANCELLED` produces\n`subtype: CANCEL`; a `NEW_LAYOVER` produces `subtype: NEW_BOOKING`.\n"},"classification":{"type":"string","example":"NEW_LAYOVER","description":"Classification of why this action is being taken. For\ndiff-derived items the value is computed by the engine; for\nairline-injected items (`POST /action-items`) the airline\nsupplies it. See the **Action item classifications** table in\nthe API overview for the full taxonomy and the v1 diff-emitted\nsubset. Common values (not exhaustive; new classifications may\nappear in future):\n\n**Hotel — diff-derived**\n* `NEW_LAYOVER` — new overnight in a pairing not previously seen\n* `LAYOVER_DATE_CHANGE` — same pairing, different layover date(s)\n* `LAYOVER_CITY_CHANGE` — layover moved to a different airport\n* `LAYOVER_CANCELLED` — pairing dropped/shortened, no longer needs hotel\n* `LAYOVER_EXTENDED` — pairing lengthened; existing hotel needs more nights\n\n**Flight (deadhead) — diff-derived**\n* `NEW_DEADHEAD` — crew needs positioning to/from a new pairing\n* `DEADHEAD_TIME_CHANGE` — start time of operating duty shifted\n* `DEADHEAD_CANCELLED` — operating pairing dropped\n\n**Pairing-level changes**\n* `PAIRING_NEW`, `PAIRING_EXTENDED`, `PAIRING_SHORTENED`, `PAIRING_CANCELLED`\n* `SWAP_OUT`, `SWAP_IN` — same pairing reassigned between crew members\n* `STATUS_TRANSITION` — operating flight became non-operational (FLT → SL/OFF/RST)\n\n**IROP — airline-injected** (via `POST /action-items` or `POST /bookings/request`)\n* `IROP_NEW_DEADHEAD` — new positioning leg needed mid-IROP\n* `IROP_REROUTE` — existing deadhead replaced with a new flight\n* `IROP_HOTEL_EXTEND` — pairing extended by an IROP; existing hotel needs more nights\n* `IROP_HOTEL_CANCEL` — pairing shortened by an IROP; hotel no longer needed\n**Data quality**\n* `DATA_QUALITY_FLAG`, `DATA_QUALITY_QUARANTINE`, `DUPLICATE_ROW`\n\n**No-op**\n* `UNCHANGED` — emitted only when the Routespring-managed\n  `emit_nop_tasks` toggle is on (off by default in v1; not\n  configurable via this API), for audit trail\n"},"priority":{"type":"string","enum":["P1","P2","P3","P4"],"description":"Drives queue ordering; review-SLA enforcement is not active\n(`priority` is recorded but the SLA targets are advisory). See the\n**Priority** table in the API overview for full semantics.\nSummary:\n* `P1` — IROP / critical, ~5 min target to disposition\n* `P2` — same-day pairing changes; default for airline injections, ~30 min\n* `P3` — routine layover bookings, within the hour\n* `P4` — backfills / low-urgency cleanup, next batch window\n"},"disposition":{"type":"string","enum":["AUTO_BOOKED","AUTO_CANCELLED","PENDING_REVIEW","FAILED"],"description":"What the booking engine did with this action:\n* `AUTO_BOOKED`    — eligible and successfully booked with a supplier\n* `AUTO_CANCELLED` — a `subtype: CANCEL` action that the engine\n  cancelled automatically with the supplier (no human needed). Also\n  covers an in-flight automatic cancel that has not yet settled.\n* `PENDING_REVIEW` — requires a human (fee risk, no preferred vendor, etc.)\n* `FAILED`         — was attempted but the supplier call failed; check `error_message`\n"},"auto_eligible":{"type":"boolean","description":"Whether this action passed all auto-booking eligibility checks.\nFor `HOTEL` actions that means the policy checks (has a preferred\nvendor, within `auto_book_max_price`, etc.). For `FLIGHT` (deadhead)\nactions it reflects whether automatic flight booking is enabled\nfor the company — `false` means the item is staged for manual\nreview; deadhead *cancellations* are always auto-executed and\ntherefore always `true`.\nDistinct from `disposition`: an action can be `auto_eligible: true`\nbut still end up `disposition: FAILED` if the supplier call fails.\nUse this to distinguish \"ineligible — don't retry\" from \"eligible\nbut failed — retry could work\". An `AUTO_BOOKED` / `AUTO_CANCELLED`\naction is always `auto_eligible: true`.\n"},"recommended_action":{"type":"string","description":"For actions with `disposition: PENDING_REVIEW`, a human-readable\nrecommendation from the engine. E.g. *\"Request fee waiver from\nsupplier before cancelling (within 24h fee window)\"*, *\"Add Hilton\nDXB to preferred-hotels config to enable auto-book\"*.\n"},"anchors":{"type":"object","description":"Airline-provided identifiers linking this action item to the crew member and pairing it was derived for.","properties":{"employee_id":{"type":"string","description":"The crew member this action item was derived for."},"pairing_number":{"type":"string","description":"The pairing this action item belongs to."},"duty_date":{"type":"string","format":"date","description":"The duty date the action item is associated with."},"flight_number":{"type":"string","description":"The operating flight number, when applicable."}}},"source_rows":{"type":"object","description":"Row numbers in the submitted roster that produced this action.\n`day1_row` references the prior submission (the \"before\"), `day2_row`\nreferences the current submission (the \"after\"). Either may be null\n(a brand-new pairing has no day1_row; a cancelled pairing has no\nday2_row). Useful for tracing automated decisions back to specific\nroster rows during audit.\n","properties":{"day1_row":{"type":["integer","null"]},"day2_row":{"type":["integer","null"]}}},"booking_id":{"type":["string","null"],"description":"Set once the action results in a booking."},"flight_options_url":{"type":["string","null"],"description":"For diff-derived `type: FLIGHT` (deadhead) action items, a ready\n`GET /flight-options` query for this leg, pre-populated with the\nDH leg's `from_airport`, `to_airport`, `date` and `employee_id`.\nPopulated regardless of `disposition` — auto-booked deadheads\ncarry it too, so an agent can re-check options, and an agent\nworking a `PENDING_REVIEW` item can fetch options then commit a\nchosen `flight_option_id` via `POST /bookings/request` using this\naction's `anchors`. Null when the leg's route/date are not\navailable on the item — e.g. airline-injected flight items, whose\nroute lives in the injection's `flight` context.\n"},"notes":{"type":"string","description":"Free-text notes associated with this action item, either from\nthe roster system remarks or added during injection.\n"},"created_at":{"type":"string","format":"date-time","description":"Timestamp when this action item was created."}}},"ActionItemListResponse":{"type":"object","properties":{"action_items":{"type":"array","items":{"$ref":"#/components/schemas/ActionItem"}},"next_cursor":{"type":["string","null"]}}},"AuditEvent":{"type":"object","properties":{"audit_id":{"type":"string"},"schedule_id":{"type":"string"},"category":{"type":"string","enum":["DATA_QUALITY","IROP","DUPLICATE"],"description":"v1 emits `DATA_QUALITY` (invalid roster rows) and `DUPLICATE`\n(duplicate-booking detector). `IROP` audit events are\nnot emitted — expect them to be absent.\n"},"severity":{"type":"string","enum":["INFO","WARNING","ERROR"]},"reason":{"type":"string"},"anchors":{"type":"object","description":"Airline-meaningful identifiers for the row/task this event\nconcerns. No `irop_ref` — audit events are data-quality /\nduplicate findings, never IROP-correlated.\n","properties":{"employee_id":{"type":["string","null"]},"pairing_number":{"type":["string","null"]},"duty_date":{"type":["string","null"],"format":"date"},"flight_number":{"type":["string","null"]}}},"created_at":{"type":"string","format":"date-time"}}},"AuditEventListResponse":{"type":"object","properties":{"audit_events":{"type":"array","items":{"$ref":"#/components/schemas/AuditEvent"}},"next_cursor":{"type":["string","null"]}}},"Anchors":{"type":"object","description":"Airline-meaningful identifiers that the booking is denormalized\nagainst — retrieval by any one of these is a single indexed lookup.\nAll identifiers come from the airline's roster system; `irop_ref`\nis supplied by the airline when injecting an action item or booking\nrequest tied to an irregular-operations event. Routespring does not\nmint its own crew/pairing/duty/leg ids.\n\nOn cancellations (bookings and action items), `pairing_number` may be\nabsent when the cancelled stay cannot be attributed to a specific\npairing.\n","properties":{"employee_id":{"type":["string","null"]},"pairing_number":{"type":["string","null"]},"duty_date":{"type":["string","null"],"format":"date"},"flight_number":{"type":["string","null"]},"irop_ref":{"type":["string","null"],"description":"Airline IROP correlation id. Populated on action items and\nbookings injected with `source: IROP` (via `POST /action-items`\nor `POST /bookings/request`). Lets the airline pull every\nartifact tied to one IROP into a single after-action thread.\n"}}},"HotelConfigInput":{"type":"object","required":["bases"],"properties":{"bases":{"type":"array","items":{"$ref":"#/components/schemas/HotelBaseConfig"}}}},"HotelConfig":{"allOf":[{"$ref":"#/components/schemas/HotelConfigInput"},{"type":"object","properties":{"version":{"type":"string","example":"v-2026-05-15-001"},"active":{"type":"boolean"},"updated_at":{"type":"string","format":"date-time"}}}]},"HotelBaseConfig":{"type":"object","required":["base_station","hotels"],"properties":{"base_station":{"type":"string","example":"SLC","description":"IATA airport code identifying the crew base station this hotel\nblock is for. Named `base_station` (not just `base`) to avoid\nconfusion with the crew member's `home_base` field — \"base\"\nalone is overloaded in airline operations.\n"},"hotels":{"type":"array","maxItems":1,"description":"The single preferred hotel for this base (one hotel per base).","items":{"$ref":"#/components/schemas/PreferredHotel"}}}},"PreferredHotel":{"type":"object","required":["property_id","policy"],"properties":{"property_id":{"type":"string","description":"Unique identifier for the hotel property. For GDS hotels, this must\nmatch a valid entry in Routespring's hotel inventory — use\n`GET /hotels` to look up valid values. For DIRECT hotels, this is\nyour airline's own identifier and is not validated externally.\n"},"chain":{"type":"string","example":"HILTON","description":"Hotel chain code (required by the supplier at booking time). For\n`GDS` hotels it is **auto-derived** from Routespring's inventory\nusing `property_id`, so you may omit it; supply it for `DIRECT`\nhotels (which have no inventory record to derive from).\n"},"contract_rate":{"$ref":"#/components/schemas/Money","description":"Default contracted rate. Used when no `rate_periods` are defined\nor when the booking date falls outside all defined periods.\nKept for simple cases; for time-varying contracts, use\n`rate_periods` instead.\n"},"rate_periods":{"type":"array","description":"Seasonal / time-varying contracted rates. When set, the rate\nmatching the booking's check-in date wins; falls back to\n`contract_rate` if no period matches. Periods are validated to\nnot overlap. Example: summer peak, winter low season,\nconvention week, etc.\n","items":{"$ref":"#/components/schemas/HotelRatePeriod"}},"policy":{"type":"string","enum":["STANDARD","GUARANTEED","FLEXIBLE"],"description":"Check-in / check-out derivation policy for this hotel. Determines\nwhich timing fields are **required**:\n* `STANDARD` — fixed property times; requires\n  `standard_check_in_time` + `standard_check_out_time`.\n* `GUARANTEED` — guaranteed early arrival / late departure; requires\n  `guaranteed_early_check_in_time` + `guaranteed_late_check_out_time`.\n* `FLEXIBLE` — a tolerance window around the scheduled times; requires\n  `flex_window_minutes`.\n"},"standard_check_in_time":{"type":"string","pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$","example":"15:00","description":"Property's standard local check-in time of day (`HH:MM`, 24-hour).\n**Required when `policy` is `STANDARD`.**\n"},"standard_check_out_time":{"type":"string","pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$","example":"11:00","description":"Property's standard local check-out time of day (`HH:MM`, 24-hour).\n**Required when `policy` is `STANDARD`.**\n"},"guaranteed_early_check_in_time":{"type":"string","pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$","example":"10:00","description":"Guaranteed earliest local check-in time (`HH:MM`, 24-hour).\n**Required when `policy` is `GUARANTEED`.**\n"},"guaranteed_late_check_out_time":{"type":"string","pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$","example":"14:00","description":"Guaranteed latest local check-out time (`HH:MM`, 24-hour).\n**Required when `policy` is `GUARANTEED`.**\n"},"flex_window_minutes":{"type":"integer","minimum":0,"example":120,"description":"Tolerance window (minutes) applied around the scheduled check-in /\ncheck-out. **Required when `policy` is `FLEXIBLE`.**\n"},"buffer_minutes":{"type":"integer","minimum":0,"example":60,"description":"Additional buffer (minutes) added to the derived check-in / check-out\nwindow. Optional; applies to every policy.\n"}},"allOf":[{"if":{"properties":{"policy":{"const":"STANDARD"}},"required":["policy"]},"then":{"required":["standard_check_in_time","standard_check_out_time"]}},{"if":{"properties":{"policy":{"const":"GUARANTEED"}},"required":["policy"]},"then":{"required":["guaranteed_early_check_in_time","guaranteed_late_check_out_time"]}},{"if":{"properties":{"policy":{"const":"FLEXIBLE"}},"required":["policy"]},"then":{"required":["flex_window_minutes"]}}]},"HotelRatePeriod":{"type":"object","required":["rate","effective_from","effective_to"],"properties":{"rate":{"$ref":"#/components/schemas/Money"},"effective_from":{"type":"string","format":"date","description":"First check-in date this rate applies to (inclusive)."},"effective_to":{"type":"string","format":"date","description":"Last check-in date this rate applies to (inclusive)."}}},"FlightConfigInput":{"type":"object","properties":{"preferred_airlines":{"type":"array","items":{"type":"string"},"example":["AA","DL","UA","EK"],"description":"IATA 2-letter carrier codes to prioritize when booking deadhead\nflights. The engine tries these carriers first; if none have\navailability, any non-blacklisted carrier is considered.\n"},"blacklisted_airlines":{"type":"array","items":{"type":"string"},"example":["F9"],"description":"IATA 2-letter carrier codes that must never be booked for deadhead\npositioning, regardless of availability or price.\n"},"preferred_cabin":{"type":"string","enum":["ECONOMY","PREMIUM_ECONOMY","BUSINESS","FIRST"],"description":"Default cabin class for deadhead flights. The engine will not book\nabove this class. Can be overridden per-request via the\n`cabin_class` field on `POST /bookings/request`.\n"},"same_carrier_preference":{"type":"boolean","description":"When true, the engine prioritizes the crew member's own airline's\nflights for deadhead positioning where available, before considering\ncarriers in `preferred_airlines`.\n"}}},"FlightConfig":{"allOf":[{"$ref":"#/components/schemas/FlightConfigInput"},{"type":"object","properties":{"version":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}}]},"FlightRulesInput":{"type":"object","properties":{"auto_book_max_price":{"$ref":"#/components/schemas/Money","description":"Maximum fare per ticket the engine will auto-book without human review.\nIf the cheapest available option exceeds this amount, the booking is\nheld as `NEEDS_REVIEW` for travel manager approval. Omit to apply no\nprice ceiling.\n"}}},"FlightRules":{"allOf":[{"$ref":"#/components/schemas/FlightRulesInput"},{"type":"object","properties":{"version":{"type":"string","description":"Identifier of this config version, referenced by bookings via `config_version_used`."},"updated_at":{"type":"string","format":"date-time","description":"Timestamp when this version was created."}}}]},"CrewCompanyConfigInput":{"type":"object","required":["crew_booker_email"],"description":"Per-company crew automation settings (`PUT /config/company`). Only\n`crew_booker_email` is required; omit any other field to keep its current\nstored value (merge semantics).\n","properties":{"crew_booker_email":{"type":"string","format":"email","example":"crew-ops@flymx.com","description":"The Routespring account under which all engine-driven hotel bookings,\namendments, and cancellations are placed. Manual flight commits via\n`POST /bookings/request` are booked under the bearer token making the\ncall, not this account.\n"},"cancellation_lookback_days":{"type":"integer","description":"Days before the processing date a checkout is still eligible for\ncancellation. Default 0.\n"}}},"CrewCompanyConfig":{"type":"object","description":"Resolved per-company crew automation settings.","properties":{"crew_booker_email":{"type":"string","format":"email"},"cancellation_lookback_days":{"type":"integer"}}},"Booking":{"type":"object","properties":{"booking_id":{"type":"string","example":"bkg_01HMWXA52N0SQ8FRPDPK7RZ44V"},"type":{"type":"string","enum":["HOTEL","FLIGHT"],"description":"Determines which detail sub-object is populated: a `HOTEL` booking\ncarries `hotel` (and `flight` is null); a `FLIGHT` booking carries\n`flight` (and `hotel` is null).\n\n`FLIGHT` bookings in v1 originate from both the schedule diff\n(deadhead positioning — a `DH` leg booked on its explicit\n`from_airport` → `to_airport`, arriving by the duty's `fdp_start`)\nand airline injection\n(`POST /bookings/request` / `POST /action-items`).\n"},"status":{"$ref":"#/components/schemas/BookingStatusEnum"},"source":{"type":"string","enum":["SCHEDULE_DIFF","AIRLINE_INJECTED","IROP"],"default":"SCHEDULE_DIFF","description":"How this booking originated.\n* `SCHEDULE_DIFF` — born from a `POST /schedules` diff-derived action item.\n* `AIRLINE_INJECTED` — created via `POST /bookings/request` or `POST /action-items` (non-IROP).\n* `IROP` — created via `POST /bookings/request` or `POST /action-items` with `source: IROP`.\nBookings with `source` of `AIRLINE_INJECTED` or `IROP` default to `manual_override: true`.\n"},"supplier":{"type":"object","properties":{"name":{"type":"string","example":"Hilton"},"confirmation_number":{"type":"string","example":"HIL-89A3X7"}}},"anchors":{"$ref":"#/components/schemas/Anchors"},"config_version_used":{"type":"string","description":"The config version in force when this booking was made.\n**In v1 this is always `\"v1\"`** — only a single config version\nexists.\n"},"contracted_rate":{"$ref":"#/components/schemas/Money","description":"For HOTEL bookings: the rate per the airline's contract at the\ntime of booking (resolved from `PreferredHotel.contract_rate`\nor the matching `rate_periods` entry). For FLIGHT bookings:\ntypically null (deadhead flights don't have contracted rates).\n"},"booked_rate":{"$ref":"#/components/schemas/Money","description":"The rate actually charged by the supplier. May differ from\n`contracted_rate` when a supplier returns a different rate\n(price discrepancy — surfaces in audit dashboards). For FLIGHT\nbookings this is the fare paid.\n"},"error_message":{"type":"string","description":"Populated when `status: FAILED`. Free-text supplier or engine\nerror message. E.g. \"Supplier returned 404: No availability\",\n\"Card declined (insufficient funds)\". Use to decide whether to retry,\nescalate, or accept failure.\n"},"cancellation_reason":{"type":["string","null"],"description":"The reason recorded when the booking was cancelled (the `reason`\npassed to `DELETE /bookings/{booking_id}`). Surfaced here for\nconvenience; the full status history remains on\n`GET /bookings/{booking_id}/status`.\n"},"retry_count":{"type":"integer","example":1,"description":"How many times this booking has been retried. Increments on\neach manual or scheduled retry attempt. Zero on the original\nattempt.\n"},"max_retries":{"type":"integer","example":3,"description":"Maximum number of automatic retries before the booking is\nconsidered terminally failed. Configurable per-company.\n"},"manual_override":{"type":"boolean","description":"When `true`, this booking is \"owned\" by manual edits — Routespring's\ndiff engine will NOT touch it on subsequent schedule submissions,\neven if the upstream pairing changes. Automatically set to `true`\nby any successful `PATCH /bookings/{id}` or `DELETE /bookings/{id}`\ncall. To release the booking back to automatic management\n(allowing future schedule submissions to modify or cancel it),\n`PATCH` with `{ \"manual_override\": false }`.\n\n**Why this exists:** prevents the diff engine from silently\nreverting a deliberate manual change. The trade-off: while\n`manual_override` is `true`, the booking won't follow the roster\nanymore — it's the airline's responsibility to keep it in sync\nor release it back.\n"},"hotel":{"$ref":"#/components/schemas/HotelBookingDetail"},"flight":{"$ref":"#/components/schemas/FlightBookingDetail"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"HotelBookingDetail":{"type":"object","properties":{"property_id":{"type":"string"},"property_name":{"type":"string"},"city":{"type":"string","example":"SLC","description":"IATA of the city where the hotel was booked. Equal to the\noriginating leg's `hotel_station_override` if set, otherwise\nthe leg's `to_airport`.\n"},"check_in_date":{"type":"string","format":"date"},"check_out_date":{"type":"string","format":"date"},"room_type":{"type":"string"}}},"FlightBookingDetail":{"type":"object","properties":{"airline_code":{"type":"string","example":"DL"},"flight_number":{"type":"string","example":"DL1234"},"from_airport":{"type":"string"},"to_airport":{"type":"string"},"dep_time":{"type":"string","format":"date-time"},"arr_time":{"type":"string","format":"date-time"},"cabin_class":{"type":"string","enum":["ECONOMY","PREMIUM_ECONOMY","BUSINESS","FIRST"],"example":"ECONOMY"},"fare_name":{"type":"string","example":"Main Cabin","description":"GDS fare-product name booked for this hop (e.g. \"Main Cabin\", \"Economy Basic\")."},"pnr":{"type":"string","example":"ABC123"},"ticket_id":{"type":"string","example":"0167891234567","description":"Airline ticket number, populated after the booking is ticketed (from the latest status refresh)."},"transaction_id":{"type":"string","example":"txn_4f9c2a","description":"Supplier/transaction identifier for the booking (from the latest status refresh)."},"trip_session_id":{"type":"string","example":"1284417","description":"Internal trip-session identifier for the booked flight, populated from the latest status refresh."}}},"BookingModification":{"type":"object","properties":{"reason":{"type":"string","example":"PAIRING_EXTENDED"},"manual_override":{"type":"boolean","description":"Optional. Defaults to `true` on any modification request\n(so once you patch a booking, the diff engine stops managing\nit). Pass `false` explicitly to release the booking back to\nautomatic management — e.g. *\"I overrode this last week; the\nissue is resolved, please let auto take over again.\"*\n"},"hotel":{"type":"object","properties":{"check_in_date":{"type":"string","format":"date"},"check_out_date":{"type":"string","format":"date"},"property_id":{"type":"string"}}},"flight":{"type":"object","description":"Either provide `flight_option_id` (from `GET /flight-options`) to\nbook a specific flight, or provide `desired_arrival_by` to let\nRoutespring auto-select. Omit the `flight` block entirely to use\nfull auto-booking based on booking configuration.\n","properties":{"flight_option_id":{"type":"string","description":"Token from `GET /flight-options`. When set, Routespring books\nexactly this flight — no auto-selection. Token expires 20 minutes\nafter the search query that produced it.\n"},"desired_arrival_by":{"type":"string","format":"date-time"},"cabin_class":{"type":"string","enum":["ECONOMY","PREMIUM_ECONOMY","BUSINESS","FIRST"]}}}}},"BookingListResponse":{"type":"object","properties":{"bookings":{"type":"array","items":{"$ref":"#/components/schemas/Booking"}},"next_cursor":{"type":["string","null"]}}},"BookingStatusResponse":{"type":"object","properties":{"booking_id":{"type":"string"},"current_status":{"$ref":"#/components/schemas/BookingStatusEnum"},"history":{"type":"array","items":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/BookingStatusEnum"},"changed_at":{"type":"string","format":"date-time"},"reason":{"type":"string"}}}}}},"FlightOption":{"type":"object","description":"A single available flight returned by `GET /flight-options`. Pass\n`flight_option_id` to `PATCH /bookings/{id}` or `POST /bookings/request`\nto book this specific flight instead of letting Routespring auto-select.\n","properties":{"flight_option_id":{"type":"string","example":"of_01HN2X9N1Q4ZJ8P7AKDQRT4F9A","description":"Opaque token identifying this option. Valid for 20 minutes from query time."},"airline_code":{"type":"string","example":"DL"},"flight_number":{"type":"string","example":"DL1234"},"from_airport":{"type":"string","example":"SLC"},"to_airport":{"type":"string","example":"JFK"},"dep_time":{"type":"string","format":"date-time"},"arr_time":{"type":"string","format":"date-time"},"cabin_class":{"type":"string","enum":["ECONOMY","PREMIUM_ECONOMY","BUSINESS","FIRST"]},"fare_name":{"type":"string","example":"Main Cabin","description":"GDS fare-product name for this option (e.g. \"Main Cabin\", \"Economy Basic\")."},"price":{"$ref":"#/components/schemas/Money"},"stops":{"type":"integer","example":0,"description":"Number of intermediate stops (0 = nonstop)."},"available_seats":{"type":"integer","description":"Seats bookable at the quoted fare. May decrease between query and booking."},"preferred_carrier":{"type":"boolean","description":"True if this airline is in the airline's `preferred_airlines` config."}}},"FlightOptionsResponse":{"type":"object","properties":{"options":{"type":"array","items":{"$ref":"#/components/schemas/FlightOption"}},"next_cursor":{"type":["string","null"]},"total_results":{"type":"integer","description":"Total matching options across all pages (approximate)."}}},"AnchorsInput":{"type":"object","required":["traveller_email"],"description":"Airline-meaningful identifiers for an injected action item or\nbooking request. `traveller_email` is required so Routespring can\nresolve the crew member's bookable profile. `employee_id` and the\nother anchors are optional but encouraged when known — they flow\ninto the booking's denormalized index so retrieval by any one is a\nsingle lookup.\n\nNote `pairing_number` is recommended even for IROP-driven bookings\nwhere the original pairing has been disrupted: keeping the link\nlets the airline and Routespring see the IROP-injected booking\nnext to the original pairing's other bookings in the same view.\n","properties":{"traveller_email":{"type":"string","format":"email","example":"jordan.reyes@flymx.com","description":"The crew member's email (their Routespring user login). The\nrequired crew anchor — Routespring resolves the traveller's\nbookable profile from it.\n"},"employee_id":{"type":"string","description":"Airline HR employee id — an optional secondary anchor."},"pairing_number":{"type":"string"},"duty_date":{"type":"string","format":"date"},"flight_number":{"type":"string"}}},"ActionItemInjection":{"type":"object","required":["type","classification","anchors"],"description":"Request body for `POST /action-items`. Either `flight` or `hotel`\nMUST be provided, matching the `type` field — the engine uses\nthese fields to search the supplier pool and apply the active\nconfiguration.\n","oneOf":[{"required":["flight"]},{"required":["hotel"]}],"if":{"required":["source"],"properties":{"source":{"const":"IROP"}}},"then":{"required":["irop_ref"]},"properties":{"type":{"type":"string","enum":["HOTEL","FLIGHT"],"description":"Whether this is a hotel layover booking or a deadhead flight\npositioning requirement. Must match the sub-object provided —\n`HOTEL` requires `hotel`, `FLIGHT` requires `flight`.\n"},"classification":{"type":"string","example":"IROP_NEW_DEADHEAD","description":"Airline-supplied classification. Uses the same vocabulary as\n`ActionItem.classification` — common values for injected items:\n`IROP_NEW_DEADHEAD`, `IROP_REROUTE`, `IROP_HOTEL_EXTEND`,\n`IROP_HOTEL_CANCEL`. For non-IROP injections, use the\nmatching `NEW_LAYOVER` / `NEW_DEADHEAD` / `LAYOVER_EXTENDED`\nvalue. The engine treats injected classifications the same way\nas diff-derived ones for disposition rules.\n"},"priority":{"type":"string","enum":["P1","P2","P3","P4"],"default":"P2","description":"Drives queue ordering. Default `P2`. Bump to `P1` for IROPs\nwith tight SLAs (~5 min to commit). `P3`/`P4` for low-urgency\nbackfills.\n"},"source":{"type":"string","enum":["AIRLINE_INJECTED","IROP"],"default":"AIRLINE_INJECTED","description":"`IROP` enables IROP-specific telemetry (after-action grouping\nby `irop_ref`) and tightens default SLAs. `AIRLINE_INJECTED`\nis the general-purpose injection (ad-hoc additions outside\nboth the diff flow and IROPs).\n"},"irop_ref":{"type":"string","example":"IROP-2026-05-18-EWR-001","description":"Airline-side correlation id. Required when `source: IROP`\nso all artifacts tied to one IROP can be queried together\nvia `GET /bookings?irop_ref=...`.\n"},"anchors":{"$ref":"#/components/schemas/AnchorsInput"},"flight":{"$ref":"#/components/schemas/InjectedFlightContext"},"hotel":{"$ref":"#/components/schemas/InjectedHotelContext"},"manual_override":{"type":"boolean","default":true,"description":"Defaults to `true` — the diff engine will not modify the\nresulting booking on subsequent schedule submissions. Pass\n`false` to release the booking to auto-management immediately\non creation.\n"},"notes":{"type":"string","description":"Free-text context for this injection — reason for the IROP,\noperational notes for the travel manager. Appears on the\nresulting action item.\n"}}},"InjectedFlightContext":{"type":"object","required":["from_airport","to_airport"],"description":"Flight constraints Routespring searches against. The engine applies\nthe active flight config (preferred airlines, cabin,\n`auto_book_max_price`, etc.) to pick a match — or flags\n`PENDING_REVIEW` when no match satisfies the config.\n","properties":{"from_airport":{"type":"string","example":"EWR"},"to_airport":{"type":"string","example":"SLC"},"depart_date":{"type":"string","format":"date","description":"Desired travel date (local at origin)."},"depart_after":{"type":"string","format":"date-time","description":"Earliest acceptable departure (UTC). Useful when crew has a\nlegal minimum rest from a prior duty; the engine will not pick\noptions departing before this instant.\n"},"arrive_by":{"type":"string","format":"date-time","description":"Latest acceptable arrival (UTC). Drives `arrive_by` filtering at the supplier."},"cabin_class":{"type":"string","enum":["ECONOMY","PREMIUM_ECONOMY","BUSINESS","FIRST"],"description":"Optional — overrides the config-level `preferred_cabin` for this request only."}}},"InjectedHotelContext":{"type":"object","required":["city","check_in_date","check_out_date"],"properties":{"city":{"type":"string","example":"SLC","description":"IATA airport code of the layover city. Routespring picks a\npreferred hotel from the airline's `/config/hotels` for this\nbase, in priority order.\n"},"check_in_date":{"type":"string","format":"date"},"check_out_date":{"type":"string","format":"date"},"num_nights":{"type":"integer","description":"Optional — derived from check-in / check-out if omitted."}}},"BookingRequestInput":{"type":"object","required":["type","classification","anchors"],"description":"Request body for `POST /bookings/request`. The airline has\nalready resolved which flight / hotel to book and asks Routespring\nto commit it directly — skipping the engine's auto-select step\nbut still running through validation, supplier booking, and the\nretry pipeline.\n\nEither `flight` or `hotel` MUST be provided, matching the `type`\nfield. For FLIGHT, the typical path is to pass `flight_option_id`\nfrom a prior `GET /flight-options` call.\n","oneOf":[{"required":["flight"]},{"required":["hotel"]}],"if":{"required":["source"],"properties":{"source":{"const":"IROP"}}},"then":{"required":["irop_ref"]},"properties":{"type":{"type":"string","enum":["HOTEL","FLIGHT"]},"classification":{"type":"string","example":"IROP_NEW_DEADHEAD","description":"Airline-supplied classification — same vocabulary as\n`ActionItem.classification`. Recorded on the resulting booking\nfor audit and on the synthesized action item that backs it.\n"},"source":{"type":"string","enum":["AIRLINE_INJECTED","IROP"],"default":"AIRLINE_INJECTED"},"irop_ref":{"type":"string","example":"IROP-2026-05-18-EWR-001","description":"Required when `source: IROP`. Correlates this booking with\nevery other artifact for the same IROP — searchable via\n`GET /bookings?irop_ref=...`.\n"},"anchors":{"$ref":"#/components/schemas/AnchorsInput"},"flight":{"$ref":"#/components/schemas/BookingRequestFlight"},"hotel":{"$ref":"#/components/schemas/BookingRequestHotel"},"manual_override":{"type":"boolean","default":true,"description":"Defaults to `true` — the diff engine will not modify this\nbooking on subsequent schedule submissions, even if the\neventual catch-up roster contradicts it. Pass `false` to\nrelease immediately to auto-management.\n"},"notes":{"type":"string"}}},"BookingRequestFlight":{"type":"object","description":"Two ways to commit a positioning flight, mirroring `BookingRequestHotel`:\n\n- **Pin a specific flight** — pass `flight_option_id` (from a prior\n  `GET /flight-options` call) to commit exactly that offer.\n- **Auto-select** — omit `flight_option_id` and pass `from_airport` +\n  `to_airport` + `depart_date` (optionally `depart_after` / `arrive_by` /\n  `cabin_class`); Routespring searches and books the best matching\n  flight automatically.\n\nThe crew member the ticket is for is taken from `anchors.traveller_email`.\n(Deadhead positioning flights derived from `POST /schedules` are booked\nautomatically by the engine and do not need this call.)\n","properties":{"flight_option_id":{"type":"string","example":"of_01HN2X9N1Q4ZJ8P7AKDQRT4F9A","description":"The offer id returned by `GET /flight-options`. Commits exactly this\noffer for the crew member named in `anchors.traveller_email`, billed\nto the airline (BILL_TO_COMPANY). The offer is held for 20 minutes\nafter the originating search; committing an expired offer fails (the\nbooking comes back `NEEDS_REVIEW`) — re-search and pick a fresh\noption.\n"},"from_airport":{"type":"string","example":"EWR"},"to_airport":{"type":"string","example":"SLC"},"depart_date":{"type":"string","format":"date"},"depart_after":{"type":"string","format":"date-time"},"arrive_by":{"type":"string","format":"date-time"},"cabin_class":{"type":"string","enum":["ECONOMY","PREMIUM_ECONOMY","BUSINESS","FIRST"]}}},"BookingRequestHotel":{"type":"object","required":["city","check_in_date","check_out_date"],"properties":{"city":{"type":"string","example":"SLC","description":"IATA airport code of the layover city. Equal to the operating\nleg's `to_airport` in normal cases; override via the\noriginating leg's `hotel_station_override` when the crew\novernights at a different airport.\n"},"check_in_date":{"type":"string","format":"date"},"check_out_date":{"type":"string","format":"date"},"property_id":{"type":"string","description":"Optional. Pin a specific hotel from the airline's\n`/config/hotels` preferred-hotels list. If omitted, Routespring\nauto-selects per priority order — common for IROP hotel commits\nwhere the airline just needs *a* preferred hotel locked in fast.\n"}}},"RetryJob":{"type":"object","properties":{"retry_job_id":{"type":"string","example":"rtj_01HMXX9N1Q4ZJ8P7AKDQRT4F9M"},"status":{"type":"string","enum":["QUEUED","RUNNING","COMPLETED","FAILED"]},"total":{"type":"integer","description":"Total bookings included in this job"},"succeeded":{"type":"integer"},"failed":{"type":"integer"},"in_flight":{"type":"integer","description":"Bookings currently being attempted"},"pending":{"type":"integer","description":"Bookings still queued"},"started_at":{"type":"string","format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"},"reason":{"type":"string"},"booking_ids":{"type":"array","items":{"type":"string"},"description":"The booking IDs included in this retry job."},"schedule_id":{"type":["string","null"],"description":"Set when the job was created from a filter scoped to a single schedule."},"triggered_by":{"type":"string","description":"Who initiated the retry — an agent's email address for\nUI/API-driven retries, or a system token (e.g. `system:auto-retry`)\nfor automated ones.\n"}}},"RetryJobListResponse":{"type":"object","properties":{"retry_jobs":{"type":"array","items":{"$ref":"#/components/schemas/RetryJob"}},"next_cursor":{"type":["string","null"]}}},"RetryQueueItem":{"type":"object","properties":{"booking_id":{"type":"string"},"retry_count":{"type":"integer"},"max_retries":{"type":"integer"},"last_error_message":{"type":"string"},"next_attempt_at":{"type":"string","format":"date-time"},"anchors":{"$ref":"#/components/schemas/Anchors"}}},"RetryQueueResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/RetryQueueItem"}},"next_cursor":{"type":["string","null"]}}},"HotelSearchResult":{"type":"object","description":"One hotel from Routespring's GDS inventory.","properties":{"property_id":{"type":"string","example":"TVP-JFK-9001","description":"GDS hotel property id. Pass this as `property_id` in\n`PUT /config/hotels`.\n"},"name":{"type":"string","example":"JFK GDS Grand"},"chain":{"type":"string","example":"HILTON"},"city":{"type":"string","example":"JFK"},"address":{"type":"string"}}},"HotelSearchResponse":{"type":"object","properties":{"hotels":{"type":"array","items":{"$ref":"#/components/schemas/HotelSearchResult"}},"next_cursor":{"type":["string","null"]},"total_results":{"type":"integer","description":"Total hotels matching the supplied filters (name / city / airport_code) across all pages."}}},"V2FlightSearchRequest":{"type":"object","required":["legs"],"properties":{"trip_type":{"type":"string","enum":["ONE_WAY","ROUND_TRIP","MULTI_CITY"],"description":"Inferred from number of legs if omitted:\n1 leg = ONE_WAY, 2 legs = ROUND_TRIP, 3+ legs = MULTI_CITY\n"},"cabin_class":{"type":"string","enum":["ECONOMY","PREMIUM_ECONOMY","BUSINESS","FIRST"],"default":"ECONOMY"},"legs":{"type":"array","minItems":1,"maxItems":6,"items":{"$ref":"#/components/schemas/V2LegRequest"}},"travellers":{"$ref":"#/components/schemas/V2TravellersInput"},"traveller_emails":{"type":"array","items":{"type":"string","format":"email"},"description":"Email addresses of travellers (used for policy lookup)"},"preferred_airlines":{"type":"array","items":{"type":"string"},"description":"IATA airline codes to prefer in results"}}},"V2LegRequest":{"type":"object","required":["origin","destination","departure_date"],"properties":{"origin":{"type":"string","description":"IATA airport code","example":"IAD"},"destination":{"type":"string","description":"IATA airport code","example":"SEA"},"departure_date":{"type":"string","format":"date","example":"2026-05-31"}}},"V2TravellersInput":{"type":"object","properties":{"adults":{"type":"integer","minimum":1,"default":1},"children":{"type":"integer","minimum":0,"default":0},"infants":{"type":"integer","minimum":0,"default":0}}},"V2BookFlightRequest":{"type":"object","required":["offer_id","passengers"],"properties":{"offer_id":{"type":"string","description":"Flight offer ID from search results"},"fare_id":{"type":"string","description":"Fare ID from fares endpoint (uses default fare if omitted)"},"search_id":{"type":"string","description":"Search session ID (recommended)"},"trip_name":{"type":"string","description":"Label for this trip","default":"API Booking"},"passengers":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/V2Passenger"}},"payment":{"$ref":"#/components/schemas/V2PaymentInfo"},"emergency_contact":{"$ref":"#/components/schemas/V2EmergencyContact"},"seat_selections":{"type":"array","items":{"$ref":"#/components/schemas/V2SeatSelection"}},"idempotency_key":{"type":"string","description":"Unique key to prevent duplicate bookings on retry"}}},"V2Passenger":{"type":"object","required":["first_name","last_name","gender","date_of_birth","email","phone"],"properties":{"title":{"type":"string","example":"Mr"},"first_name":{"type":"string","example":"Hitesh"},"middle_name":{"type":"string"},"last_name":{"type":"string","example":"Sharma"},"gender":{"type":"string","enum":["male","female","m","f"]},"date_of_birth":{"type":"string","format":"date","example":"1980-02-25"},"email":{"type":"string","format":"email"},"phone":{"type":"string","example":"+919818822639"},"nationality":{"type":"string","description":"ISO 3166-1 alpha-2 country code","example":"IN"},"known_traveller_number":{"type":"string","description":"TSA Known Traveller Number (for US flights)"}}},"V2PaymentInfo":{"type":"object","properties":{"method":{"type":"string","enum":["invoice","company_card","personal_card"],"default":"invoice","description":"- `invoice` / `company_card` → Bill to company\n- `personal_card` → Charge to traveller's card\n"}}},"V2EmergencyContact":{"type":"object","properties":{"name":{"type":"string"},"relationship":{"type":"string"},"phone":{"type":"string"}}},"V2SeatSelection":{"type":"object","required":["origin","destination","carrier","flight_number","seat","passenger_name"],"properties":{"origin":{"type":"string","description":"IATA airport code"},"destination":{"type":"string","description":"IATA airport code"},"carrier":{"type":"string","description":"IATA airline code"},"flight_number":{"type":"string"},"seat":{"type":"string","example":"8A"},"passenger_name":{"type":"string","description":"First name of the passenger"}}},"V2FlightSearchResponse":{"type":"object","properties":{"searchId":{"type":"string","description":"Reference this ID in subsequent fares/seats/booking calls"},"expiresAt":{"type":"string","format":"date-time","description":"Search results expire after this time (~20 min)"},"flights":{"type":"array","items":{"$ref":"#/components/schemas/V2FlightOffer"}}}},"V2FlightOffer":{"type":"object","properties":{"offerId":{"type":"string","description":"Use this to get fares, seats, or to book"},"price":{"$ref":"#/components/schemas/V2Price"},"legs":{"type":"array","items":{"$ref":"#/components/schemas/V2Leg"}},"fareAttributes":{"$ref":"#/components/schemas/V2FareAttributes"},"refundable":{"type":"boolean"},"passportRequired":{"type":"boolean"}}},"V2Leg":{"type":"object","description":"One direction of travel. One-way flights have 1 leg, round trips have 2\n(outbound + return), and multi-city flights have N legs matching the request.\n","properties":{"stops":{"type":"integer","description":"Number of stops (0 = nonstop)"},"segments":{"type":"array","items":{"$ref":"#/components/schemas/V2Segment"}}}},"V2Segment":{"type":"object","properties":{"origin":{"type":"string","description":"IATA airport code"},"destination":{"type":"string","description":"IATA airport code"},"departure":{"type":"string","description":"ISO 8601 datetime with timezone"},"arrival":{"type":"string","description":"ISO 8601 datetime with timezone"},"carrier":{"type":"string","description":"Marketing carrier IATA code"},"operatingCarrier":{"type":"string","description":"Operating carrier IATA code"},"flightNumber":{"type":"string"},"duration":{"type":"integer","description":"Duration in minutes"},"cabinClass":{"type":"string","description":"e.g. Economy, Basic economy, First class"},"fareClass":{"type":"string","description":"e.g. SAVER, MAIN, REFUNDABLE MAIN, FIRST"}}},"V2Price":{"type":"object","properties":{"total":{"type":"number","format":"double"},"base":{"type":"number","format":"double"},"tax":{"type":"number","format":"double"},"currency":{"type":"string","example":"USD"}}},"V2FareAttributes":{"type":"object","description":"Values: `INCLUDED`, `NOTOFFERED`, `AVAILABLEFORCHARGE`, `NOTFOUND`\n","properties":{"checkedBag":{"type":"string"},"carryBag":{"type":"string"},"rebooking":{"type":"string"},"refund":{"type":"string"},"seatSelection":{"type":"string"},"meal":{"type":"string"},"wifi":{"type":"string"},"legRoom":{"type":"string"}}},"V2FaresResponse":{"type":"object","properties":{"offerId":{"type":"string"},"fares":{"type":"array","items":{"$ref":"#/components/schemas/V2FareOption"}}}},"V2FareOption":{"type":"object","properties":{"fareId":{"type":"string","description":"Use this when booking to select this fare"},"fareClass":{"type":"string","description":"e.g. SAVER, MAIN, REFUNDABLE MAIN, FIRST"},"cabinClass":{"type":"string","description":"e.g. Basic economy, Economy, First class"},"price":{"$ref":"#/components/schemas/V2Price"},"fareAttributes":{"$ref":"#/components/schemas/V2FareAttributes"},"refundable":{"type":"boolean"}}},"V2SeatMapResponse":{"type":"object","properties":{"segments":{"type":"array","items":{"$ref":"#/components/schemas/V2SegmentSeatMap"}}}},"V2SegmentSeatMap":{"type":"object","properties":{"origin":{"type":"string"},"destination":{"type":"string"},"carrier":{"type":"string"},"flightNumber":{"type":"string"},"rows":{"type":"array","items":{"$ref":"#/components/schemas/V2SeatRow"}}}},"V2SeatRow":{"type":"object","properties":{"rowNumber":{"type":"integer"},"seats":{"type":"array","items":{"$ref":"#/components/schemas/V2SeatInfo"}}}},"V2SeatInfo":{"type":"object","properties":{"seat":{"type":"string","example":"8A"},"available":{"type":"boolean"},"type":{"type":"string","description":"Window, Middle, or Aisle"},"price":{"type":"number","format":"double","description":"Additional cost for this seat (0 if free)"}}},"V2BookingResponse":{"type":"object","properties":{"bookingRequestId":{"type":"string","description":"Temporary booking request ID (prefix: bkreq_). Use this only to poll\nGET /bookings/{bookingRequestId}/status. Expires after 24 hours.\n"},"status":{"type":"string","enum":["PROCESSING","CONFIRMED","FAILED"]},"pollUrl":{"type":"string","description":"URL to poll for status (only when PROCESSING)"},"pollAfterMs":{"type":"integer","description":"Recommended polling interval in milliseconds"}}},"V2BookingStatusResponse":{"type":"object","properties":{"bookingRequestId":{"type":"string","description":"Same temporary ID from the booking request"},"status":{"type":"string","enum":["PROCESSING","CONFIRMED","FAILED"]},"pollAfterMs":{"type":"integer","description":"Polling interval (only when PROCESSING)"},"confirmation":{"$ref":"#/components/schemas/V2ConfirmationInfo"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/V2ApiError"}}}},"V2ConfirmationInfo":{"type":"object","description":"Present when status is CONFIRMED","properties":{"pnr":{"type":"string","description":"Airline confirmation number","example":"RRQXOB"},"ticketId":{"type":"string","example":"173971000310610936"},"tripId":{"type":"string","example":"589991w75FeU9T"},"transactionId":{"type":"string","example":"IL71furC"},"tripSessionId":{"type":"integer","format":"int64","example":100045745}}},"V2ErrorResponse":{"type":"object","properties":{"error":{"$ref":"#/components/schemas/V2ApiError"},"status":{"type":"integer"}}},"V2ApiError":{"type":"object","properties":{"code":{"type":"string","description":"Machine-readable error code"},"message":{"type":"string","description":"Human-readable error message"}}},"V2CancelBookingRequest":{"type":"object","required":["transaction_id"],"properties":{"transaction_id":{"type":"string","description":"Transaction ID from the booking confirmation response"},"ticket_id":{"type":"string","description":"Ticket ID (optional, for reference)"},"trip_id":{"type":"string","description":"Trip ID (optional, for reference)"}}},"V2CancelBookingResponse":{"type":"object","properties":{"transactionId":{"type":"string"},"status":{"type":"string","enum":["CANCELLED"]},"message":{"type":"string"}}}},"examples":{"ScheduleSubmissionFull":{"summary":"Full roster submission","value":{"external_ref":"MX-20260527-4821","as_of":"2026-05-27T08:00:00Z","schedule_start_date":"2026-05-16","schedule_end_date":"2026-05-31","pairings":[{"pairing_number":"MX-20260527-4821","origin_base":"SLC","terminus_base":"SLC","check_in_local":"2026-05-26T23:30:00-06:00","check_out_local":"2026-05-29T04:00:00-06:00","tafb_hours":52.5,"crew":[{"employee_id":"EMP-1042","name":"Jordan Reyes","rank":"CA","home_base":"SLC","traveller_email":"jreyes@flymx.com","union_code":"ALPA"},{"employee_id":"EMP-2317","name":"Alex Pham","rank":"FO","home_base":"SLC","traveller_email":"apham@flymx.com","union_code":"ALPA"}],"duty_periods":[{"duty_period_num":1,"duty_date":"2026-05-27","fdp_start_local":"2026-05-26T23:30:00-06:00","fdp_end_local":"2026-05-27T12:15:00-04:00","legs":[{"leg_sequence":1,"activity_code":"FLT","flight_number":"MX401","from_airport":"SLC","to_airport":"LAX","dep_time_local":"2026-05-27T01:00:00-06:00","arr_time_local":"2026-05-27T01:30:00-07:00","aircraft_type":"A220-300","aircraft_tail":"N412MX"},{"leg_sequence":2,"activity_code":"FLT","flight_number":"MX588","from_airport":"LAX","to_airport":"IAD","dep_time_local":"2026-05-27T03:00:00-07:00","arr_time_local":"2026-05-27T14:15:00-04:00","aircraft_type":"A220-300","aircraft_tail":"N412MX"}],"layover":{"station":"IAD","booking_required":true,"checkin_time_local":"2026-05-27T14:45:00-04:00","checkout_time_local":"2026-05-28T01:00:00-04:00"}},{"duty_period_num":2,"duty_date":"2026-05-28","fdp_start_local":"2026-05-28T02:00:00-04:00","fdp_end_local":"2026-05-28T09:30:00-05:00","legs":[{"leg_sequence":3,"activity_code":"FLT","flight_number":"MX210","from_airport":"IAD","to_airport":"TUL","dep_time_local":"2026-05-28T04:00:00-04:00","arr_time_local":"2026-05-28T05:45:00-05:00","aircraft_type":"A220-300","aircraft_tail":"N388MX"},{"leg_sequence":4,"activity_code":"DH","from_airport":"TUL","to_airport":"DAL","expected_dep_time_local":"2026-05-28T07:00:00-05:00","expected_arr_time_local":"2026-05-28T08:15:00-05:00","booking_required":true}],"layover":{"station":"DAL","booking_required":true,"checkin_time_local":"2026-05-28T08:45:00-05:00","checkout_time_local":"2026-05-29T00:30:00-05:00"}},{"duty_period_num":3,"duty_date":"2026-05-29","fdp_start_local":"2026-05-29T01:30:00-05:00","fdp_end_local":"2026-05-29T04:00:00-06:00","legs":[{"leg_sequence":5,"activity_code":"FLT","flight_number":"MX305","from_airport":"DAL","to_airport":"SLC","dep_time_local":"2026-05-29T03:00:00-05:00","arr_time_local":"2026-05-29T03:45:00-06:00","aircraft_type":"A220-300","aircraft_tail":"N401MX"}]}]}]}}}}}