/api/* is served directly by Bun (src/server/api.ts) so it stays available even when the Next.js subprocess is down. Every route delegates to a framework-agnostic core under src/api/* that the equivalent Next.js routes also import — Bun and Next behave identically.
CORS is enabled (Access-Control-Allow-Origin: *); no auth (staging only).
| Where | Default |
|---|---|
| Unified server | http://localhost:4242 |
| Next.js dev | http://localhost:3000 |
| Electron app | random local port (the app loads /ui from it) |
In Postman, the DOBBY_SERVER collection variable points at this base.
Content-Type: application/json.YYYY-MM-DD (UTC).7015400 for London St Pancras). The MCP tool eurostar_list_stations is the easiest way to look these up; the STATIONS source is the authoritative list.{ "success": false, "error": "..." } (or { "error": "..." } on routes that don't carry a success flag) plus an HTTP status code.GET /healthServer health + version. (/healthz is kept as a quiet alias for backward compatibility - some Google front-ends reserve /healthz for platform checks and don't pass it through.)
curl http://localhost:4242/health
Response:
{
"ok": true,
"name": "booking-bot",
"version": "1.0.38",
"uptimeSeconds": 1234
}
POST /api/bookingCreate a staging booking. Drives the full pipeline: journey search → cart → hold → checkout. Synchronous; 10–30 s typical.
Request body — BookingRequest (see app/types/api.ts for the source of truth):
| Field | Type | Required | Notes |
|---|---|---|---|
origin |
string | yes | UIC code |
destination |
string | yes | UIC code |
outboundDate |
string | yes | YYYY-MM-DD |
inboundDate |
string | no | YYYY-MM-DD. Provide for a return journey. |
passengers.adults |
number | yes | 0–9 |
passengers.children |
number | yes | 0–9 |
passengers.seniors |
number | yes | 0–9 |
passengers.youths |
number | yes | 0–9 |
passengers.infants |
number | yes | 0–9 |
passengers.adultsWheelchair |
number | yes | 0–1. Use INSTEAD of adults, not in addition. |
passengers.childrenWheelchair |
number | yes | 0–1. Use INSTEAD of children, not in addition. |
passengers.wheelchairCompanions |
number | yes | 0–1. Only valid alongside adultsWheelchair or childrenWheelchair. |
passengers.guideDogs |
number | yes | 0–9 |
market |
enum | yes | uk-en, fr-fr, be-fr, be-en, be-nl, de-de, nl-nl, en-US, en-RW. Sets currency + language defaults. |
currency |
enum | no | GBP, EUR, USD — overrides the market's currency. |
language |
enum | no | en, fr, de, nl — overrides the market's language. |
outClass |
array of STANDARD / PLUS / PREMIER |
no | Outbound class preference. Cheapest if omitted. |
outTrain |
string | no | Specific outbound train number (e.g. "9012"). |
outTime |
string | no | HH:MM. Selects the first journey at or after this time. |
outNth |
number | no | 0 = first journey of the day, -1 = last, 1 = second, etc. |
rtnClass, rtnTrain, rtnTime, rtnNth |
same as outbound | no | Inbound selection. |
api |
boolean | no | Run the Advanced Passenger Information flow with test passport data after the booking. |
snap |
boolean | no | Snap-app inventory: LASTMIN, direct trains, STANDARD only. |
unallocated |
boolean | no | Unallocated inventory: RED_US, direct trains, UNASSIGNED class. |
productFamily |
string | no | Override the productFamilies value used in journey search (e.g. "LASTMIN", "RED_US"). |
shopperEmail |
string | no | Override shopper email. Supports placeholders: {carrier}, {scenario}, {market}, {pax}, {od}. |
cid |
string | no | CID template. Supports {carrier}, {scenario}, {datetime}, {type}, {pax}, {od}, {i}, {market}. |
cancel |
object | no | Chain a cancellation onto this booking. cancel.method is "CC" (default) or "VOUCHER". Mirrors --cancel --refund-method on the CLI; the standalone form is POST /api/cancel. |
Example:
curl -X POST http://localhost:4242/api/booking \
-H "content-type: application/json" \
-d '{
"origin": "7015400",
"destination": "8727100",
"outboundDate": "2026-12-01",
"passengers": {
"adults": 1, "children": 0, "seniors": 0, "youths": 0, "infants": 0,
"adultsWheelchair": 0, "childrenWheelchair": 0, "wheelchairCompanions": 0, "guideDogs": 0
},
"market": "uk-en",
"outClass": ["STANDARD"]
}'
Success response (HTTP 200) — BookingResponse (see app/types/api.ts):
{
"success": true,
"bookingReference": "ABC123",
"cartId": "kNWbdOYLr6j4wGXE",
"links": { "myb": "https://...", "voyager": "https://..." },
"timings": { "search": 616, "createCart": 71, "holdCart": 519, "checkout": 1234 },
"selectedJourney": {
"outbound": {
"legs": [
{
"origin": "London St Pancras Int'l",
"destination": "Paris Gare du Nord",
"departureDate": "2026-12-01",
"departureTime": "18:01",
"arrivalTime": "21:17",
"trainNumber": "9046",
"classOfService": "STANDARD"
}
],
"totalPrice": 56.5
}
}
}
Error responses:
| Status | Cause | Shape |
|---|---|---|
| 400 | Invalid JSON body | { "success": false, "error": "Invalid JSON body" } |
| 400 | Schema validation failed | { "success": false, "error": "Invalid request", "details": <zod format> } |
| 405 | Wrong HTTP method | { "success": false, "error": "Method not allowed" } |
| 500 | Booking pipeline failed (any stage) | { "success": false, "error": "<message>" } |
| 500 | Checkout vars missing and URL fetch failed (UNATTENDED=true server mode) |
{ "success": false, "error": "Checkout variables are missing..." } |
POST /api/search-linkBuild a deep-link URL into eurostar.com that opens the same search the booking would run. Useful for "preview the journey before booking it".
Request body: identical to /api/booking.
Example:
curl -X POST http://localhost:4242/api/search-link \
-H "content-type: application/json" \
-d '{ "origin": "7015400", "destination": "8727100", "outboundDate": "2026-12-01",
"passengers": { "adults": 1, "children": 0, "seniors": 0, "youths": 0, "infants": 0,
"adultsWheelchair": 0, "childrenWheelchair": 0, "wheelchairCompanions": 0, "guideDogs": 0 },
"market": "uk-en" }'
Success response (HTTP 200):
{ "url": "https://staging.eurostar.com/search/uk-en?origin=7015400&destination=8727100&outbound=2026-12-01&adult=1" }
Error responses:
| Status | Cause | Shape |
|---|---|---|
| 400 | Invalid JSON body | { "error": "Invalid JSON body" } |
| 405 | Wrong HTTP method | { "success": false, "error": "Method not allowed" } |
| 500 | URL generation failed | { "error": "<message>" } |
POST /api/cancelCancel an existing staging booking by reference + surname. Mirrors the CLI's --cancel --pnr <X> --surname <Y> flow — and is the endpoint the Web UI's Advanced → Cancel form calls.
Request body — CancelRequest (see app/types/api.ts):
| Field | Type | Required | Notes |
|---|---|---|---|
reference |
string | yes | Booking reference / PNR. |
lastName |
string | yes | Surname on the booking. |
market |
enum | yes | Same set as /api/booking. |
method |
enum | no | "CC" (refund to original card, default) or "VOUCHER". |
trains |
object | no | Restrict refund scope: { outbound: string[], inbound: string[], outboundLegIndex?, inboundLegIndex? }. |
noRefundItemRefs |
object | no | Item refs to exclude from the refund: { outbound: string[], inbound: string[] }. |
gateway |
string | no | Gateway override (PR number or full URL). |
cookies |
object | no | Per-request cookies (e.g. feature flags). |
headers |
object | no | Per-request HTTP headers. |
Example:
curl -X POST http://localhost:4242/api/cancel \
-H "content-type: application/json" \
-d '{ "reference": "ABC123", "lastName": "Smith", "market": "uk-en", "method": "CC" }'
Success response (HTTP 200) — CancelResponse:
{
"success": true,
"reference": "ABC123",
"result": { "kind": "card", "bookingReference": "ABC123", "lastName": "Smith" },
"timings": { "bookingLogin": 412, "bookingBySession": 230, "refundProposal": 318, "refund": 905 }
}
For voucher refunds, result.kind is "voucher" and the payload includes value and voucher.{reference,expiryDate}.
Error responses:
| Status | Cause | Shape |
|---|---|---|
| 400 | Schema validation failed | { "success": false, "error": "Invalid request", "details": <zod format> } |
| 4xx/5xx | Refund pipeline failed | { "success": false, "error": "<message>" } |
GET /api/checkout-varsStatus of the local variables/checkout.json file used in the final checkout mutation. Used by the UI's "are we ready to book?" indicator.
Response (HTTP 200):
{
"exists": true,
"fresh": true,
"contentValid": true,
"valid": true,
"age": 12345,
"debug": {
"resolvedPath": "/Users/me/.booking-bot/variables/checkout.json",
"homePath": "/Users/me/.booking-bot/variables/checkout.json",
"cwdPath": "/Users/me/eil/booking-bot/variables/checkout.json",
"homeExists": true,
"cwdExists": false,
"resolvedExists": true,
"cwd": "/Users/me/eil/booking-bot"
}
}
| Field | Meaning |
|---|---|
exists |
The resolved file exists on disk |
fresh |
mtime within CHECKOUT_VARIABLES_MAX_AGE_MS (default 12 hours) |
contentValid |
parses as JSON and contains checkout.payment |
valid |
exists && fresh && contentValid |
age |
seconds since the file was last modified (or null) |
debug |
resolved + candidate paths and which exist |
POST /api/checkout-vars/refreshServer-Sent Events stream that spawns bun refresh (Playwright) and forwards parsed progress events. Used by the UI's "Refresh credentials" button.
Request body (all fields optional):
{ "showBrowser": false, "forceRefresh": true }
Response: Content-Type: text/event-stream. Each line is data: <JSON>\n\n where the JSON has a type discriminator:
type |
When | Other fields |
|---|---|---|
status |
progress message | message |
debug |
extra info from the script | message or arbitrary keys |
complete |
refresh finished successfully | success: true, message |
error |
refresh failed | message, optional details |
Example:
curl -N -X POST http://localhost:4242/api/checkout-vars/refresh \
-H "content-type: application/json" \
-d '{ "showBrowser": false, "forceRefresh": true }'
data: {"type":"status","message":"Starting refresh process..."}
data: {"type":"status","message":"Searching for journeys..."}
data: {"type":"status","message":"Cart created, starting browser..."}
data: {"type":"complete","success":true,"message":"Credentials refreshed successfully"}
Errors: delivered as { "type": "error", "message": "..." } events on the stream itself. The HTTP status is always 200 once the stream starts.
Every /api/* route accepts OPTIONS and replies with the CORS preflight headers:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type
These aren't /api/* but they're part of the same server — see server.md for the full routing table:
| Path | What it is |
|---|---|
/ |
Landing page listing every interface |
/ui |
Next.js Web UI (proxied to the standalone subprocess) |
/mcp |
Streamable HTTP MCP transport |
/slack/events |
Slack Events webhook (slack.md) |
/docs/ |
This documentation, rendered as HTML |
/dobby-browser-extension.zip |
Chrome extension download (extension.md) |
/dobby-postman-collection.json |
Importable Postman collection (postman.md) |