The Datalastic Node.js SDK is a typed client for the Datalastic Maritime API. It covers real-time vessel tracking, port lookups, sea routing, maritime intelligence records, and async report jobs, with full TypeScript declarations and dual ESM/CommonJS output.
- Installation
- Authentication
- Quick Start
- Response Metadata
- Vessels
- Ports
- Sea Routes
- Intelligence Records
- Async Reports
- Error Handling
- Retries and Timeouts
- TypeScript
- Development
- License
npm install datalasticNode.js 18 or later is required.
Pass your API key to the constructor. The Datalastic Node.js SDK sends it as an x-api-key HTTP header on every request automatically.
import { Datalastic } from 'datalastic';
const client = new Datalastic(process.env.DATALASTIC_API_KEY!);CommonJS:
const { Datalastic } = require('datalastic');
const client = new Datalastic(process.env.DATALASTIC_API_KEY);Security: The API key is sent in the x-api-key HTTP header, not the URL, so it does not appear in request paths or server access logs. Keep it in an environment variable, do not commit it to version control, and avoid logging request headers in production.
A quick way to confirm your key is working:
const stat = await client.stat();
console.log(`Requests remaining: ${stat.requests_remaining}`);stat() returns { user_id, key_status, requests_made, requests_remaining }.
You can raise the default 30-second timeout if your environment needs more headroom:
const client = new Datalastic(process.env.DATALASTIC_API_KEY!, { timeout: 60_000 });Every constructor option is validated up front — see Retries and Timeouts.
The API wraps every response in a { data, meta } envelope. Methods return the data payload with the envelope's meta attached as a non-enumerable property, so results still behave like plain arrays and objects:
const ports = await client.ports.find({ name: 'Rotterdam' });
Array.isArray(ports); // true
ports.length; // 2
Object.keys(ports[0]); // no 'meta' key
JSON.stringify(ports); // serializes exactly as the API returned it
ports.meta; // credits, usage counters, pagination cursormeta is typed as ResponseMeta: success?, message?, next?, plus an index signature for the per-endpoint counters. It defaults to {} when the API omits it.
const stat = await client.stat();
console.log(stat.meta);The SDK never mutates a payload destructively and never crashes the caller to deliver metadata. In three cases the payload is returned exactly as received and meta is unavailable on it:
nullor a primitive payload — nothing can hold a property.- A frozen, sealed, or otherwise non-extensible payload.
vessels.inRadiuslikewise skips writing itsnextcursor onto such a payload rather than throwing. - A payload that already owns a
metafield. The payload's own value wins and is left untouched; the envelope'smetais not attached over it.
Pagination is unaffected in every one of these cases: vessels.find and vessels.inRadius take the cursor from the envelope returned by the request itself, not from the attached property, so page.next is still correct.
If the same object is handed back for a second response (for example a shared fixture in your own tests), the attached meta is refreshed to the newer envelope. The SDK tells its own attachment apart from an API field by its property descriptor: API fields parsed from JSON are always enumerable, the SDK's meta never is. One consequence of that rule: a non-enumerable, configurable meta property placed on the payload by something other than this SDK, an accessor in a hand-written test double for instance, matches the same signature and is replaced. Nothing JSON.parse produces can look like that.
If the API answers HTTP 200 with meta.success === false, the SDK raises an APIError carrying meta.message instead of returning a half-empty payload.
Returns a single Vessel with the latest AIS position: identity (uuid, name, mmsi, imo, eni, country_iso, type, type_specific), position (lat, lon), movement (speed in knots, course, heading, navigation_status), the reported destination, and the timestamp of the fix (last_position_epoch, last_position_UTC).
const vessel = await client.vessels.get({ mmsi: '477882000' });
console.log(`${vessel.name} is at ${vessel.lat}, ${vessel.lon}`);One of uuid, mmsi, or imo is required.
Returns a VesselPro, which includes everything vessels.get returns (identity, position, movement, destination, timestamps) plus a voyage layer:
current_draught— reported draught in metersdest_port/dest_port_unlocode— declared destination port name and UN/LOCODEdep_port/dep_port_unlocode— departure port name and UN/LOCODEatd_UTC/atd_epoch— actual time of departureeta_UTC/eta_epoch— estimated time of arrival
const pro = await client.vessels.pro({ imo: '9525338' });
console.log(pro.destination, pro.eta_UTC);Returns a VesselEstimated: all fields from vessels.pro plus estimated_position: { lat, lon }. When a vessel goes dark, this is the position dead-reckoned forward from its last known course and speed:
const est = await client.vessels.estimated({ uuid: 'e7a3c1f2-...' });
console.log(est.estimated_position.lat, est.estimated_position.lon);Returns { total, vessels }, where vessels is an array of Vessel records (same fields as vessels.get) and total is the count matched:
const bulk = await client.vessels.bulk({
mmsi: ['477882000', '538003876', '219023000'],
});
console.log(bulk.total, bulk.vessels);mmsi, imo, and uuid each accept a single string or an array.
Returns { point: { lat, lon, radius }, total, vessels }. Each entry in vessels is a standard Vessel with a distance field (nautical miles from center). Pass a center point, a radius in nautical miles, and an optional vessel type:
const nearby = await client.vessels.inRadius({
lat: 1.29,
lon: 103.85,
radius: 25,
type: 'Cargo',
});
console.log(nearby.total, nearby.vessels);lat and lon must always be provided together. You can also center on a port (port_uuid, port_unlocode) or a vessel identifier.
Returns a VesselHistory: the vessel's identity fields (uuid, name, mmsi, imo, eni, country_iso, type, type_specific) plus a positions array. Each position holds lat, lon, speed, course, heading, destination, and the timestamp (last_position_epoch, last_position_UTC):
const history = await client.vessels.history({ mmsi: '477882000', days: 7 });Returns a VesselInfo with the vessel's static record: identity and flag (uuid, name, name_ais, mmsi, imo, eni, country_iso, country_name, callsign, type, type_specific), capacity (gross_tonnage, deadweight, teu, liquid_gas), dimensions (length, breadth, draught_avg, draught_max), performance (speed_avg, speed_max), and registration (year_built, is_navaid, home_port). Capacity, draught, and speed averages may be null when unknown.
const info = await client.vessels.info({ imo: '9525338' });
console.log(info.gross_tonnage, info.length, info.year_built);Returns a VesselFindResult — { vessels, next } — where vessels is an array of VesselInfo records (the same static particulars as vessels.info: tonnage, deadweight, dimensions, year built, home port, call sign, etc.) and next is the pagination cursor, undefined on the last page:
const page = await client.vessels.find({
name: 'EVER',
vesselType: 'Cargo',
gross_tonnage_min: 50_000,
});
console.log(page.vessels.length, page.next);vesselType maps to the type query parameter internally. The field name avoids colliding with the JavaScript type keyword.
Feed next back in to walk every page:
const all: VesselInfo[] = [];
let cursor: string | undefined;
do {
const page = await client.vessels.find({ vesselType: 'Cargo', next: cursor });
all.push(...page.vessels);
cursor = page.next;
} while (cursor);vessels.inRadius paginates the same way — its result also carries a next cursor taken from the envelope meta:
const seen = [];
let cursor: string | undefined;
do {
const page = await client.vessels.inRadius({
lat: 1.29,
lon: 103.85,
radius: 25,
next: cursor,
});
seen.push(...page.vessels);
cursor = page.next;
} while (cursor);Both cursors come from meta.next, which is also readable directly as page.meta.next.
Returns an array of Port records. Each has uuid, port_name, country_iso, country_name, unlocode, port_type, coordinates (lat, lon), and regional grouping (area_lvl1, area_lvl2):
const ports = await client.ports.find({ name: 'Rotterdam' });Returns a PortDetail: all Port fields plus a terminals array. Each terminal carries terminal_code, terminal_name, company_name, coordinates (lat, lon), url, and address:
const port = await client.ports.get({ unlocode: 'NLRTM' });
console.log(port.terminals);Calculate a navigable sea route between two points. Returns a SeaRoute of { from, route, to }, where from and to are GeoJSON point features and route is the GeoJSON line of the path. The total distance in nautical miles is at route.properties.total_dist. Port UUIDs and UN/LOCODEs are also accepted as origin and destination:
// By coordinates
const route = await client.routes.calculate({
lat_from: 51.95,
lon_from: 4.14,
lat_to: 1.26,
lon_to: 103.83,
});
console.log(route.route.properties.total_dist); // nautical miles
// By UN/LOCODE
const route2 = await client.routes.calculate({
port_unlocode_from: 'NLRTM',
port_unlocode_to: 'SGSIN',
});
// By port UUID
const route3 = await client.routes.calculate({
port_uuid_from: 'uuid-of-origin',
port_uuid_to: 'uuid-of-destination',
});Both a departure and arrival point are required. The SDK throws before making a network call if either is missing.
All intel methods return typed arrays of records. At least one parameter is required per method.
Returns DryDockRecord[]. Each record has an id and covers survey and docking dates (special_survey_date, dry_dock_date), the IOPP certificate window (iopp_issue_date, iopp_exp_date), the responsible technical_manager, and top-level contact fields (country_code, website, email, phone, address, linkedin), keyed by imo and vessel_name with modified_at:
const schedule = await client.intel.dryDock({ imo: '9525338' });Returns CasualtyRecord[]. Each record covers a single incident: casualty_date, casualty_type, and free-text casualty_details, tied to imo and vessel_name with modified_at:
const casualties = await client.intel.casualties({ imo: '9525338', from: '2020-01-01' });Returns InspectionRecord[]. Each record covers one PSC inspection: inspection_date, inspection_authority, inspection_port, inspection_type, the detention value (a string code), deficiency counts and text (ship_deficiencies, deficiency_description), vessel classification (vessel_type_code, flag_code), the responsible technical_ism_manager, and top-level contact fields (country_code, website, email, phone, address, company_imo):
const inspections = await client.intel.inspections({ imo: '9525338' });Returns SPDRecord[]. Each record is a sale-and-purchase transaction: seller, buyer, price (sales_price_usd_mio (nullable), sales_price_usd/ldt), sales_report_date, sales_type, destination, vessel particulars (flag_name, vessel_type_code, built_year, dwt_design, gt, ldt), survey dates (dry_dock_date, special_survey_date), and a sales_note:
const deals = await client.intel.spd({ name: 'EVER ACE' });Returns OwnershipRecord[]. Each record maps the management chain: beneficial_owner (+ beneficial_owner_country), operator (+ operator_country), technical_manager (+ technical_manager_country, both string | null), commercial_manager (+ commercial_manager_country), plus flag_name, vessel_type_code, built_year, dwt_design, the nullable buyer, and class1_code:
const ownership = await client.intel.ownership({ beneficial_owner: 'Evergreen Marine' });Returns ClassSocietyRecord[]. Each record holds the class assignment (class1_code) alongside vessel classification (vessel_type_code, flag_name, built_year), survey dates (special_survey_date, dry_dock_date, both string | null), hull dimensions (loa, lbp, depth, beam_moduled, draft_design), tonnages (gt, nt, dwt_design), propulsion (engine_builder, engine_designer, propulsion_type_code), and ownership links (beneficial_owner / beneficial_owner_imo, technical_manager / technical_manager_imo):
const classSociety = await client.intel.classSociety({ imo: '9525338' });Returns EngineRecord[]. Each record describes the main engine: engine_designation, engine_builder, engine_designer, propulsion_type_code, and maximum continuous output (mco with mco_unit and mco_rpm), plus vessel_type_code, trading_category_code, built_year, and gt:
const engines = await client.intel.engine({ imo: '9525338' });
console.log(engines[0].engine_designation, engines[0].mco, engines[0].mco_unit);Returns CompanyRecord[]. Each record is a maritime company: short_name, long_name, company_type, company_imo, company_status, country_code, contact details (website, email, phone, address, linkedin), parent linkage (parent_company_imo, parent_company_name), and modified_at:
const companies = await client.intel.companies({ company_imo: '1234567' });Report jobs run asynchronously. Submit a job, then poll until status is 'complete'.
Returns a Report: report_id, report_type, status, created_at, optional updated_at and params, and a result_url that is populated once the job finishes. Use report_id to poll:
const job = await client.reports.submit('port_calls', { imo: '9525338' });
console.log(job.report_id);Returns the same Report shape, refreshed. Poll until status === 'complete', at which point result_url points to the generated output:
let report;
do {
report = await client.reports.get(job.report_id);
if (report.status !== 'complete') {
await new Promise(r => setTimeout(r, 5_000));
}
} while (report.status !== 'complete');
console.log(report.result_url);Returns an array of every Report on the account, each with the same shape (report_id, report_type, status, result_url?, created_at, ...):
const all = await client.reports.listAll();Every error thrown by the Datalastic Node.js SDK extends DatalasticError. You can catch the base class for a catch-all, or branch on subclasses for specific recovery logic.
import {
Datalastic,
DatalasticError,
AuthenticationError,
InsufficientCreditsError,
NotFoundError,
RateLimitError,
APIError,
} from 'datalastic';
const client = new Datalastic(process.env.DATALASTIC_API_KEY!);
try {
const vessel = await client.vessels.get({ mmsi: '477882000' });
console.log(vessel.name);
} catch (err) {
if (err instanceof AuthenticationError) {
console.error('Invalid or expired API key');
} else if (err instanceof InsufficientCreditsError) {
console.error('No requests remaining on this key');
} else if (err instanceof NotFoundError) {
console.error('Vessel not found');
} else if (err instanceof RateLimitError) {
const wait = err.retryAfter ?? 60;
console.error(`Rate limited. Retry after ${wait}s`);
} else if (err instanceof APIError) {
console.error('API or network error:', err.message);
} else {
throw err;
}
}| HTTP status | Error class |
|---|---|
| 401 / 403 | AuthenticationError |
| 402 | InsufficientCreditsError |
| 404 | NotFoundError |
| 429 | RateLimitError (.retryAfter?: number from the Retry-After header) |
| 400, 500, timeout, malformed body | APIError |
200 with meta.success === false |
APIError (.statusCode is the HTTP status, message is meta.message) |
Validation errors (missing required fields) and invalid constructor options throw DatalasticError before any network call is made.
Requests are retried automatically. The defaults are conservative: rate limits are retried on both verbs, transport failures on GET only.
| Option | Default | Meaning |
|---|---|---|
timeout |
30000 |
Per-attempt timeout in ms. Finite, > 0, <= 2147483647. |
maxRetries |
3 |
Retries after the first attempt. 0 disables retrying. Integer in [0, 100]. |
backoffMs |
500 |
Base for exponential backoff: retry n (0-based) waits backoffMs * 2 ** n. |
maxRetryDelayMs |
60000 |
Ceiling applied to every wait, including one derived from Retry-After. |
retryableStatuses |
[429] |
Statuses that trigger a retry. Each must be 408, 429, or in [500, 599]. |
sleep |
setTimeout |
Delay function between attempts. Exposed as a test hook. |
const client = new Datalastic(process.env.DATALASTIC_API_KEY!, {
maxRetries: 5,
backoffMs: 250,
maxRetryDelayMs: 10_000,
retryableStatuses: [429, 500, 502, 503, 504],
});
// Opt out entirely:
const noRetry = new Datalastic(process.env.DATALASTIC_API_KEY!, { maxRetries: 0 });- Network failures and timeouts: GET only. A POST that fails in transit may still have been processed, and
reports.submitcreates a job, so POST is never retried on a connection error or timeout. - Retryable statuses: both verbs. Opting 5xx in therefore applies to POST too, which can duplicate a submitted report if the server processed the request before the response failed. Opt in deliberately.
- Nothing else. 400, 401, 402, 404, and any 5xx you have not opted in throw immediately, without waiting.
- The set is used verbatim: passing
retryableStatuseswithout429means 429 is not retried.
For a retryable status the SDK honors the Retry-After response header in both of its forms — delta-seconds (Retry-After: 2) and HTTP-date (Retry-After: Wed, 21 Oct 2015 07:28:00 GMT). An absent or unparseable header falls back to exponential backoff. Every wait, from either source, is capped at maxRetryDelayMs, so a broken or hostile header cannot stall your process. RateLimitError.retryAfter reports the same value in whole seconds.
Bad options fail at construction with a DatalasticError naming the option, the value received, and the accepted range — no silent coercion. Rejected: a timeout that is zero, negative, NaN, Infinity, not a number, or above 2147483647; a maxRetries that is not an integer in [0, 100]; a negative or non-finite backoffMs; a maxRetryDelayMs that is not above zero; a retryableStatuses entry that is not an integer 408, 429, or 500-599 (401, 402, and 404 are rejected outright); and a sleep that is not a function.
The package exports typed interfaces for every resource. Import them alongside the client:
import type {
Vessel,
VesselPro,
VesselEstimated,
VesselInfo,
VesselHistory,
VesselBulkResult,
VesselInRadiusResult,
VesselFindResult,
Port,
PortDetail,
SeaRoute,
ApiStat,
Report,
DryDockRecord,
CasualtyRecord,
InspectionRecord,
SPDRecord,
OwnershipRecord,
ClassSocietyRecord,
EngineRecord,
CompanyRecord,
ResponseMeta,
WithMeta,
} from 'datalastic';Every method returns WithMeta<T> — its payload type plus the non-enumerable meta property described in Response Metadata. The runtime SDK version is exported as SDK_VERSION and is sent on every request as User-Agent: datalastic-node/<version>.
import { SDK_VERSION } from 'datalastic';The package ships .d.ts declaration files for both the ESM and CJS entry points, so TypeScript resolves types without any extra tsconfig configuration.
npm install
npm test # offline, fetch mocked
npm run build # ESM + CJS + type declarations into dist/On Windows, if npm test fails to launch the Jest shim, run it directly:
node --experimental-vm-modules node_modules/jest/bin/jest.jsMIT. See LICENSE.