- Developers
- Official TypeScript SDK
Official TypeScript SDK
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.
What the SDK covers
Section titled “What the SDK covers”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.
Install
Section titled “Install”The SDK is on npm. It needs Node.js 20 or later; Bun, Deno, and Cloudflare Workers work too:
npm install @ferryvn/sdkAfter 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.
Before you start
Section titled “Before you start”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.
| Environment | Address the SDK uses | Select with |
|---|---|---|
| Production (default) | https://api.ferry.vn | no configuration |
| UAT | https://api.ferryvn.net | environment: '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.
Quick start
Section titled “Quick start”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 − vouchersBuild a pre-validated booking
Section titled “Build a pre-validated booking”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.
prepareonly validates and quotes; it creates nothing.createvalidates 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
FerryBookingDraftErrorwith anissueslist. TheisVoyageBookablehelper 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.
Route prices
Section titled “Route prices”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.
Pagination
Section titled “Pagination”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.
Retries and no idempotency
Section titled “Retries and no idempotency”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.
Errors
Section titled “Errors”Everything the SDK throws extends FerryError:
| Error class | When |
|---|---|
FerryApiError | Any 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 |
FerryConnectionError | No response: timeout, network error, or you aborted with an AbortSignal |
FerryConfigError, FerryBookingDraftError | Bad 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.
Webhooks
Section titled “Webhooks”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 thex-event-typeheader.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.
Dates and money
Section titled “Dates and money”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 ₫.
Money-moving calls
Section titled “Money-moving calls”These three call groups have financial effects. The SDK never retries them.
| Action | SDK call | Effect |
|---|---|---|
| Create a booking | bookings.create, bookingDrafts.create | Attaches seats and passengers to the booking, charges nothing; booking is Requested and unpaid with a payment deadline |
| Confirm ticket issuance | bookings.confirmToIssue | Charges 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 request | cancelTicketRequests.create | Holds 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.
Common mistakes
Section titled “Common mistakes”| Misconception | Reality | Correct practice |
|---|---|---|
| Embed the API key in a web app for convenience | The key carries organization-admin power | Keep the key on the server; call through your own backend |
| Resend a write call after a timeout | The API has no idempotency key, a resend can create a second order | Look up bookings.list by name, phone, or email first |
| A 403 means the key is broken | Ferry also answers 403 for ids that do not exist or belong to another organization | Check the id and the organization before changing the key |
| UAT is as safe as a sandbox | Issuing on UAT places real orders and spends real money | Only issue on voyages agreed with Ferry |
prices.forRoute is always live pricing | Without live, prices come from synced data and can lag | Use live when you need near-real prices, and re-check before checkout |
| Compute dates in the server’s time zone | Schedules run on Vietnam time | Use toYmd and todayInVietnam |
Where to look in Scalar
Section titled “Where to look in Scalar”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 up | Where |
|---|---|
| SDK calls, options, and data types | The SDK package README on GitHub |
| HTTP routes, parameters, and schemas | Scalar, groups Bookings, Voyages, Customers |
| Business error codes | Errors and how to handle them |
Read next
Section titled “Read next”| Page | When to read |
|---|---|
CLI ferry | Manual work or building bookings from a CSV file without writing code |
| MCP for AI agents | Let Claude search voyages, prices, and bookings with safety limits |
| Webhooks | Understand why a webhook is only a signal |
| The booking flow | Understand the booking life cycle the SDK wraps |
| General conventions | Pagination, date and money formats |