{"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\n`POST /v1/resolutions` requires an `Idempotency-Key` header. Re-submitting the same key returns the existing resolution rather than starting a new one.\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","tags":["Health"],"description":"Quick links to docs and openapi spec.","responses":{"200":{"description":"Default Response"}}}},"/v1/resolutions":{"post":{"summary":"Create a price resolution request","tags":["Resolutions"],"description":"Starts a new resolution workflow 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":{"type":"object","required":["tour_id","source","hotel","stay","rooms","pax"],"properties":{"tour_id":{"type":"string","description":"NOVA tour identifier this resolution belongs to."},"inquiry_id":{"type":"string"},"source":{"type":"string","enum":["itinerary_builder","intake_form","direct_api","client_preference_transfer"]},"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.","properties":{"supplier_id":{"type":"string","description":"NOVA supplier ID if known (Path A/D)."},"hotel_name":{"type":"string"},"city_code":{"type":"string","description":"3-letter IATA-like city code (e.g. LON, ROM, PAR, MAD)."},"country_code":{"type":"string","description":"ISO 3166-1 alpha-2 (e.g. GB, IT, FR, ES)."},"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}},"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},"client_context":{"type":"object","properties":{"agent_code":{"type":"string"},"agent_name":{"type":"string"},"tour_name":{"type":"string"},"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 to POST signed events on terminal transitions (reconciled / undercut_detected / failed)."}},"additionalProperties":false}}},"required":true},"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. Re-submitting the same key returns the existing resolution."},{"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"}}}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","default":"about:blank"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"resolution_id":{"type":"string"}}}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","default":"about:blank"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"resolution_id":{"type":"string"}}}}}}}},"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/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"}}}}},"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."}]}