HTTP API

/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).

Base URL

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.

Conventions


Endpoints

GET /health

Server 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/booking

Create a staging booking. Drives the full pipeline: journey search → cart → hold → checkout. Synchronous; 10–30 s typical.

Request bodyBookingRequest (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-link

Build 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/cancel

Cancel 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 bodyCancelRequest (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-vars

Status 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/refresh

Server-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.


OPTIONS / CORS

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

Other unified-server routes

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)