Official JavaScript/TypeScript client for the Fleeta Open API —
fleet dashcam data, GPS tracking, safety events, video and driving reports.
Zero dependencies (built on the fetch that ships with Node 18+), works in Node and in the browser.
npm install @fleeta/sdkconst { FleetaClient, OpenApiError } = require('@fleeta/sdk');
const fleeta = new FleetaClient({
apiKey: 'flt_live_xxxxxxxxxxxxxxxx', // issued in the Fleeta web viewer: Management › Open API
// baseUrl defaults to https://openapi.fleeta.io
});
// Dashcams that have not connected for 30 days (server-side filter)
const { data, pagination } = await fleeta.devices.list({ lastConnectedBefore: '30d', perPage: 50 });
// Create a GPS export job
const job = await fleeta.telemetry.createExportJob({
from: '2026-06-01T00:00:00Z', to: '2026-07-01T00:00:00Z', format: 'csv',
});
// Walk every safety event — the SDK follows the cursor for you
// One event is one row — ev.channels lists every angle it recorded (['front','rear'])
for await (const ev of fleeta.events.iterate({ from: '2026-06-01T00:00:00Z' })) {
console.log(ev.eventId, ev.type, ev.occurredAt, ev.channels);
}
// Thumbnails for a whole page in one request — no extra calls, no quota
const { data: page } = await fleeta.events.list({ limit: 20, include: 'thumbnail' });
page.forEach((ev) => console.log(ev.eventId, ev.thumbnailUrl));
// Errors are RFC 9457 problem+json, surfaced as OpenApiError
try {
await fleeta.events.video(someEvent.eventId, { channel: someEvent.channels[0] });
} catch (e) {
if (e instanceof OpenApiError) console.error(e.status, e.code, e.detail, e.requestId);
// 0.6.0+: narrow the body by code — every extension field is typed (mirrors the spec's Problem<Code> schemas)
if (e instanceof OpenApiError && e.is('cloud_only')) console.error('Wi-Fi-only dashcams:', e.body.psns);
}No dashcam yet? Use the public sandbox key flt_test_ent_0000000000000000001 — it reaches a demo
fleet with synthetic media, so every call in this README works before you own a device.
See Sandbox.
devices.list() returns every dashcam in the organization, cloud-connected and Wi-Fi-only alike
(category: 'cloud' | 'wifi'). A Wi-Fi-only dashcam is always offline, and cloud-only features
(location, trips, events, video, SD card, recall, settings, reboot, GPS export) answer
422 cloud_only for its PSN — do not retry. Pass { category: 'cloud' } to list only the dashcams
those features apply to.
const cloudOnly = await fleeta.devices.list({ category: 'cloud' });Seats go to the most recently registered cloud dashcams, up to the subscription's camera count. A
dashcam beyond it is entitlement: 'over_limit' (0.8.0+): it is still listed, and everything already
stored — location, trips, events, thumbnails, statistics, geofence alerts, GPS export — reads as
usual. Only video URLs, SD-card access, recall and remote commands answer 403 camera_limit_exceeded,
as does devices.create() when a batch would exceed the count (all-or-nothing). Nothing to reconfigure:
raise the camera count in the Fleeta web viewer (Account › Subscription) and the same key picks it up
within 5 minutes. Sandbox keys are never locked.
const { data: devices, cameras } = await fleeta.devices.list(); // cameras: { limit, registered, overLimit }
const locked = devices.filter((d) => d.entitlement === 'over_limit'); // or devices.list({ entitlement: 'over_limit' })
try {
await fleeta.events.video(ev.eventId);
} catch (e) {
if (e instanceof OpenApiError && e.is('camera_limit_exceeded')) {
console.error(`raise the camera count to ${e.body.requiredCameraLimit} (now ${e.body.cameraLimit})`); // no Retry-After — waiting does not help
}
}
// Driving reports and the fleet summary count covered dashcams by default (the web viewer's numbers);
// pass entitlement: 'all' to include over-limit dashcams. Device/event/location lists default to 'all'.
const everyone = await fleeta.insights.drivingReports({ entitlement: 'all' });
const { cameras: { overLimitPsns } } = await fleeta.admin.usage(); // exactly which dashcams are lockedmedia.sdFileVideo() returns a playable link for one recording straight from the dashcam's SD card
(0.7.0+). A recall job is created behind the scenes — url is that job's downloadUrl, and jobId
lets you poll progress or stop the upload. quality defaults to 'sub' (the low-resolution copy meant
for playback); pass 'main' for full resolution. Pass the main recording name exactly as listed by
media.sdFiles() — the sub-stream is selected with quality, never by appending S to the filename
(…S.mp4 answers 422 invalid_field).
A
GETwith side effects: every call asks the dashcam to upload over its own LTE connection and counts toward the recall allowance (also when the media server already holds a copy). Request the link once and reuse it untilexpiresAt— never on every render. If the same file (same quality) was requested within 48 h and the copy is still on the media server (about one day), no mobile data is spent.
const psn = '7XBPK0BE00000001';
const { data: files } = await fleeta.media.sdFiles(psn, { stream: 'main' });
const { data: clip } = await fleeta.media.sdFileVideo(psn, files[0].filename); // quality defaults to 'sub'
console.log(clip.url, clip.expiresAt); // hand url to <video src> or fetch it as returned
// { url, expiresAt, jobId, sizeBytes, receivedBytes, quality }
// expiresAt: null = deadline unknown (not unlimited) · receivedBytes: null = not probed (always null in the sandbox)
// Upload progress / cancel through the job behind the link
const { data: job } = await fleeta.media.recallJob(clip.jobId);
await fleeta.media.cancelRecallJob(clip.jobId);
// Full resolution instead
await fleeta.media.sdFileVideo(psn, files[0].filename, { quality: 'main' });media.createRecallJob() is the same flow as a job resource: use it when you want the job in hand
(download intent — quality defaults to 'main' here). The 201 already carries a downloadUrl
that plays or downloads immediately; the file streams while the dashcam is still uploading.
A dashcam handles one transfer at a time. While another transfer is running, the API answers
409 device_busy (with a Retry-After header) and creates no job. Pass retryOnBusy and the SDK
retries for you, honoring the header. (sdFileVideo() answers the same 409 — retry it yourself
after e.retryAfter seconds.)
// Success means status 'ready' with a downloadUrl right away (there is no queued state)
const { data: job } = await fleeta.media.createRecallJob(
{ psn: '7XBPK0BE00000001', filename: '20260727_131445_NF.mp4', quality: 'sub' },
{ retryOnBusy: true }, // default: retry at the Retry-After interval (5 s), up to 60 s in total
// { retryOnBusy: { timeoutMs: 120000 } } // adjust the total timeout
);
console.log(job.status, job.downloadUrl);
// Optional progress polling — receivedBytes is present only when the server could verify it
for (;;) {
const { data: j } = await fleeta.media.recallJob(job.jobId);
if (j.receivedBytes !== undefined && j.sizeBytes) {
console.log(`received ${Math.round((j.receivedBytes / j.sizeBytes) * 100)}%`);
if (j.receivedBytes >= j.sizeBytes) break;
}
await new Promise((r) => setTimeout(r, 3000));
}
// Handling it yourself, without retryOnBusy:
try {
await fleeta.media.createRecallJob({ psn, filename });
} catch (e) {
if (e instanceof OpenApiError && e.code === 'device_busy') {
// retry after e.retryAfter seconds; if it stays busy for minutes, fleeta.devices.reboot(psn) recovers the device
}
}Since 0.5.0 every one of the operations (52 as of 0.7.0) in the spec has a domain method. For an endpoint that
is newer than the SDK, call request(method, path, opts) directly. opts is an
envelope { query, body }; passing the payload flat fails immediately with a TypeError
(before 0.5.0 the body was silently dropped and the server answered 422).
await fleeta.request('GET', '/v1/groups'); // options may be omitted (= fleeta.devices.groups())
await fleeta.request('GET', '/v1/devices', { query: { perPage: 50 } }); // query string
await fleeta.request('POST', '/v1/media/recall-jobs', { body: { psn, filename } }); // request body
await fleeta.request('POST', '/v1/media/recall-jobs', { psn, filename }); // ✗ TypeError| Accessor | Resources |
|---|---|
fleeta.devices |
Devices: list / detail / update / pre-register / battery / firmware / settings (read) / reboot, groups (groups(), 0.5.0+) |
fleeta.media |
SD-card recordings: list / metadata / delete, playable link (sdFileVideo(), 0.7.0+); recall jobs: create / get / cancel |
fleeta.telemetry |
Latest locations (snapshot and feed), trips, GPS tracks, GPS export jobs |
fleeta.events |
Safety events: list / feed / detail / video and thumbnail URLs / statistics |
fleeta.geofences |
Geofence CRUD, alert history (alerts() · iterateAlerts(), 0.5.0+) |
fleeta.insights |
Fleet summary, driving reports |
fleeta.admin |
Audit logs, API call logs (apiLogs() · iterateApiLogs(), 0.5.0+), usage |
fleeta.webhooks |
Webhook subscriptions, test deliveries, delivery history; reactivate and secret rotation (0.6.0+) |
Every operation in the spec (openapi.yaml, 52 in total) has a method. The types are checked
against the spec field by field, so the SDK cannot drift from the API silently.
- Cursor resources (events · telemetry · geofence-alerts · admin · insights
GET /v1/reports/driving): the response carriespagination.nextCursor/hasMore; the type isCursorPage<T>. Auto-iteration helpers exist for events (iterate()), geofence-alerts (iterateAlerts()), audit-logs (iterateAuditLogs()) and api-logs (iterateApiLogs()). For telemetry (fleetLocations,trips,exportJobs, …) and insights (drivingReports) passnextCursorback asafter. - Offset resources (devices · sd-files · geofences · recall-jobs · webhooks · deliveries): the response
carries
pagination.page/totalPages. Useiterate()(webhooks: calllist({ page, perPage })), or pass{ page, perPage }yourself. The type isOffsetPage<T>.
index.d.ts is synchronized field by field with the 85 schemas of openapi.yaml (0.8.0). Response
envelopes are Single<T> (one item) and Page<T> (lists) — the six operations that carry dashcams (four lists
plus the fleetSummary() / events.stats() aggregates) add a cameras: CameraCoverage block beside data
(0.8.0+); the three 204 operations
(webhooks.delete, geofences.delete, telemetry.cancelExportJob) return NoContent ({ data: null }).
Every write method takes a typed request body (WebhookCreateRequest, GpsExportJobCreateRequest,
GeofenceCreateRequest, …).
import { FleetaClient, GeofenceAlert, WebhookCreated } from '@fleeta/sdk';
const { data: hook }: { data: WebhookCreated } = await fleeta.webhooks.create({ url, events: ['safety_event'] });
hook.secret; // visible only in the create response
for await (const a of fleeta.geofences.iterateAlerts({ type: 'speed' })) {
const alert: GeofenceAlert = a; // location is GeoPoint | null (no field-level nulls)
}Upgrading from 0.4.0: RecallJobStatus is now 'ready' | 'canceled' (US spelling),
devices.create() returns a DevicePreRegisterResult, and Usage.volumeQuotas is an array —
the full list is in CHANGELOG.md. 0.6.0 adds OpenApiError.is(code) / isKnown() /
CODES, the Problem<Code> types and webhooks.reactivate() / rotateSecret(); Webhook gains two
required-nullable fields (suspendedAt, previousSecretExpiresAt), so code that builds Webhook
literals by hand must add them — see CHANGELOG.md.
- Authentication:
Authorization: Bearer <apiKey>(attached by the SDK). - Time: every field is RFC 3339 UTC. Units: metric (km/h, m). GPS export ranges are UTC calendar days
(
from/totime of day is ignored);insights.drivingReports()takesYYYY-MM-DDor an RFC 3339 date-time with offset (reduced to the UTC calendar date), whiledrivingReport(psn)takesdateonly. - Errors:
application/problem+json→OpenApiError(status,code,detail,requestId,retryAfter).e.is(code)narrowse.bodyto that code's fields (ProblemCloudOnly.psns,ProblemQuotaExceeded.bucket, …; 0.6.0+). - 429: retried once after
Retry-After. A monthly-cap 429 (monthly_limit_exceeded) is thrown immediately. - 409
device_busy: a dashcam handles one transfer at a time. Thrown as-is by default —media.createRecallJob(body, { retryOnBusy: true })retries for you (see above). request()options: only{ query, body }— any other key is aTypeError(0.5.0+).
- 0.8.0 (unpublished; 0.7.0 shipped on npm 2026-09-02 with only the first item below) —
media.sdFileVideo(psn, filename, { quality? })+SdFileVideoUrl(GET /v1/devices/{psn}/sd-files/{filename}/video, all 52 operations covered); recall-job comments reframed as play-or-download (sizeBytes= size of the requested stream).geofence_unsupported_shape(409ongeofences.update()without ashape) andquery_timeout(504onevents.stats()over too wide a period) added toProblemCode/CODES. Type comments: GPS exportfrom/toare UTC calendar days,insights.drivingReports()accepts RFC 3339 date-times,drivingReport(psn)rejectsfrom/towith400. Camera-limit gate (2026-09-08):403 camera_limit_exceeded(ProblemCameraLimitExceeded;CODESnow 53 codes),entitlementon every dashcam row plusregisteredAt/lastLoginAt/connectivity/cameraPositionon devices, acamerasblock besidedataon six operations (four lists and two aggregates), a typedentitlementfilter;insights.drivingReports()/fleetSummary()count covered dashcams by default ({ entitlement: 'all' }for everything).RecordingTypevocabulary change and the new required row fields are the only things that can break a build — seeCHANGELOG.md. - 0.6.0 —
webhooks.reactivate()/webhooks.rotateSecret(), typedProblem<Code>errors (error.is(code)/isKnown()/CODES),WebhookDelivery.eventKey(all 51 operations covered). - 0.5.1 — English documentation, type comments and error messages. No API change.
- 0.5.0 —
request()envelope validation (a flat payload is now aTypeError). Type declarations synchronized with the 56 spec schemas:getSettingsdeclared,WebhookCreated.secret,DevicePreRegisterResult,GeofenceAlert, non-nullGeoPoint,Usage(callHistory/statusBreakdown/volumeQuotas[]), real types for export jobs, event media, reboot and webhook test/deliveries,NoContentfor 204 operations, 7 request types. New methods —devices.groups(),admin.apiLogs()/iterateApiLogs(),geofences.iterateAlerts()(all 49 operations covered).DeviceCategoryis'cloud' | 'wifi'again and cloud-only features answer422 cloud_onlyfor a Wi-Fi-only PSN. Breaking —RecallJobStatus'cancelled'→'canceled'; the 429 retry is capped at 60 s and the monthly cap is thrown immediately. Full details inCHANGELOG.md. - 0.4.0 — 409
device_busyredesign +retryOnBusy.
- Developer portal: https://developers.fleeta.io — Getting Started · API Reference · Plans & Limits
- AI assistants (MCP): https://developers.fleeta.io/docs/ai — connect ChatGPT, Claude or Cursor to your fleet with the same API key
- Source: https://github.com/fleeta-io/sdk-js
MIT © Pittasoft Co., Ltd.