{"openapi":"3.1.0","info":{"title":"Pricing Resolver Service (PRS) API","version":"0.1.0-demo","description":"**Pricing Resolver Service** — triangulates a bookable hotel rate by reconciling three independent price sources:\n1. **NOVA pre-agreed rate** (read from a CSV-backed simulator until the production NOVA ERP API is wired up).\n2. **Fresh hotel quote** captured via stubbed RFQ email (with stubbed voice-call escalation on silence).\n3. **Public OTA prices** (Booking, Expedia, Trivago) from a stubbed scraper, returning seeded snapshots.\n\nThe reconciliation engine produces a recommendation (`USE_FRESH_QUOTE`, `UNDERCUT_DETECTED_RENEGOTIATE`, `PUBLIC_PRICE_FALLBACK`, etc.) along with pairwise deltas, an overall confidence, and (when triggered) **undercut evidence** for renegotiation.\n\n### Authentication\nSend `X-API-Key: <your_key>` on every request to `/v1/*`. Demo keys are configured in the server's `.env` (see README).\n\n### Idempotency-Key\n`POST /v1/resolutions` requires an `Idempotency-Key` header. It is recorded for client-side request tracing and echoed in audit events; PRS does **not** dedupe on it — every request runs the orchestrator fresh and returns a new `resolution_id`. The same key may be reused across calls.\n\n### Sync vs async\n`POST /v1/resolutions?wait=true` blocks for up to `wait_seconds` (default 60) and returns the full `ResolutionDetail`. Without `wait`, you get a 202 acceptance and the terminal state is delivered to your `webhook_url` (HMAC-signed, header `X-PRS-Signature: sha256=<hex>`).\n\n### Demo simulation\n- See `POST /v1/admin/simulate/email-reply` to inject inbound replies on demand.\n- See `POST /v1/admin/simulate/voice-outcome` to drive the voice branch.\n- See `POST /v1/admin/fast-forward/{resolution_id}` to skip the email silence timer.","contact":{"name":"Europe Incoming Holdings — engineering"},"license":{"name":"Proprietary"}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key"}},"schemas":{}},"paths":{"/healthz":{"get":{"summary":"Liveness probe","tags":["Health"],"description":"Returns 200 when the process is up. No external dependency checks.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"version":{"type":"string"}}}}}}}}},"/readyz":{"get":{"summary":"Readiness probe","tags":["Health"],"description":"Returns 200 when the DB is reachable, fixtures are loadable, and (if configured) Anthropic key is present.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"demo_mode":{"type":"boolean"},"llm_enabled":{"type":"boolean"}}}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"error":{"type":"string"}}}}}}}}},"/":{"get":{"summary":"Service banner with live counters","tags":["Health"],"description":"HTML landing page with quick links + live SQL aggregates fetched from /v1/stats.","responses":{"200":{"description":"Default Response"}}}},"/v1/resolutions":{"post":{"summary":"Create a price resolution (hotel or a service vertical)","tags":["Resolutions"],"description":"Creates one price resolution. The request body is a **discriminated union** — pick the variant via `service_type`: **Hotel** (default when absent), or a service vertical — **Transfer / Car hire / Train / Ferry / Tour (activities & museum tickets) / Chauffeur / Restaurant** — which fan out to every enabled supplier in that vertical and return the cheapest (`service_prices`). Full per-vertical guide: docs/SERVICES_INTEGRATION.md.\n\nHotel workflow (when `service_type` is `hotel` or absent) — starts a resolution for a hotel + stay window.\n\nThe workflow:\n1. Resolves hotel identity across 4 paths (NOVA known / cold / preference-transfer / DMC default).\n2. Fetches NOVA pre-agreed rate (when applicable).\n3. Fans out:\n   - Direct outreach: stubbed RFQ email → on silence escalates to stubbed voice call.\n   - Public-price scrape: stubbed OTA snapshots (Booking, Expedia, Trivago).\n4. Joins both branches → 3-way reconciliation → recommendation (USE_FRESH_QUOTE, UNDERCUT_DETECTED_RENEGOTIATE, PUBLIC_PRICE_FALLBACK, etc.).\n\nPass `?wait=true` for a synchronous response (returns the full ResolutionDetail). Default is async — the response is a 202 acceptance and the terminal state is delivered to `webhook_url` if provided.","requestBody":{"content":{"application/json":{"schema":{"description":"Request body for creating a price resolution.\n\nPRS handles multiple itinerary product types, discriminated by the `service_type` field:\n- `hotel` (default when service_type is absent) — runs the hotel pipeline (NOVA + email/voice + OTA scrape + reconciliation, or FIT bedbanks below 20 pax).\n- `transfer` / `car_hire` / `train` / `ferry` / `tour` / `chauffeur` / `restaurant` — runs the service pipeline: fan out to all enabled suppliers in that vertical in parallel, return cheapest. See docs/SERVICES_INTEGRATION.md for the full per-vertical guide.\n\nEach request resolves a single item. For a multi-item itinerary, Voyager fans out one POST per item.","oneOf":[{"type":"object","title":"Hotel","required":["tour_id","source","hotel","stay","rooms","pax"],"description":"Hotel resolution request. `service_type` is optional here — when absent, PRS treats the call as hotel (back-compat with all pre-2026-05 Voyager integrations).\n\nThe system uses 4 identity paths automatically based on what you send:\n- **A_DMC_KNOWN** — pass `supplier_id` of a known NOVA hotel (or fuzzy-match by name) with `client_specified: true`\n- **B_CLIENT_COLD** — pass `hotel_name + country_code` only; system creates a `pending_supplier` and live-scrapes Google Hotels\n- **C_CLIENT_PREF_TRANSFER** — set `source: \"client_preference_transfer\"` and pass `hotel.preference_transfer_context`\n- **D_DMC_DEFAULT** — pass `supplier_id` with `client_specified: false` (DMC chose the hotel, not the client)","properties":{"tour_id":{"type":"string","description":"NOVA tour identifier this resolution belongs to.","example":"tour_voyager_001"},"inquiry_id":{"type":"string","description":"Optional Voyager inquiry identifier for cross-system linking.","example":"inq_2026_04_30_001"},"source":{"type":"string","enum":["itinerary_builder","intake_form","direct_api","client_preference_transfer"],"description":"Which Voyager surface created this request."},"client_context":{"type":"object","description":"Optional context the inquiry agent can pass through. Used in email/voice templates for warmer outreach.","properties":{"agent_code":{"type":"string","example":"EU-LON-001"},"agent_name":{"type":"string","example":"Sarah Mitchell"},"tour_name":{"type":"string","example":"Imperial Italian Cities — May 2026"},"past_tour_refs":{"type":"array","items":{"type":"string"}}},"additionalProperties":true},"resolution_policy":{"type":"object","description":"Optional per-request behavior overrides. Defaults are demo-friendly.","properties":{"language":{"type":"string","enum":["auto","en","it","fr","de","es","ja","zh"],"default":"auto"},"send_from_mailbox_id":{"type":"string"},"email_silence_minutes":{"type":"integer","default":180},"business_hours_aware":{"type":"boolean","default":true},"voice_max_retries":{"type":"integer","default":2},"voice_retry_interval_minutes":{"type":"integer","default":30},"overall_deadline_hours":{"type":"integer","default":24},"freshness_short_circuit_days":{"type":"integer","default":30},"voice_provider":{"type":"string","enum":["vapi","elevenlabs"],"default":"vapi"},"undercut_threshold_percent":{"type":"number","default":5,"description":"Material undercut threshold; default 5%."}},"additionalProperties":false},"webhook_url":{"type":"string","format":"uri","description":"Where PRS POSTs HMAC-signed events on terminal transitions.","example":"https://voyager.example.com/api/prs-events"},"requested_service_type":{"type":"string","description":"Automatic — set by PRS, not by callers. When `service_type` is a Luna nomenclature code (RST→restaurant, GUI/ENT/ATR→tour, MTC→transfer, FRY→ferry, …), PRS translates it to the internal vertical and records the original code here, echoing it back in the response/webhook so Luna sees its own nomenclature."},"nova_service_id":{"type":"string","description":"Luna/NOVA service item id (e.g. \"item:uuid\"). Cross-reference Luna attaches; PRS captures and echoes it back so Luna can correlate the answer. Also accepted inside the per-vertical block.","example":"item:d07d4603-2ad4-432e-abca-679060b8b0f9"},"service_type":{"type":"string","enum":["hotel"],"description":"Optional; defaults to \"hotel\" when absent."},"hotel":{"type":"object","description":"Identity hint for the hotel. Provide supplier_id when known, or hotel_name + city_code + country_code for fuzzy lookup. The resolver classifies the request into Path A/B/C/D internally. For cold hotels in FIT flow, address + geo dramatically improve bedbank-catalog match accuracy.","properties":{"supplier_id":{"type":"string","description":"NOVA supplier ID if known (Path A/D)."},"novaerp_mongo_id":{"type":"string","description":"NOVA's canonical accommodation ID (mongo ObjectId hex). When provided, PRS matches NOVA's /accommodations/search result by external_ids.novaerp_id directly (confidence 1.0) instead of fuzzy name/postcode. Voyager populates this from its own NOVA lookups."},"hotel_name":{"type":"string"},"city_code":{"type":"string","description":"3-letter IATA-like city code (e.g. LON, ROM, PAR, MAD)."},"city_name":{"type":"string","description":"Plain English city name (e.g. \"Belgrade\", \"Pristina\"). RECOMMENDED — the most reliable city signal. Bedbanks resolve cities by name, so when provided PRS resolves live and is immune to unseeded/variant city codes. Optional; falls back to city_code, then address."},"country_code":{"type":"string","description":"ISO 3166-1 alpha-2 (e.g. GB, IT, FR, ES)."},"address":{"type":"string","description":"Full single-line street address. Used by the FIT bedbank matcher when supplier_id is not pre-mapped."},"postal_code":{"type":"string","description":"Optional postcode/ZIP. Strong tiebreaker for the matcher when a bedbank exposes it (Hotelbeds, TBO)."},"geo":{"type":"object","description":"WGS-84 GPS coordinates. When present, matcher accepts only candidates within a 100m radius — eliminates chain-hotel ambiguity (e.g. multiple Hilton London branches).","required":["latitude","longitude"],"properties":{"latitude":{"type":"number","minimum":-90,"maximum":90},"longitude":{"type":"number","minimum":-180,"maximum":180}},"additionalProperties":false},"client_specified":{"type":"boolean","default":false,"description":"True if the client inquiry explicitly requested this hotel — affects path classification."},"preference_transfer_context":{"type":"object","description":"Provide when source = client_preference_transfer (Path C).","additionalProperties":true,"properties":{"source_tours":{"type":"array","items":{"type":"string"}},"brand":{"type":"string"},"avg_satisfaction":{"type":"number"}}}},"additionalProperties":true},"stay":{"type":"object","required":["check_in","check_out"],"properties":{"check_in":{"type":"string","format":"date","description":"ISO date YYYY-MM-DD"},"check_out":{"type":"string","format":"date"},"nights":{"type":"integer","minimum":1}},"additionalProperties":false},"rooms":{"type":"array","minItems":1,"items":{"type":"object","required":["room_type","qty"],"properties":{"room_type":{"type":"string","enum":["SGL","TWN","DBL","TRP","QUAD","SUITE","FAMILY"]},"qty":{"type":"integer","minimum":1},"meal_plan":{"type":"string","enum":["RO","BB","HB","FB","AI"],"default":"BB"},"occupancy":{"type":"integer"},"notes":{"type":"string"}},"additionalProperties":false},"description":"Room mix to price. Each entry is one room type × qty."},"pax":{"type":"object","properties":{"adults":{"type":"integer","minimum":0},"children":{"type":"integer","minimum":0},"infants":{"type":"integer","minimum":0},"student_rate_eligible":{"type":"integer","minimum":0}},"additionalProperties":false}},"additionalProperties":false,"example":{"tour_id":"tour_voyager_001","source":"itinerary_builder","hotel":{"supplier_id":"SUP_LON_001","hotel_name":"Hilton London Park Lane","city_code":"LON","country_code":"GB"},"stay":{"check_in":"2026-06-15","check_out":"2026-06-19"},"rooms":[{"room_type":"DBL","qty":3,"meal_plan":"BB"}],"pax":{"adults":6},"webhook_url":"https://voyager.example.com/api/prs-events"}},{"type":"object","title":"Transfer (Daytrip)","required":["tour_id","source","service_type","transfer"],"description":"Point-to-point transfer (Daytrip + future suppliers).","properties":{"tour_id":{"type":"string","description":"NOVA tour identifier this resolution belongs to.","example":"tour_voyager_001"},"inquiry_id":{"type":"string","description":"Optional Voyager inquiry identifier for cross-system linking.","example":"inq_2026_04_30_001"},"source":{"type":"string","enum":["itinerary_builder","intake_form","direct_api","client_preference_transfer"],"description":"Which Voyager surface created this request."},"client_context":{"type":"object","description":"Optional context the inquiry agent can pass through. Used in email/voice templates for warmer outreach.","properties":{"agent_code":{"type":"string","example":"EU-LON-001"},"agent_name":{"type":"string","example":"Sarah Mitchell"},"tour_name":{"type":"string","example":"Imperial Italian Cities — May 2026"},"past_tour_refs":{"type":"array","items":{"type":"string"}}},"additionalProperties":true},"resolution_policy":{"type":"object","description":"Optional per-request behavior overrides. Defaults are demo-friendly.","properties":{"language":{"type":"string","enum":["auto","en","it","fr","de","es","ja","zh"],"default":"auto"},"send_from_mailbox_id":{"type":"string"},"email_silence_minutes":{"type":"integer","default":180},"business_hours_aware":{"type":"boolean","default":true},"voice_max_retries":{"type":"integer","default":2},"voice_retry_interval_minutes":{"type":"integer","default":30},"overall_deadline_hours":{"type":"integer","default":24},"freshness_short_circuit_days":{"type":"integer","default":30},"voice_provider":{"type":"string","enum":["vapi","elevenlabs"],"default":"vapi"},"undercut_threshold_percent":{"type":"number","default":5,"description":"Material undercut threshold; default 5%."}},"additionalProperties":false},"webhook_url":{"type":"string","format":"uri","description":"Where PRS POSTs HMAC-signed events on terminal transitions.","example":"https://voyager.example.com/api/prs-events"},"requested_service_type":{"type":"string","description":"Automatic — set by PRS, not by callers. When `service_type` is a Luna nomenclature code (RST→restaurant, GUI/ENT/ATR→tour, MTC→transfer, FRY→ferry, …), PRS translates it to the internal vertical and records the original code here, echoing it back in the response/webhook so Luna sees its own nomenclature."},"nova_service_id":{"type":"string","description":"Luna/NOVA service item id (e.g. \"item:uuid\"). Cross-reference Luna attaches; PRS captures and echoes it back so Luna can correlate the answer. Also accepted inside the per-vertical block.","example":"item:d07d4603-2ad4-432e-abca-679060b8b0f9"},"service_type":{"type":"string","enum":["transfer"]},"transfer":{"type":"object","required":["from","to","pickup_at","pax"],"properties":{"from":{"type":"object","description":"A pickup or dropoff point. Provide address (free text) and/or geo (lat/lng). Adapters that require geo (e.g. Daytrip) will fail without it.","properties":{"address":{"type":"string"},"geo":{"type":"object","required":["latitude","longitude"],"properties":{"latitude":{"type":"number","minimum":-90,"maximum":90},"longitude":{"type":"number","minimum":-180,"maximum":180}},"additionalProperties":false},"iata_code":{"type":"string","description":"IATA airport/city code — preferred for car-hire pickup/return."},"station_code":{"type":"string","description":"UIC station code — skips supplier-side station search for trains."}},"additionalProperties":false},"to":{"type":"object","description":"A pickup or dropoff point. Provide address (free text) and/or geo (lat/lng). Adapters that require geo (e.g. Daytrip) will fail without it.","properties":{"address":{"type":"string"},"geo":{"type":"object","required":["latitude","longitude"],"properties":{"latitude":{"type":"number","minimum":-90,"maximum":90},"longitude":{"type":"number","minimum":-180,"maximum":180}},"additionalProperties":false},"iata_code":{"type":"string","description":"IATA airport/city code — preferred for car-hire pickup/return."},"station_code":{"type":"string","description":"UIC station code — skips supplier-side station search for trains."}},"additionalProperties":false},"pickup_at":{"type":"string","format":"date-time","description":"ISO 8601 timestamp WITH timezone offset (e.g. 2026-06-12T14:30:00+02:00)."},"pax":{"type":"object","properties":{"adults":{"type":"integer","minimum":0},"children":{"type":"integer","minimum":0},"infants":{"type":"integer","minimum":0},"student_rate_eligible":{"type":"integer","minimum":0}},"additionalProperties":false},"luggage":{"type":"integer","minimum":0},"vehicle_preference":{"type":"string","enum":["sedan","van","minibus","coach","any"]}},"additionalProperties":true}},"additionalProperties":false,"example":{"tour_id":"tour_voyager_001","source":"itinerary_builder","service_type":"transfer","transfer":{"from":{"address":"Belgrade Nikola Tesla Airport","geo":{"latitude":44.8184,"longitude":20.3091}},"to":{"address":"Hotel Moskva, Terazije 20, Belgrade","geo":{"latitude":44.8138,"longitude":20.4598}},"pickup_at":"2026-06-12T14:30:00+02:00","pax":{"adults":2},"luggage":2}}},{"type":"object","title":"Car hire (AutoEurope)","required":["tour_id","source","service_type","car_hire"],"description":"Car rental (AutoEurope + future suppliers).","properties":{"tour_id":{"type":"string","description":"NOVA tour identifier this resolution belongs to.","example":"tour_voyager_001"},"inquiry_id":{"type":"string","description":"Optional Voyager inquiry identifier for cross-system linking.","example":"inq_2026_04_30_001"},"source":{"type":"string","enum":["itinerary_builder","intake_form","direct_api","client_preference_transfer"],"description":"Which Voyager surface created this request."},"client_context":{"type":"object","description":"Optional context the inquiry agent can pass through. Used in email/voice templates for warmer outreach.","properties":{"agent_code":{"type":"string","example":"EU-LON-001"},"agent_name":{"type":"string","example":"Sarah Mitchell"},"tour_name":{"type":"string","example":"Imperial Italian Cities — May 2026"},"past_tour_refs":{"type":"array","items":{"type":"string"}}},"additionalProperties":true},"resolution_policy":{"type":"object","description":"Optional per-request behavior overrides. Defaults are demo-friendly.","properties":{"language":{"type":"string","enum":["auto","en","it","fr","de","es","ja","zh"],"default":"auto"},"send_from_mailbox_id":{"type":"string"},"email_silence_minutes":{"type":"integer","default":180},"business_hours_aware":{"type":"boolean","default":true},"voice_max_retries":{"type":"integer","default":2},"voice_retry_interval_minutes":{"type":"integer","default":30},"overall_deadline_hours":{"type":"integer","default":24},"freshness_short_circuit_days":{"type":"integer","default":30},"voice_provider":{"type":"string","enum":["vapi","elevenlabs"],"default":"vapi"},"undercut_threshold_percent":{"type":"number","default":5,"description":"Material undercut threshold; default 5%."}},"additionalProperties":false},"webhook_url":{"type":"string","format":"uri","description":"Where PRS POSTs HMAC-signed events on terminal transitions.","example":"https://voyager.example.com/api/prs-events"},"requested_service_type":{"type":"string","description":"Automatic — set by PRS, not by callers. When `service_type` is a Luna nomenclature code (RST→restaurant, GUI/ENT/ATR→tour, MTC→transfer, FRY→ferry, …), PRS translates it to the internal vertical and records the original code here, echoing it back in the response/webhook so Luna sees its own nomenclature."},"nova_service_id":{"type":"string","description":"Luna/NOVA service item id (e.g. \"item:uuid\"). Cross-reference Luna attaches; PRS captures and echoes it back so Luna can correlate the answer. Also accepted inside the per-vertical block.","example":"item:d07d4603-2ad4-432e-abca-679060b8b0f9"},"service_type":{"type":"string","enum":["car_hire"]},"car_hire":{"type":"object","required":["pickup_location","return_location","pickup_at","return_at","driver_age"],"properties":{"pickup_location":{"type":"string","description":"IATA airport/city code preferred (e.g. LHR, LON). Free text falls back to supplier location search."},"return_location":{"type":"string"},"pickup_at":{"type":"string","format":"date-time"},"return_at":{"type":"string","format":"date-time"},"driver_age":{"type":"integer","minimum":18,"maximum":99},"vehicle_category":{"type":"string","enum":["economy","compact","midsize","fullsize","suv","luxury","van","any"]},"transmission":{"type":"string","enum":["manual","automatic","any"]},"driver_country":{"type":"string","description":"ISO 3166-1 alpha-2 — affects insurance options on some suppliers."}},"additionalProperties":true}},"additionalProperties":false,"example":{"tour_id":"tour_voyager_001","source":"itinerary_builder","service_type":"car_hire","car_hire":{"pickup_location":"LHR","return_location":"LHR","pickup_at":"2026-06-12T09:00:00Z","return_at":"2026-06-19T18:00:00Z","driver_age":35,"vehicle_category":"economy"}}},{"type":"object","title":"Train (Rail Europe)","required":["tour_id","source","service_type","train"],"description":"Train journey (Rail Europe + future suppliers).","properties":{"tour_id":{"type":"string","description":"NOVA tour identifier this resolution belongs to.","example":"tour_voyager_001"},"inquiry_id":{"type":"string","description":"Optional Voyager inquiry identifier for cross-system linking.","example":"inq_2026_04_30_001"},"source":{"type":"string","enum":["itinerary_builder","intake_form","direct_api","client_preference_transfer"],"description":"Which Voyager surface created this request."},"client_context":{"type":"object","description":"Optional context the inquiry agent can pass through. Used in email/voice templates for warmer outreach.","properties":{"agent_code":{"type":"string","example":"EU-LON-001"},"agent_name":{"type":"string","example":"Sarah Mitchell"},"tour_name":{"type":"string","example":"Imperial Italian Cities — May 2026"},"past_tour_refs":{"type":"array","items":{"type":"string"}}},"additionalProperties":true},"resolution_policy":{"type":"object","description":"Optional per-request behavior overrides. Defaults are demo-friendly.","properties":{"language":{"type":"string","enum":["auto","en","it","fr","de","es","ja","zh"],"default":"auto"},"send_from_mailbox_id":{"type":"string"},"email_silence_minutes":{"type":"integer","default":180},"business_hours_aware":{"type":"boolean","default":true},"voice_max_retries":{"type":"integer","default":2},"voice_retry_interval_minutes":{"type":"integer","default":30},"overall_deadline_hours":{"type":"integer","default":24},"freshness_short_circuit_days":{"type":"integer","default":30},"voice_provider":{"type":"string","enum":["vapi","elevenlabs"],"default":"vapi"},"undercut_threshold_percent":{"type":"number","default":5,"description":"Material undercut threshold; default 5%."}},"additionalProperties":false},"webhook_url":{"type":"string","format":"uri","description":"Where PRS POSTs HMAC-signed events on terminal transitions.","example":"https://voyager.example.com/api/prs-events"},"requested_service_type":{"type":"string","description":"Automatic — set by PRS, not by callers. When `service_type` is a Luna nomenclature code (RST→restaurant, GUI/ENT/ATR→tour, MTC→transfer, FRY→ferry, …), PRS translates it to the internal vertical and records the original code here, echoing it back in the response/webhook so Luna sees its own nomenclature."},"nova_service_id":{"type":"string","description":"Luna/NOVA service item id (e.g. \"item:uuid\"). Cross-reference Luna attaches; PRS captures and echoes it back so Luna can correlate the answer. Also accepted inside the per-vertical block.","example":"item:d07d4603-2ad4-432e-abca-679060b8b0f9"},"service_type":{"type":"string","enum":["train"]},"train":{"type":"object","required":["origin_station","dest_station","depart_at","pax"],"properties":{"origin_station":{"type":"string","description":"Station name (free text). PRS hits the supplier station search to resolve."},"origin_station_uic":{"type":"string","description":"Optional UIC station code — skips the search round-trip."},"dest_station":{"type":"string"},"dest_station_uic":{"type":"string"},"depart_at":{"type":"string","format":"date-time"},"return_at":{"type":"string","format":"date-time","description":"Optional — set for round-trip pricing."},"pax":{"type":"object","properties":{"adults":{"type":"integer","minimum":0},"children":{"type":"integer","minimum":0},"infants":{"type":"integer","minimum":0},"student_rate_eligible":{"type":"integer","minimum":0}},"additionalProperties":false},"class":{"type":"string","enum":["standard","first","business"]},"rail_pass":{"type":"string","description":"e.g. eurail_global, swiss_pass. Adapter validates against supplier offerings."}},"additionalProperties":true}},"additionalProperties":false,"example":{"tour_id":"tour_voyager_001","source":"itinerary_builder","service_type":"train","train":{"origin_station":"Paris Gare du Nord","dest_station":"London St Pancras","depart_at":"2026-06-13T10:00:00+02:00","pax":{"adults":2},"class":"standard"}}},{"type":"object","title":"Ferry (phase 1.5)","required":["tour_id","source","service_type","ferry"],"description":"Ferry crossing (Direct Ferries — phase 1.5).","properties":{"tour_id":{"type":"string","description":"NOVA tour identifier this resolution belongs to.","example":"tour_voyager_001"},"inquiry_id":{"type":"string","description":"Optional Voyager inquiry identifier for cross-system linking.","example":"inq_2026_04_30_001"},"source":{"type":"string","enum":["itinerary_builder","intake_form","direct_api","client_preference_transfer"],"description":"Which Voyager surface created this request."},"client_context":{"type":"object","description":"Optional context the inquiry agent can pass through. Used in email/voice templates for warmer outreach.","properties":{"agent_code":{"type":"string","example":"EU-LON-001"},"agent_name":{"type":"string","example":"Sarah Mitchell"},"tour_name":{"type":"string","example":"Imperial Italian Cities — May 2026"},"past_tour_refs":{"type":"array","items":{"type":"string"}}},"additionalProperties":true},"resolution_policy":{"type":"object","description":"Optional per-request behavior overrides. Defaults are demo-friendly.","properties":{"language":{"type":"string","enum":["auto","en","it","fr","de","es","ja","zh"],"default":"auto"},"send_from_mailbox_id":{"type":"string"},"email_silence_minutes":{"type":"integer","default":180},"business_hours_aware":{"type":"boolean","default":true},"voice_max_retries":{"type":"integer","default":2},"voice_retry_interval_minutes":{"type":"integer","default":30},"overall_deadline_hours":{"type":"integer","default":24},"freshness_short_circuit_days":{"type":"integer","default":30},"voice_provider":{"type":"string","enum":["vapi","elevenlabs"],"default":"vapi"},"undercut_threshold_percent":{"type":"number","default":5,"description":"Material undercut threshold; default 5%."}},"additionalProperties":false},"webhook_url":{"type":"string","format":"uri","description":"Where PRS POSTs HMAC-signed events on terminal transitions.","example":"https://voyager.example.com/api/prs-events"},"requested_service_type":{"type":"string","description":"Automatic — set by PRS, not by callers. When `service_type` is a Luna nomenclature code (RST→restaurant, GUI/ENT/ATR→tour, MTC→transfer, FRY→ferry, …), PRS translates it to the internal vertical and records the original code here, echoing it back in the response/webhook so Luna sees its own nomenclature."},"nova_service_id":{"type":"string","description":"Luna/NOVA service item id (e.g. \"item:uuid\"). Cross-reference Luna attaches; PRS captures and echoes it back so Luna can correlate the answer. Also accepted inside the per-vertical block.","example":"item:d07d4603-2ad4-432e-abca-679060b8b0f9"},"service_type":{"type":"string","enum":["ferry"]},"ferry":{"type":"object","required":["origin_port","dest_port","depart_at","pax"],"properties":{"origin_port":{"type":"string"},"dest_port":{"type":"string"},"depart_at":{"type":"string","format":"date-time"},"return_at":{"type":"string","format":"date-time"},"pax":{"type":"object","properties":{"adults":{"type":"integer","minimum":0},"children":{"type":"integer","minimum":0},"infants":{"type":"integer","minimum":0},"student_rate_eligible":{"type":"integer","minimum":0}},"additionalProperties":false},"vehicle":{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["car","motorcycle","van","none"]},"length_m":{"type":"number"},"height_m":{"type":"number"}},"additionalProperties":false},"cabin_preference":{"type":"string","enum":["deck","cabin_inner","cabin_outer","any"]}},"additionalProperties":true}},"additionalProperties":false},{"type":"object","title":"Tour / activities / tickets (Viator + Daytrip + Amadeus + Tiqets)","required":["tour_id","source","service_type","tour"],"description":"Tour / activity / excursion (Viator + Daytrip Day Trips — cheapest-of-all).","properties":{"tour_id":{"type":"string","description":"NOVA tour identifier this resolution belongs to.","example":"tour_voyager_001"},"inquiry_id":{"type":"string","description":"Optional Voyager inquiry identifier for cross-system linking.","example":"inq_2026_04_30_001"},"source":{"type":"string","enum":["itinerary_builder","intake_form","direct_api","client_preference_transfer"],"description":"Which Voyager surface created this request."},"client_context":{"type":"object","description":"Optional context the inquiry agent can pass through. Used in email/voice templates for warmer outreach.","properties":{"agent_code":{"type":"string","example":"EU-LON-001"},"agent_name":{"type":"string","example":"Sarah Mitchell"},"tour_name":{"type":"string","example":"Imperial Italian Cities — May 2026"},"past_tour_refs":{"type":"array","items":{"type":"string"}}},"additionalProperties":true},"resolution_policy":{"type":"object","description":"Optional per-request behavior overrides. Defaults are demo-friendly.","properties":{"language":{"type":"string","enum":["auto","en","it","fr","de","es","ja","zh"],"default":"auto"},"send_from_mailbox_id":{"type":"string"},"email_silence_minutes":{"type":"integer","default":180},"business_hours_aware":{"type":"boolean","default":true},"voice_max_retries":{"type":"integer","default":2},"voice_retry_interval_minutes":{"type":"integer","default":30},"overall_deadline_hours":{"type":"integer","default":24},"freshness_short_circuit_days":{"type":"integer","default":30},"voice_provider":{"type":"string","enum":["vapi","elevenlabs"],"default":"vapi"},"undercut_threshold_percent":{"type":"number","default":5,"description":"Material undercut threshold; default 5%."}},"additionalProperties":false},"webhook_url":{"type":"string","format":"uri","description":"Where PRS POSTs HMAC-signed events on terminal transitions.","example":"https://voyager.example.com/api/prs-events"},"requested_service_type":{"type":"string","description":"Automatic — set by PRS, not by callers. When `service_type` is a Luna nomenclature code (RST→restaurant, GUI/ENT/ATR→tour, MTC→transfer, FRY→ferry, …), PRS translates it to the internal vertical and records the original code here, echoing it back in the response/webhook so Luna sees its own nomenclature."},"nova_service_id":{"type":"string","description":"Luna/NOVA service item id (e.g. \"item:uuid\"). Cross-reference Luna attaches; PRS captures and echoes it back so Luna can correlate the answer. Also accepted inside the per-vertical block.","example":"item:d07d4603-2ad4-432e-abca-679060b8b0f9"},"service_type":{"type":"string","enum":["tour"]},"tour":{"type":"object","required":["country_code","date","pax"],"properties":{"city_code":{"type":"string","description":"Optional. City-based suppliers (Viator, Daytrip, Tiqets) need it; the geo-based supplier (Amadeus museum/attraction tickets) uses geo instead. Provide geo or city_code."},"city_name":{"type":"string","description":"Optional fallback — if city_code is omitted, PRS resolves city_name → city_code (e.g. \"London\" → LON). A caller-supplied city_code always wins."},"country_code":{"type":"string"},"date":{"type":"string","format":"date"},"duration_hours":{"type":"number"},"interests":{"type":"array","items":{"type":"string"}},"pax":{"type":"object","properties":{"adults":{"type":"integer","minimum":0},"children":{"type":"integer","minimum":0},"infants":{"type":"integer","minimum":0},"student_rate_eligible":{"type":"integer","minimum":0}},"additionalProperties":false},"language":{"type":"string","enum":["auto","en","it","fr","de","es","ja","zh"]},"geo":{"type":"object","required":["latitude","longitude"],"properties":{"latitude":{"type":"number","minimum":-90,"maximum":90},"longitude":{"type":"number","minimum":-180,"maximum":180}},"additionalProperties":false},"radius_km":{"type":"number","minimum":1,"maximum":20,"description":"Search radius (km) for geo-based tour suppliers (Amadeus). 1–20, default 5. Ignored by city-based suppliers (Viator, Daytrip). RECOMMENDED — pass the stop coordinates in geo for relevant nearby activities/museum tickets."},"name":{"type":"string","description":"Specific attraction / POI name (e.g. \"British Museum\") when Luna targets a named place. Captured + echoed; used as a search hint."},"nova_service_id":{"type":"string","description":"Luna/NOVA service item id; also accepted at the top level."}},"additionalProperties":true}},"additionalProperties":false},{"type":"object","title":"Chauffeur (Daytrip Hourly)","required":["tour_id","source","service_type","chauffeur"],"description":"Hourly chauffeur (Daytrip Hourly Ride API) — driver + vehicle rented by the hour from a pickup.","properties":{"tour_id":{"type":"string","description":"NOVA tour identifier this resolution belongs to.","example":"tour_voyager_001"},"inquiry_id":{"type":"string","description":"Optional Voyager inquiry identifier for cross-system linking.","example":"inq_2026_04_30_001"},"source":{"type":"string","enum":["itinerary_builder","intake_form","direct_api","client_preference_transfer"],"description":"Which Voyager surface created this request."},"client_context":{"type":"object","description":"Optional context the inquiry agent can pass through. Used in email/voice templates for warmer outreach.","properties":{"agent_code":{"type":"string","example":"EU-LON-001"},"agent_name":{"type":"string","example":"Sarah Mitchell"},"tour_name":{"type":"string","example":"Imperial Italian Cities — May 2026"},"past_tour_refs":{"type":"array","items":{"type":"string"}}},"additionalProperties":true},"resolution_policy":{"type":"object","description":"Optional per-request behavior overrides. Defaults are demo-friendly.","properties":{"language":{"type":"string","enum":["auto","en","it","fr","de","es","ja","zh"],"default":"auto"},"send_from_mailbox_id":{"type":"string"},"email_silence_minutes":{"type":"integer","default":180},"business_hours_aware":{"type":"boolean","default":true},"voice_max_retries":{"type":"integer","default":2},"voice_retry_interval_minutes":{"type":"integer","default":30},"overall_deadline_hours":{"type":"integer","default":24},"freshness_short_circuit_days":{"type":"integer","default":30},"voice_provider":{"type":"string","enum":["vapi","elevenlabs"],"default":"vapi"},"undercut_threshold_percent":{"type":"number","default":5,"description":"Material undercut threshold; default 5%."}},"additionalProperties":false},"webhook_url":{"type":"string","format":"uri","description":"Where PRS POSTs HMAC-signed events on terminal transitions.","example":"https://voyager.example.com/api/prs-events"},"requested_service_type":{"type":"string","description":"Automatic — set by PRS, not by callers. When `service_type` is a Luna nomenclature code (RST→restaurant, GUI/ENT/ATR→tour, MTC→transfer, FRY→ferry, …), PRS translates it to the internal vertical and records the original code here, echoing it back in the response/webhook so Luna sees its own nomenclature."},"nova_service_id":{"type":"string","description":"Luna/NOVA service item id (e.g. \"item:uuid\"). Cross-reference Luna attaches; PRS captures and echoes it back so Luna can correlate the answer. Also accepted inside the per-vertical block.","example":"item:d07d4603-2ad4-432e-abca-679060b8b0f9"},"service_type":{"type":"string","enum":["chauffeur"]},"chauffeur":{"type":"object","required":["pickup","pickup_at","duration_hours","pax"],"description":"Time-based chauffeur: a driver + vehicle rented for a duration from (and back to) the pickup. Pickup needs geo or an IATA code.","properties":{"pickup":{"type":"object","description":"A pickup or dropoff point. Provide address (free text) and/or geo (lat/lng). Adapters that require geo (e.g. Daytrip) will fail without it.","properties":{"address":{"type":"string"},"geo":{"type":"object","required":["latitude","longitude"],"properties":{"latitude":{"type":"number","minimum":-90,"maximum":90},"longitude":{"type":"number","minimum":-180,"maximum":180}},"additionalProperties":false},"iata_code":{"type":"string","description":"IATA airport/city code — preferred for car-hire pickup/return."},"station_code":{"type":"string","description":"UIC station code — skips supplier-side station search for trains."}},"additionalProperties":false},"pickup_at":{"type":"string","format":"date-time","description":"ISO 8601 timestamp WITH timezone offset."},"duration_hours":{"type":"number","minimum":1,"description":"Hours to rent the driver + vehicle."},"pax":{"type":"object","properties":{"adults":{"type":"integer","minimum":0},"children":{"type":"integer","minimum":0},"infants":{"type":"integer","minimum":0},"student_rate_eligible":{"type":"integer","minimum":0}},"additionalProperties":false},"vehicle_preference":{"type":"string","enum":["sedan","van","minibus","coach","any"]}},"additionalProperties":true}},"additionalProperties":false,"example":{"tour_id":"tour_voyager_001","source":"itinerary_builder","service_type":"chauffeur","chauffeur":{"pickup":{"iata_code":"PRG"},"pickup_at":"2026-09-10T10:00:00+02:00","duration_hours":6,"pax":{"adults":3}}}},{"type":"object","title":"Restaurant / dining (TheFork)","required":["tour_id","source","service_type","restaurant"],"description":"Restaurant / dining (TheFork + future suppliers) — priced dining experiences with an optional booking seam.","properties":{"tour_id":{"type":"string","description":"NOVA tour identifier this resolution belongs to.","example":"tour_voyager_001"},"inquiry_id":{"type":"string","description":"Optional Voyager inquiry identifier for cross-system linking.","example":"inq_2026_04_30_001"},"source":{"type":"string","enum":["itinerary_builder","intake_form","direct_api","client_preference_transfer"],"description":"Which Voyager surface created this request."},"client_context":{"type":"object","description":"Optional context the inquiry agent can pass through. Used in email/voice templates for warmer outreach.","properties":{"agent_code":{"type":"string","example":"EU-LON-001"},"agent_name":{"type":"string","example":"Sarah Mitchell"},"tour_name":{"type":"string","example":"Imperial Italian Cities — May 2026"},"past_tour_refs":{"type":"array","items":{"type":"string"}}},"additionalProperties":true},"resolution_policy":{"type":"object","description":"Optional per-request behavior overrides. Defaults are demo-friendly.","properties":{"language":{"type":"string","enum":["auto","en","it","fr","de","es","ja","zh"],"default":"auto"},"send_from_mailbox_id":{"type":"string"},"email_silence_minutes":{"type":"integer","default":180},"business_hours_aware":{"type":"boolean","default":true},"voice_max_retries":{"type":"integer","default":2},"voice_retry_interval_minutes":{"type":"integer","default":30},"overall_deadline_hours":{"type":"integer","default":24},"freshness_short_circuit_days":{"type":"integer","default":30},"voice_provider":{"type":"string","enum":["vapi","elevenlabs"],"default":"vapi"},"undercut_threshold_percent":{"type":"number","default":5,"description":"Material undercut threshold; default 5%."}},"additionalProperties":false},"webhook_url":{"type":"string","format":"uri","description":"Where PRS POSTs HMAC-signed events on terminal transitions.","example":"https://voyager.example.com/api/prs-events"},"requested_service_type":{"type":"string","description":"Automatic — set by PRS, not by callers. When `service_type` is a Luna nomenclature code (RST→restaurant, GUI/ENT/ATR→tour, MTC→transfer, FRY→ferry, …), PRS translates it to the internal vertical and records the original code here, echoing it back in the response/webhook so Luna sees its own nomenclature."},"nova_service_id":{"type":"string","description":"Luna/NOVA service item id (e.g. \"item:uuid\"). Cross-reference Luna attaches; PRS captures and echoes it back so Luna can correlate the answer. Also accepted inside the per-vertical block.","example":"item:d07d4603-2ad4-432e-abca-679060b8b0f9"},"service_type":{"type":"string","enum":["restaurant"]},"restaurant":{"type":"object","required":["country_code","date","party_size"],"description":"Dining for an itinerary stop — focus on PRICED experiences (set/tasting menus, chef's table, events). Located by geo (lat/lng + optional radius_km) and/or city_code; provide at least one. name targets a specific venue.","properties":{"city_code":{"type":"string","description":"Optional — restaurant search is geo-based; provide geo or city_code."},"city_name":{"type":"string","description":"Optional fallback — if city_code is omitted, PRS resolves city_name → city_code. A caller-supplied city_code always wins."},"country_code":{"type":"string","description":"ISO 3166-1 alpha-2."},"geo":{"type":"object","required":["latitude","longitude"],"properties":{"latitude":{"type":"number","minimum":-90,"maximum":90},"longitude":{"type":"number","minimum":-180,"maximum":180}},"additionalProperties":false},"date":{"type":"string","format":"date"},"time":{"type":"string","description":"Preferred seating time, 'HH:mm' (24h)."},"party_size":{"type":"integer","minimum":1,"description":"Number of covers / guests."},"cuisine":{"type":"array","items":{"type":"string"}},"experience_type":{"type":"string","enum":["standard","set_menu","tasting_menu","chefs_table","event","any"]},"price_tier":{"type":"string","enum":["$","$$","$$$","$$$$"]},"language":{"type":"string","enum":["auto","en","it","fr","de","es","ja","zh"]}},"additionalProperties":true}},"additionalProperties":false,"example":{"tour_id":"tour_voyager_001","source":"itinerary_builder","service_type":"restaurant","restaurant":{"city_code":"PAR","country_code":"FR","date":"2026-05-16","time":"20:00","party_size":2,"experience_type":"tasting_menu","cuisine":["french"]}}}]}}},"description":"Request body for creating a price resolution.\n\nPRS handles multiple itinerary product types, discriminated by the `service_type` field:\n- `hotel` (default when service_type is absent) — runs the hotel pipeline (NOVA + email/voice + OTA scrape + reconciliation, or FIT bedbanks below 20 pax).\n- `transfer` / `car_hire` / `train` / `ferry` / `tour` / `chauffeur` / `restaurant` — runs the service pipeline: fan out to all enabled suppliers in that vertical in parallel, return cheapest. See docs/SERVICES_INTEGRATION.md for the full per-vertical guide.\n\nEach request resolves a single item. For a multi-item itinerary, Voyager fans out one POST per item."},"parameters":[{"schema":{"type":"boolean","default":false},"in":"query","name":"wait","required":false,"description":"If true, blocks for up to wait_seconds (default 60) and returns the full ResolutionDetail rather than a 202 acceptance."},{"schema":{"type":"integer","default":60,"minimum":5,"maximum":120},"in":"query","name":"wait_seconds","required":false},{"schema":{"type":"string"},"in":"header","name":"idempotency-key","required":true,"description":"Required. Echoed in audit events for client-side request tracing. PRS does NOT dedupe on this value — every request runs the orchestrator fresh and returns a new resolution_id. Same key may be reused across calls."},{"schema":{"type":"string"},"in":"header","name":"x-api-key","required":false}],"responses":{"200":{"description":"Sync mode: ResolutionDetail.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"Sync mode: ResolutionDetail."}}}},"202":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"resolution_id":{"type":"string"},"status":{"type":"string","enum":["ACCEPTED"]},"created_at":{"type":"string","format":"date-time"},"estimated_completion_at":{"type":"string","format":"date-time"}}}}}}}},"get":{"summary":"List resolutions","tags":["Resolutions"],"parameters":[{"schema":{"type":"string"},"in":"query","name":"tour_id","required":false},{"schema":{"type":"string","enum":["IN_PROGRESS","COMPLETED","FAILED","CANCELLED"]},"in":"query","name":"status","required":false},{"schema":{"type":"string","format":"date-time"},"in":"query","name":"since","required":false},{"schema":{"type":"integer","default":50,"minimum":1,"maximum":200},"in":"query","name":"limit","required":false}],"responses":{"200":{"description":"Default Response"}}}},"/v1/resolutions/{id}":{"get":{"summary":"Get a specific resolution (full detail + audit events)","tags":["Resolutions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/v1/resolutions/{id}/cancel":{"post":{"summary":"Cancel an in-flight resolution","tags":["Resolutions"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string"}}}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/v1/resolutions/{id}/events":{"get":{"summary":"Audit trail for a resolution","tags":["Resolutions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/v1/resolutions/{id}/public-prices":{"get":{"summary":"Public OTA price snapshot for this resolution","tags":["Resolutions"],"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/v1/resolutions/{id}/renegotiate":{"post":{"summary":"Trigger renegotiation flow (manual or automated voice bot)","tags":["Resolutions"],"description":"Logs an intent to renegotiate. In the demo build only `manual_logged` is supported; `automated_voice` is reserved for production.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["mode"],"properties":{"mode":{"type":"string","enum":["manual_logged","automated_voice"]},"language":{"type":"string"},"notes_for_agent":{"type":"string"}}}}},"required":true},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/v1/hotels/lookup":{"post":{"summary":"Pre-flight hotel identity lookup","tags":["Hotels"],"description":"Classifies a hotel hint into Path A/B/C/D without starting a resolution workflow. Useful for the Voyager UI to show the agent which path will be taken before they commit.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["hotel_name"],"properties":{"hotel_name":{"type":"string"},"city_code":{"type":"string"},"country_code":{"type":"string"},"supplier_id":{"type":"string"},"source":{"type":"string","enum":["itinerary_builder","intake_form","direct_api","client_preference_transfer"],"default":"direct_api"},"client_specified":{"type":"boolean","default":false}},"additionalProperties":false}}},"required":true},"responses":{"200":{"description":"Default Response"}}}},"/v1/admin/simulate/email-reply":{"post":{"summary":"Inject a simulated email reply for an in-flight resolution","tags":["Admin (demo)"],"description":"Simulates an inbound hotel reply. Useful when DEMO_EMAIL_SILENCE_SECONDS is large or you want to drive specific reply types from Postman. Fires only if the resolution is currently waiting on a reply.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["resolution_id","fixture"],"properties":{"resolution_id":{"type":"string"},"fixture":{"type":"object","required":["fixture"],"properties":{"fixture":{"type":"string","enum":["auto_quote","auto_ack_then_quote","silent","decline","custom"]},"custom_body":{"type":"string"},"rate_hint":{"type":"number"},"currency_hint":{"type":"string"},"hotel_name":{"type":"string"}},"additionalProperties":false}},"additionalProperties":false}}},"required":true},"responses":{"200":{"description":"Default Response"}}}},"/v1/admin/simulate/voice-outcome":{"post":{"summary":"Inject a simulated voice-call outcome","tags":["Admin (demo)"],"description":"Overrides the next stubbed voice-call outcome for the given resolution. The override is consumed when the orchestrator next reaches the voice step.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["resolution_id","fixture"],"properties":{"resolution_id":{"type":"string"},"fixture":{"type":"object","required":["fixture"],"properties":{"fixture":{"type":"string","enum":["quote","will_email","no_answer","declined"]},"rate_hint":{"type":"number"},"currency_hint":{"type":"string"}},"additionalProperties":false}},"additionalProperties":false}}},"required":true},"responses":{"200":{"description":"Default Response"}}}},"/v1/admin/warmup":{"post":{"summary":"Pre-warm OTA cache for a list of hotels","tags":["Admin (demo)"],"description":"Fan out resolutions in parallel to populate the 24h OTA cache. Replaces the bash warm-up loop with a single API call. Use a `preset` for the curated lists, or pass `hotels[]` for arbitrary entries.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"preset":{"type":"string","enum":["p0","famous_european","all_seeded"],"description":"p0 = Hilton London + Quirinale Rome + The Savoy. famous_european = Sacher Vienna + Cipriani + Bristol Paris + Adlon + Negresco + de Russie. all_seeded = all 11 seeded hotels."},"hotels":{"type":"array","description":"Explicit list of hotels to warm. Each entry follows the resolution-request shape.","items":{"type":"object","required":["stay"],"properties":{"supplier_id":{"type":"string"},"hotel_name":{"type":"string"},"city_code":{"type":"string"},"country_code":{"type":"string"},"stay":{"type":"object","required":["check_in","check_out"],"properties":{"check_in":{"type":"string","format":"date"},"check_out":{"type":"string","format":"date"}}},"rooms":{"type":"array","items":{"type":"object","additionalProperties":true}},"pax":{"type":"object","additionalProperties":true}}}},"stay":{"type":"object","description":"Default stay window for preset entries (overridden per-hotel if entry has its own stay).","properties":{"check_in":{"type":"string","format":"date","default":"2026-08-15"},"check_out":{"type":"string","format":"date","default":"2026-08-19"}}}}}}}},"responses":{"200":{"description":"Default Response"}}}},"/v1/admin/fast-forward/{resolution_id}":{"post":{"summary":"Force the email-silence timer to fire immediately","tags":["Admin (demo)"],"description":"Skips waiting on the email silence timer. The orchestrator will treat the email as silent and escalate to voice immediately.","parameters":[{"schema":{"type":"string"},"in":"path","name":"resolution_id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/v1/stats":{"get":{"summary":"Aggregate counters across all resolutions","tags":["Stats"],"description":"Live SQL aggregations of the resolutions table — useful for dashboards, monitoring, and the live banner on /. Public (no auth).","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/v1/places/cities":{"get":{"summary":"Resolve a city name → IATA city code","tags":["Places"],"description":"Substring search over the PRS city catalogue (the table the tour/restaurant city-based suppliers resolve against). Use to turn \"London\" into the city_code \"LON\".","parameters":[{"schema":{"type":"string"},"in":"query","name":"q","required":true,"description":"City name (or code) to search."},{"schema":{"type":"integer","minimum":1,"maximum":50,"default":10},"in":"query","name":"limit","required":false}],"responses":{"200":{"description":"Default Response"}}}},"/v1/places/airports":{"get":{"summary":"Resolve an airport name → IATA code + coordinates","tags":["Places"],"description":"Substring search over ≈9k IATA airports (OurAirports). Returns iata_code + lat/lng — useful for transfer/chauffeur/car_hire endpoints.","parameters":[{"schema":{"type":"string"},"in":"query","name":"q","required":true,"description":"Airport name, city, or IATA code."},{"schema":{"type":"integer","minimum":1,"maximum":50,"default":10},"in":"query","name":"limit","required":false}],"responses":{"200":{"description":"Default Response"}}}},"/guide":{"get":{"summary":"Voyager integration tutorial — hotels (HTML)","tags":["Docs"],"description":"Comprehensive tutorial for integrating PRS hotel flows into Voyager: architecture, auth, sync vs async, webhook HMAC, recommendation handling, cold-hotel flow, error envelopes. Source: docs/VOYAGER_INTEGRATION_GUIDE.md (raw at /guide.md). For the non-hotel service verticals, see /guide/services.","responses":{"200":{"description":"Default Response"}}}},"/guide.md":{"get":{"summary":"Voyager integration tutorial — hotels (raw markdown)","tags":["Docs"],"responses":{"200":{"description":"Default Response"}}}},"/guide/services":{"get":{"summary":"Service verticals integration guide (HTML)","tags":["Docs"],"description":"Integration guide for the non-hotel verticals: per-vertical request/response, sync vs async flow, the service_prices bundle + failure reasons, booking seam, gotchas, and live-status matrix. Source: docs/SERVICES_INTEGRATION.md (raw at /guide/services.md).","responses":{"200":{"description":"Default Response"}}}},"/guide/services.md":{"get":{"summary":"Service verticals integration guide (raw markdown)","tags":["Docs"],"responses":{"200":{"description":"Default Response"}}}},"/guide/superset":{"get":{"summary":"Luna payload superset (HTML)","tags":["Docs"],"description":"The union of every field across all verticals + required minimums, so Luna sends one rich payload and PRS extracts what each supplier needs. Source: docs/LUNA_PAYLOAD_SUPERSET.md (raw at /guide/superset.md).","responses":{"200":{"description":"Default Response"}}}},"/guide/superset.md":{"get":{"summary":"Luna payload superset (raw markdown)","tags":["Docs"],"responses":{"200":{"description":"Default Response"}}}}},"servers":[{"url":"/"}],"security":[{"ApiKeyAuth":[]}],"tags":[{"name":"Resolutions","description":"Create, fetch, cancel, and audit price resolutions."},{"name":"Hotels","description":"Pre-flight hotel identity lookup."},{"name":"Admin (demo)","description":"Inject simulated inbound events to drive the state machine without waiting."},{"name":"Health","description":"Liveness and readiness."}]}