{"openapi": "3.1.0", "info": {"title": "Bahn Transfer", "version": "1.0", "description": "Rail re-planning for a traveller already on a delayed train: the best onward options, concrete transfer alternatives with buffer transparency, and a specific recommendation — fast and cheap, instead of the agent researching several routing APIs itself.", "x-guidance": "Rail re-planning for a traveller already on a delayed train. Workflow: (1) GET /api/stations?q=<name> to resolve a stop_id; (2) GET /api/departures?station=<stop_id> for trip_id and live delay; (3) POST /api/transfer-chain with trip_id and the destination stop_id as 'to'. The paid call answers HTTP 402 with a PAYMENT-REQUIRED header (base64 x402 V2 PaymentRequired) when no PAYMENT-SIGNATURE is sent; read amount and payTo from that header and retry with a signed EIP-3009 authorisation. Steps 1 and 2 are free.", "contact": {"name": "Citizen01", "url": "https://1f916.de", "email": "citizen01@1f916.de"}}, "servers": [{"url": "https://bahn.1f916.de"}], "paths": {"/health": {"get": {"operationId": "health", "summary": "Service status (free)", "security": [], "responses": {"200": {"description": "Status, price, rate limit", "content": {"application/json": {"schema": {"type": "object"}}}}}}}, "/mcp": {"post": {"operationId": "mcp", "summary": "MCP transport (JSON-RPC 2.0, Streamable HTTP)", "description": "Model Context Protocol endpoint. Free: initialize, tools/list, search_stations, departures. Paid: tools/call on plan_transfer, with the x402 PaymentPayload in params._meta['x402/payment']. Payment signalling follows the x402 V2 MCP transport spec: an unpaid call returns a tool result with isError=true and the PaymentRequired object in structuredContent (and identically in content[0].text).", "security": [], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object"}}}}, "responses": {"200": {"description": "JSON-RPC response. Notifications answer 202 with no body.", "content": {"application/json": {"schema": {"type": "object"}}}}}}}, "/api/discovery": {"get": {"operationId": "discovery", "summary": "x402 V2 resource metadata (free)", "security": [], "responses": {"200": {"description": "Discovery metadata", "content": {"application/json": {"schema": {"type": "object"}}}}}}}, "/api/stations": {"get": {"operationId": "stations", "summary": "Station search (free)", "security": [], "parameters": [{"name": "q", "in": "query", "required": true, "schema": {"type": "string"}, "description": "Station or city name, e.g. 'Leipzig Hbf' or 'Leipzig'. A bare city name is resolved to that city's main rail hub when exactly one exists, and the answer then carries a `resolution` object. Genuinely ambiguous names (Basel, Frankfurt, Regensburg) still answer 400 with candidates.", "example": "Leipzig Hbf"}], "responses": {"200": {"description": "Matching stations with stop_id. Present only when a heuristic decided: `resolution` names the rule, the chosen station, a prose reason, and the rejected alternatives in the same shape as `results`. An exact match carries no `resolution`.", "content": {"application/json": {"schema": {"type": "object", "properties": {"query": {"type": "string"}, "results": {"type": "array", "items": {"type": "object"}}, "resolution": {"type": "object", "description": "How an ambiguous name was resolved. Absent for a direct match.", "properties": {"rule": {"type": "string"}, "resolved": {"type": "string"}, "reason": {"type": "string"}, "alternatives": {"type": "array", "items": {"type": "object"}}}}, "provenance": {"type": "object"}}}}}}, "400": {"description": "Name matches several stations with no unique hub; `candidates` lists them.", "content": {"application/json": {"schema": {"type": "object"}}}}, "404": {"description": "No station for that name. When the geocoder's closest hit was a local stop ('City, Place'), the message names it so the caller can confirm.", "content": {"application/json": {"schema": {"type": "object"}}}}}}}, "/api/departures": {"get": {"operationId": "departures", "summary": "Departures incl. trip_id and live delay (free)", "security": [], "parameters": [{"name": "station", "in": "query", "required": false, "schema": {"type": "string"}, "description": "A stop id exactly as /api/stations returned it. The id namespaces are provider-specific (`gtfsdefree_…`, `gtfsch_…`, `de-DELFI_…`); an id from another source — an EVA number such as 8000105, or a station name — is not one of them and answers 404 `Unknown Stop ID`. Feed stop ids are renumbered when the feed is republished, so resolve this value per session instead of hardcoding one. Omitted or empty: the reply is 200 describing how to call this route (with a working example), not an error — it carries no `departures` key, so an empty list can never be read as `this train does not run`.", "example": "gtfsdefree_105848"}], "responses": {"200": {"description": "Upcoming departures with `trip_id` (the value POST /api/transfer-chain takes), line, headsigned destination, scheduled/actual time, delay in minutes and cancellation flag. The `provenance` block names the provider and whether the times are realtime or scheduled. When `station` is omitted or empty this is a usage reply with `use`, `description` and `example` and NO `departures` key.", "content": {"application/json": {"schema": {"type": "object"}}}}, "404": {"description": "The stop id is not usable. `error` names which of two distinct causes: `Station Not Found` (the provider knows no such station) or `Unknown Stop ID` (the id belongs to no provider namespace — a station name, an EVA number, or a feed id from a previous publication). Neither is retryable: resolve the station via /api/stations and use the id it returns. The `hint` field says so.", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string", "enum": ["Station Not Found", "Unknown Stop ID"]}, "message": {"type": "string"}, "hint": {"type": "string"}}}}}}, "429": {"description": "Rate limit reached for this client. `Retry-After` carries the wait in seconds. The limit is shared across free endpoints; discovery documents such as /openapi.json do not consume it.", "content": {"application/json": {"schema": {"type": "object"}}}}, "500": {"description": "Upstream answered, but not usably. Not a caller problem and not necessarily transient.", "content": {"application/json": {"schema": {"type": "object"}}}}, "503": {"description": "Upstream unreachable, rate-limited or timing out. Transient by definition; `Retry-After` carries the wait. Distinct from 404 on purpose: a 503 invites a retry, a 404 never does.", "content": {"application/json": {"schema": {"type": "object"}}}}}}}, "/api/transfer-chain": {"post": {"operationId": "transfer_chain", "summary": "Transfer recommendation (paid, x402)", "description": "Costs 10000 atomic units of USDC (1 USDC-cent per query) on eip155:8453.", "tags": ["Travel", "Rail"], "x-payment-info": {"price": {"mode": "fixed", "currency": "USD", "amount": "0.010000"}, "protocols": [{"x402": {}}]}, "parameters": [{"name": "PAYMENT-SIGNATURE", "in": "header", "required": false, "schema": {"type": "string"}, "description": "base64 x402 V2 PaymentPayload. Missing -> HTTP 402 with a PAYMENT-REQUIRED header."}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["trip_id", "to"], "properties": {"trip_id": {"type": "string"}, "to": {"type": "string", "description": "destination stop_id"}, "min_buffer": {"type": "integer", "default": 8}, "manual_delay": {"type": ["integer", "null"]}, "from_stop_name": {"type": ["string", "null"]}, "cache_ttl": {"type": "integer", "default": 1200}}}}}}, "responses": {"200": {"description": "Transfer recommendation", "content": {"application/json": {"schema": {"type": "object"}}}}, "402": {"description": "Payment required; PAYMENT-REQUIRED header carries the requirement", "content": {"application/json": {"schema": {"type": "object"}}}}, "400": {"description": "Caller-side input problem. `reason` names which: `invalid_trip_id` (the trip_id itself is unusable — typo or the journey has ended) or `unknown_destination` (the `to` id is unknown to upstream; resolve it via /api/stations). Nothing was settled; retrying the same id can never succeed.", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string"}, "message": {"type": "string"}, "reason": {"type": "string", "enum": ["invalid_trip_id", "unknown_destination"]}, "retryable": {"type": "boolean", "enum": [false]}, "payment_settled": {"type": "boolean", "enum": [false]}, "hint": {"type": "string"}}}}}}, "500": {"description": "Upstream outage (5xx/timeout), NOT a caller problem. The payment was already verified: no automatic refund, but nothing was settled for an unsettled claim. Retry later with the same authorization while validBefore is open.", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string"}, "message": {"type": "string"}}}}}}, "422": {"description": "Valid request, unfulfillable condition: no connection reaches min_buffer. The body names the earliest reachable option; nothing was settled, so retrying with a smaller min_buffer is free.", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string"}, "message": {"type": "string"}, "reason": {"type": "string", "enum": ["no_safe_recommendation"]}, "retryable": {"type": "boolean"}, "payment_settled": {"type": "boolean"}, "hint": {"type": "string"}}}}}}}}}}}