Skip to content

Official TypeScript SDK

8 min read

Ferry ships an official TypeScript SDK for the partner API, so you do not have to hand-build an HTTP client. It lives in the open-source repository ferry-vn/ferry-sdk, together with the CLI command-line tool and an MCP server for AI agents.

One FerryClient exposes resource groups by business area: piers and routes, voyages, route prices, seat maps, bookings, orders, cancellation requests, customers, the organization, transactions, debts, vouchers, and disruptions. Its types are generated from the OpenAPI specification of the partner API (backend v3.2.16); the repository’s CI fails when the types drift from the specification, and a read-only contract suite runs nightly against UAT to catch backend changes.

The SDK does not replace the business rules in this documentation: the booking, ticketing, and money flows work exactly as described; the SDK only wraps them.

The SDK is on npm. It needs Node.js 20 or later; Bun, Deno, and Cloudflare Workers work too:

Terminal window
npm install @ferryvn/sdk

After that, import { FerryClient } from '@ferryvn/sdk' (or require) and a locations.find() call work immediately, even without an API key, because the pier catalogue is public data.

An organization admin creates the API key in the Dashboard; it is shown once, never expires, and carries organization-admin power: creating bookings, issuing tickets against your balance, changing settings. Keep it on your server. The SDK refuses to start in a browser when an apiKey is set, unless you pass dangerouslyAllowBrowser: true; do not enable that flag for client-side apps. Calls that need no key (piers, routes, operators, and bookings.getPublic) work in a browser. How to create a key: see Authentication with an API key.

EnvironmentAddress the SDK usesSelect with
Production (default)https://api.ferry.vnno configuration
UAThttps://api.ferryvn.netenvironment: 'uat' with a UAT key

To point at another address (for example a UAT base URL Ferry gave you), use the baseUrl option; the SDK appends /api/v1 itself.

The example below looks up piers, finds voyages and a customer, and creates a draft booking. No step charges money:

import { FerryClient } from '@ferryvn/sdk';
const ferry = new FerryClient({ apiKey: process.env.FERRY_API_KEY, environment: 'uat' });
const [from] = await ferry.locations.find('Rạch Giá');
const [to] = await ferry.locations.find('phu quoc');
const { data: voyages } = await ferry.voyages.search({ date: '2026-11-01', departureId: from.id, destinationId: to.id });
const [customer] = await ferry.customers.search('0901234567');
const { booking, draft } = await ferry.bookingDrafts.create({
customerId: customer.id,
ordererName: 'Nguyễn Văn A',
contactEmail: 'a@example.com',
depart: {
voyageId: voyages[0].voyage.id,
seats: ['A1'],
passengers: [{ name: 'Nguyen Van A', dateOfBirth: '1990-05-01', socialId: '079090000001', homeTown: 'Cần Thơ' }],
},
});
draft.estimatedTotalVnd; // tickets + harbor fees − vouchers

Building a booking by hand means filling 17 fields per ticket, taken from two different structures, where the ticket type must match the passenger’s age and the price row must match the seat type. The bookingDrafts group does that for you: it applies the backend’s rules locally and reports every problem before anything is sent.

  • prepare only validates and quotes; it creates nothing. create validates and then creates the booking.
  • You provide the voyage, seat names (or leave them out so the SDK picks adjacent seats in the cheapest passenger seat type with enough room), and the passengers.
  • The SDK picks the ticket type from age and nationality using each operator’s rules, matches prices, checks that seats are free and sellable, rejects duplicate ID or passport numbers, and applies the operator’s input-length rules for passenger details. Nationality defaults to Vietnam.
  • The quote is the ticket price including VAT plus the operator’s harbor fee, minus vouchers. On 2 real production bookings the server’s total matched the SDK’s quote to the đồng.
  • On a problem the SDK throws FerryBookingDraftError with an issues list. The isVoyageBookable helper tells which search results can be sold, because the API also returns disabled, sold-out, and departed voyages.

Creating a booking attaches the chosen seats and passengers to the booking and charges nothing; the booking starts as Requested and unpaid, with a payment deadline. The operator confirms the seats only when tickets are issued; see Bookings.

prices.forRoute returns the selling prices of a route per voyage, seat type, and ticket type, with the per-ticket harbor fee and the age rules. These are selling prices including VAT, after the operator’s promotions and your organization’s price additions, which is what the server charges; your own cost is never in the result (see Fares and multi-level markup).

Without live, prices come from synced voyage data and can lag the operator. Pass live: n (at most 10) to re-price the first n voyages from their seat maps; each one may query the operator.

Every list call returns { data, next_cursor }. Use iterate() to let the SDK follow the cursors; the SDK function collect(...) gathers the results of iterate() into an array (only for small scripts, because it uses memory). The page limit for partner lists is 50 (routes: 100); see also General conventions.

The SDK automatically retries only GET and HEAD calls, on network errors or on 408, 429, 502, 503, and 504, at most 2 times. Write calls (POST, PATCH, and so on) are never retried, because the Ferry API has no idempotency key and a resend can create a second order.

After a network error or timeout on a write call, look the booking up with bookings.list and the namePhoneEmail filter before sending again. Every call also accepts per-request options: signal, timeoutMs, maxRetries, headers, and requestId.

Everything the SDK throws extends FerryError:

Error classWhen
FerryApiErrorAny unsuccessful response; carries status, errorCode, errorName, the Vietnamese message userMessage, and requestId
FerryAuthError (401), FerryPermissionError (403)Invalid or revoked key; not allowed
FerryNotFoundError (404), FerryConflictError (409), FerryValidationError (422)Business errors by HTTP status
FerryRateLimitError (429)Rate limit reached; carries retryAfterMs
FerryServerError (5xx)Failure on the Ferry or operator side
FerryConnectionErrorNo response: timeout, network error, or you aborted with an AbortSignal
FerryConfigError, FerryBookingDraftErrorBad configuration (before anything is sent); invalid booking draft

Note on FerryPermissionError: Ferry also answers 403 for ids that do not exist or belong to another organization, so do not treat a 403 as a broken key. Business codes are available as constants, for example FerryErrorCode.OrganizationBalanceNotEnough (120120) and FerryErrorCode.UnavailableSeats (150201); errorCodeName(code) returns the name of a code. The full code table is in Errors and how to handle them.

Every request carries an x-request-id header; the backend logs it, so quote the value to Ferry support when you report an incident.

Ferry webhooks are unsigned and delivered once (2-second timeout, no retries); see Webhooks. The SDK has two helpers for safer receiving:

  • parseWebhook({ headers, body }) reads the payload and types it by the x-event-type header.
  • safeEqual(a, b) compares the secret token in your URL in constant time.

You still need an unguessable secret in the URL, a fast response, and reconciliation by polling, because a lost event is never resent.

Schedules run on Vietnam time. toYmd(date) formats a Date as the calendar day in Vietnam, so 00:30 on the 20th in Vietnam always gives 2026-10-20 whatever the server’s time zone; related helpers are todayInVietnam(), addDays(), and isValidYmd(). Money is integer VND: formatVnd(1250000) returns 1.250.000 ₫.

These three call groups have financial effects. The SDK never retries them.

ActionSDK callEffect
Create a bookingbookings.create, bookingDrafts.createAttaches seats and passengers to the booking, charges nothing; booking is Requested and unpaid with a payment deadline
Confirm ticket issuancebookings.confirmToIssueCharges the organization’s balance and credit, then orders from the operator; can take minutes; at most 5 attempts per booking per minute
Send a cancellation requestcancelTicketRequests.createHolds the cancellation fee immediately, marks the tickets cancelled at once, and starts the approval chain

bookings.confirmToIssue returns no data: read the outcome with bookings.get. With automated operators the tickets are issued during the call, and its minimum timeout is 10 minutes. A customer shortfall is booked as debt, so the booking usually ends as paid with debt; see Ticket issuance and Money.

If you need a route the SDK does not wrap yet, use ferry.request with the method, a path under /api/v1/…, query, and body; it shares the SDK’s authentication, retries, hooks, and errors.

MisconceptionRealityCorrect practice
Embed the API key in a web app for convenienceThe key carries organization-admin powerKeep the key on the server; call through your own backend
Resend a write call after a timeoutThe API has no idempotency key, a resend can create a second orderLook up bookings.list by name, phone, or email first
A 403 means the key is brokenFerry also answers 403 for ids that do not exist or belong to another organizationCheck the id and the organization before changing the key
UAT is as safe as a sandboxIssuing on UAT places real orders and spends real moneyOnly issue on voyages agreed with Ferry
prices.forRoute is always live pricingWithout live, prices come from synced data and can lagUse live when you need near-real prices, and re-check before checkout
Compute dates in the server’s time zoneSchedules run on Vietnam timeUse toYmd and todayInVietnam

Full descriptions of every call, option, and the 0.x migration guide are in the SDK package README on GitHub. The raw HTTP reference is on Scalar UI. Package bugs go to GitHub Issues; report security vulnerabilities privately as described in SECURITY.md in the repository, never in a public issue; questions about accounts, API keys, and bookings go to hi@ferry.vn.

What to look upWhere
SDK calls, options, and data typesThe SDK package README on GitHub
HTTP routes, parameters, and schemasScalar, groups Bookings, Voyages, Customers
Business error codesErrors and how to handle them
PageWhen to read
CLI ferryManual work or building bookings from a CSV file without writing code
MCP for AI agentsLet Claude search voyages, prices, and bookings with safety limits
WebhooksUnderstand why a webhook is only a signal
The booking flowUnderstand the booking life cycle the SDK wraps
General conventionsPagination, date and money formats