Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@fleeta/sdk

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.

Install

npm install @fleeta/sdk

Quick start

const { 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.

Wi-Fi-only dashcams

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' });

Camera limit — over-limit dashcams are readable but locked

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 locked

SD-card recordings — play a clip

media.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 GET with 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 until expiresAt — 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' });

SD-card recall — busy retry and progress polling

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
  }
}

Low-level requests — the third argument is an envelope

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

Domains

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.

Pagination

  • Cursor resources (events · telemetry · geofence-alerts · admin · insights GET /v1/reports/driving): the response carries pagination.nextCursor / hasMore; the type is CursorPage<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) pass nextCursor back as after.
  • Offset resources (devices · sd-files · geofences · recall-jobs · webhooks · deliveries): the response carries pagination.page / totalPages. Use iterate() (webhooks: call list({ page, perPage })), or pass { page, perPage } yourself. The type is OffsetPage<T>.

TypeScript

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.

Conventions

  • 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 / to time of day is ignored); insights.drivingReports() takes YYYY-MM-DD or an RFC 3339 date-time with offset (reduced to the UTC calendar date), while drivingReport(psn) takes date only.
  • Errors: application/problem+jsonOpenApiError (status, code, detail, requestId, retryAfter). e.is(code) narrows e.body to 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 a TypeError (0.5.0+).

Versions

  • 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 (409 on geofences.update() without a shape) and query_timeout (504 on events.stats() over too wide a period) added to ProblemCode / CODES. Type comments: GPS export from / to are UTC calendar days, insights.drivingReports() accepts RFC 3339 date-times, drivingReport(psn) rejects from / to with 400. Camera-limit gate (2026-09-08): 403 camera_limit_exceeded (ProblemCameraLimitExceeded; CODES now 53 codes), entitlement on every dashcam row plus registeredAt / lastLoginAt / connectivity / cameraPosition on devices, a cameras block beside data on six operations (four lists and two aggregates), a typed entitlement filter; insights.drivingReports() / fleetSummary() count covered dashcams by default ({ entitlement: 'all' } for everything). RecordingType vocabulary change and the new required row fields are the only things that can break a build — see CHANGELOG.md.
  • 0.6.0webhooks.reactivate() / webhooks.rotateSecret(), typed Problem<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.0request() envelope validation (a flat payload is now a TypeError). Type declarations synchronized with the 56 spec schemas: getSettings declared, WebhookCreated.secret, DevicePreRegisterResult, GeofenceAlert, non-null GeoPoint, Usage (callHistory / statusBreakdown / volumeQuotas[]), real types for export jobs, event media, reboot and webhook test/deliveries, NoContent for 204 operations, 7 request types. New methodsdevices.groups(), admin.apiLogs() / iterateApiLogs(), geofences.iterateAlerts() (all 49 operations covered). DeviceCategory is 'cloud' | 'wifi' again and cloud-only features answer 422 cloud_only for a Wi-Fi-only PSN. BreakingRecallJobStatus 'cancelled''canceled'; the 429 retry is capped at 60 s and the monthly cap is thrown immediately. Full details in CHANGELOG.md.
  • 0.4.0 — 409 device_busy redesign + retryOnBusy.

Links

MIT © Pittasoft Co., Ltd.