# Viaferry booking API guide for AI agents Viaferry (viaferry.com) sells ferry tickets across many operators. This guide describes the public JSON API an agent can use to search live availability, assemble a booking, and hand its human user a payment link. It is the same API the viaferry.com frontend uses. ## Basics - Base URL: `https://viaferry.com/api/v1` (also served at `https://api.viaferry.com/api/v1`) - No authentication: no API key, no bearer token, no cookies - Requests and responses are JSON (`Content-Type: application/json`); the ticket PDF endpoint returns binary PDF - Money values are integers in the ISO 4217 minor unit (`1234` = EUR 12.34; IDR has no minor unit, so `234460` = IDR 234,460). Never do your own FX or rounding — use `PUT /cart/{sessionId}/currency` to convert - All IDs (session, sailing, route, company, cart item, order) are opaque strings; never parse them - `robots.txt` disallows crawling `/api/` — that applies to bulk indexing, not to the documented programmatic calls below. Please send a descriptive `User-Agent` and keep request rates modest ## Session contract One opaque `sessionId` binds the whole flow: 1. `POST /search` returns a `sessionId` (generated if you omit it). Keep it. 2. Pass it when creating the cart, in every cart URL, and in the checkout body. 3. Send it as an `x-session-id` header when reading orders or ticket PDFs. Lifetimes: the search result snapshot lives ~30 minutes; the cart lives ~45 minutes and refreshes on every read. A `409 "snapshot expired"` means search again; a `404` on the cart means recreate it and re-add items. ## Booking flow ### 1. Find ports GET /api/v1/catalog/ports?q=santorini&limit=20 GET /api/v1/catalog/ports?from=IDPAD&limit=50 (destinations reachable from an origin) Use the returned `code` values as `origin` / `destination` below. ### 2. Passenger types for the corridor GET /api/v1/catalog/passenger-age-schema?origin=IDPAD&destination=IDGAR&departureDate=2026-08-20 Use the returned passenger types and age bands; do not hard-code adult/child/infant thresholds. ### 3. Search live availability POST /api/v1/search { "origin": "IDPAD", "destination": "IDGAR", "departureDate": "2026-08-20", "currency": "EUR", "passengers": [ { "type": "adult", "count": 2 }, { "type": "child", "age": 7, "count": 1 } ], "vehicles": [] } Round trips: `POST /api/v1/search/round-trip` with an extra `returnDate`; the response has `outbound` and `inbound` sailing lists. From the response, keep the top-level `sessionId` and, for each chosen sailing: its `id` (the `sailingId`), its own `routeId` and `companyId`, and a selected `accommodations[].id`. ### 4. Create the cart and add each leg POST /api/v1/cart { "sessionId": "", "currency": "EUR" } POST /api/v1/cart/{sessionId}/items { "sourceSessionId": "", "groupId": "", "groupKind": "one-way", "direction": "outbound", "routeId": "", "companyId": "", "sailingId": "", "departureDate": "2026-08-20", "accommodationId": "", "fareTypeId": "", "extraIds": [], "passengers": [ { "age": 35 }, { "age": 32 }, { "age": 7 } ] } One request per leg; a round trip uses the same `groupId` with `"groupKind": "round-trip"` and directions `outbound` / `return`. The response contains the cart item `id` and `bookingRequirements` — the fields the operator requires per passenger (e.g. passport data). ### 5. Passenger details (per cart item) PUT /api/v1/cart/{sessionId}/items/{itemId}/passengers { "passengers": [ { "type": "adult", "title": "Mr", "firstName": "Ada", "lastName": "Example", "gender": "Female", "dateOfBirth": "1990-05-12", "countryCode": "NL", "passportNumber": "", "passportExpiry": "2030-05-12", "passportCountryCode": "NL" } ] } Only send optional fields when `bookingRequirements` requires them. Dates are `YYYY-MM-DD`. ### 6. Terms and payment methods GET /api/v1/checkout/terms?sessionId={sessionId} -> keep "snapshotHash" GET /api/v1/payment-methods?currency=EUR -> pick a method Present the terms to your user before checkout. If checkout later returns `409 terms_stale`, fetch a fresh snapshot and re-present it. ### 7. Checkout — then hand payment to your human POST /api/v1/checkout { "sessionId": "", "customerFirstName": "Ada", "customerLastName": "Example", "customerEmail": "ada@example.com", "customerPhone": "+31600000000", "paymentProvider": "", "paymentMethod": "", "currency": "EUR", "returnUrl": "https://viaferry.com/booking/confirmation", "termsAcceptance": { "snapshotHash": "", "checkboxLabel": "I agree to the booking terms" } } The `201` response contains `orderId`, `orderNumber`, and a hosted-payment `redirectUrl`. **Payment cannot be completed by API** — give `redirectUrl` to your user to open in a browser. Afterwards: POST /api/v1/checkout/complete { "orderId": "" } returns `409 payment_not_complete` until the payment settles — poll gently. Authoritative state (order status, payment status, per-trip fulfillment) is: GET /api/v1/orders/{orderId} (header: x-session-id: ) GET /api/v1/orders/{orderId}/ticket-pdf (header: x-session-id: ) Never re-send checkout after an ambiguous failure; read the order first and use `POST /api/v1/orders/{orderId}/payment-attempts` to retry payment on a pending order. ## Errors { "requestId": "...", "error": { "code": "bad_request", "message": "..." } } | Status | Meaning | |---|---| | 400 | Invalid body, missing field, or currency mismatch | | 401 | Missing `x-session-id` on a protected order/ticket read | | 404 | Resource missing, expired, or not owned by this session | | 409 | Terms stale, checkout in progress, or payment not complete | | 422 | Business rule violated (e.g. currency without an exchange rate) | | 5xx | Temporary failure — retry with backoff | ## Endpoint summary | Method | Path | Purpose | |---|---|---| | GET | /api/v1/currencies | Currencies and minor-unit exponents | | GET | /api/v1/nationalities | Nationality codes for passenger details | | GET | /api/v1/catalog/ports | Port/area search and reachability | | GET | /api/v1/catalog/resolve?origin=&destination= | Canonical route for a port pair | | GET | /api/v1/catalog/routes/{routeId}/search-form | Passenger/vehicle limits, currencies | | GET | /api/v1/catalog/passenger-age-schema | Passenger types and age bands | | GET | /api/v1/site/booking-window-calendar | Bookable-day calendar for a corridor | | POST | /api/v1/search | One-way live search | | POST | /api/v1/search/round-trip | Round-trip live search | | POST | /api/v1/cart | Create session cart | | GET | /api/v1/cart/{sessionId} | Read cart and totals | | POST | /api/v1/cart/{sessionId}/items | Add a sailing leg | | PUT | /api/v1/cart/{sessionId}/items/{id}/passengers | Save passenger details | | GET | /api/v1/transfers | Transfer offers for a sailing endpoint | | POST | /api/v1/cart/{sessionId}/transfers | Add a transfer | | PUT | /api/v1/cart/{sessionId}/currency | Convert cart currency | | GET | /api/v1/checkout/terms | Terms snapshot (+hash) for checkout | | GET | /api/v1/payment-methods | Payment methods for a currency | | POST | /api/v1/checkout | Create order + payment attempt | | POST | /api/v1/checkout/complete | Reconcile payment, finish fulfillment | | POST | /api/v1/orders/{id}/payment-attempts | Retry payment on a pending order | | GET | /api/v1/orders/{id} | Authoritative order state | | GET | /api/v1/orders/{id}/ticket-pdf | Ticket PDF | ## Content Human-readable pages are indexed at https://viaferry.com/sitemap.xml and summarized in https://viaferry.com/llms.txt.