// Fleeta Open API — type declarations // // Response types are copied from the objects the facade handlers actually return (the map* functions). // Source of truth = openapi.yaml `components.schemas` (2026-09-08, 85 schemas — 66 + 19 `Problem`) + operation-inline schemas. // The root `npm run sdk:types` (scripts/check-sdk-types.js) checks spec fields against this file for drift // and full coverage of the 52 operations; `npm run sdk:typecheck` compiles the index.test-d.ts fixtures. // A field typed `| null` means "unknown" and is never substituted with 0 or an empty string — // e.g. speedKmh 0 is a factual claim ("stationary"), so it cannot stand in for a missing value. // Responses may gain fields over time (additive-only), so do not treat any interface as a closed set. export interface ClientOptions { apiKey: string; /** Defaults to https://openapi.fleeta.io (production). Set explicitly for dev/sandbox. */ baseUrl?: string; fetch?: typeof fetch; timeoutMs?: number; } export interface CursorPagination { nextCursor?: string | null; hasMore?: boolean; } export interface OffsetPagination { page?: number; perPage?: number; total?: number; totalPages?: number; } export interface Page { data: T[]; pagination?: CursorPagination & OffsetPagination; } /** * Cursor pages (events · telemetry · admin · geofence-alerts · insights `GET /v1/reports/driving`): follow `pagination.nextCursor` / `hasMore`. * Auto-iteration helpers exist only for events (`iterate`), geofence-alerts (`iterateAlerts`), audit-logs (`iterateAuditLogs`) and api-logs (`iterateApiLogs`); * for telemetry and insights pass `nextCursor` back as `after` yourself. * Structurally identical to `Page` (not narrowed) — a marker alias so the pagination style is readable from the signature. */ export type CursorPage = Page; /** Offset pages (devices · sd-files · geofences · recall-jobs · webhooks · deliveries): `pagination.page` / `totalPages`. Same shape as `Page`. */ export type OffsetPage = Page; export interface Single { data: T; } /** 204 No Content — the SDK normalizes it to `{ data: null }` (deleteWebhook · deleteGeofence · cancelGpsExportJob). */ export interface NoContent { data: null; } /** Every `code` the API documents (errors guide table, 53). Additive-only — treat unknown values as a generic error. */ export type ProblemCode = | 'invalid_parameter' | 'invalid_body' | 'invalid_cursor' | 'unauthorized' | 'forbidden' | 'insufficient_scope' | 'group_not_allowed' | 'tier_invalid' | 'subscription_inactive' | 'quota_exceeded' | 'recall_limit_exceeded' | 'camera_limit_exceeded' | 'route_not_found' | 'group_not_found' | 'device_not_found' | 'event_not_found' | 'geofence_not_found' | 'webhook_not_found' | 'trip_not_found' | 'job_not_found' | 'audit_log_not_found' | 'video_not_available' | 'settings_not_found' | 'file_not_found' | 'not_found' | 'job_already_completed' | 'device_busy' | 'device_offline' | 'export_in_progress' | 'sd_card_absent' | 'concurrency_limit_exceeded' | 'invalid_field' | 'unknown_psn' | 'unsupported_device' | 'cloud_only' | 'no_scoped_devices' | 'monthly_limit_exceeded' | 'rate_limited' | 'internal_error' | 'configuration_error' | 'not_implemented' | 'write_delegation_not_configured' | 'export_not_configured' | 'device_settings_not_configured' | 'device_settings_read_not_configured' | 'device_command_not_configured' | 'media_recall_not_configured' | 'delegate_failed' | 'upstream_unavailable' | 'command_timeout' | 'query_timeout' | 'webhook_not_suspended' | 'geofence_unsupported_shape'; export type ApiScope = 'devices:read' | 'devices:write' | 'gps:read' | 'gps:export' | 'events:read' | 'video:read' | 'media:read' | 'media:write' | 'media:recall' | 'geofences:read' | 'geofences:write' | 'insights:read' | 'audit:read' | 'usage:read' | 'webhooks:manage'; /** RFC 9457 problem+json body. Every field is optional because a parse failure yields `{}`. */ export interface Problem { type?: string; title?: string; status?: number; code?: string; detail?: string; requestId?: string; } /* Per-code problem shapes — mirror openapi.yaml `Problem` (allOf Problem + extension fields). Only codes that carry extension fields have one. */ export interface ProblemInsufficientScope extends Problem { code: 'insufficient_scope'; requiredScope: ApiScope; } /** `bucket`/`limit` are absent on the API-edge usage-plan 429 variant; both are always present on the 403 from the quota store. */ export interface ProblemQuotaExceeded extends Problem { code: 'quota_exceeded'; bucket?: string; limit?: number; used?: number; } export interface ProblemMonthlyLimitExceeded extends Problem { code: 'monthly_limit_exceeded'; limit: number; used: number; resetsAt: string; } export interface ProblemSubscriptionInactive extends Problem { code: 'subscription_inactive'; reason: 'no_active_subscription' | 'plan_not_eligible'; } /** * 403 `camera_limit_exceeded` (0.7.0) — the dashcam is registered beyond the subscription's camera count * (`entitlement: 'over_limit'`), or `devices.create()` would exceed it. Everything already stored stays readable * (location, trips, events, thumbnails, statistics, geofences, GPS export); only video URLs, SD-card access, recall * and remote commands answer this code. Raise the camera count in the Fleeta web viewer (Account › Subscription) — * the same key picks it up within 5 minutes, no reissue. `psn` / `cameraPosition` come on single-device routes, * `psns` / `remainingSeats` on `POST /v1/devices` (all-or-nothing). No `Retry-After`; retrying is pointless. */ export interface ProblemCameraLimitExceeded extends Problem { code: 'camera_limit_exceeded'; /** The locked dashcam (single-device routes). */ psn?: string; /** The PSNs that could not be registered (`POST /v1/devices`). */ psns?: string[]; /** This dashcam's seat position, 1 = most recently registered (single-device routes). */ cameraPosition?: number; /** Camera count covered by the current subscription. */ cameraLimit: number; /** Cloud dashcams registered to the organization. */ registeredCameras: number; /** The camera count that would cover this dashcam (or this registration). */ requiredCameraLimit: number; /** Seats still free for registration (`POST /v1/devices` only). */ remainingSeats?: number; } export interface ProblemCloudOnly extends Problem { code: 'cloud_only'; psn?: string; psns?: string[]; category: 'wifi'; } export interface ProblemUnknownPsn extends Problem { code: 'unknown_psn'; psns: string[]; } export interface ProblemUnsupportedDevice extends Problem { code: 'unsupported_device'; psn?: string; model?: string; } export interface ProblemInvalidField extends Problem { code: 'invalid_field'; allowed?: string[]; maxRangeDays?: number; rangeDays?: number; min?: number; max?: number; } export interface ProblemInvalidParameter extends Problem { code: 'invalid_parameter'; allowed?: string[]; } export interface ProblemConcurrencyLimitExceeded extends Problem { code: 'concurrency_limit_exceeded'; limit: number; active: number; } export interface ProblemWebhookNotSuspended extends Problem { code: 'webhook_not_suspended'; subscriptionStatus: WebhookStatus; } export interface ProblemExportInProgress extends Problem { code: 'export_in_progress'; activeJobId?: string; activeStatus?: string; } export interface ProblemJobAlreadyCompleted extends Problem { code: 'job_already_completed'; jobId: string; } export interface ProblemDeviceBusy extends Problem { code: 'device_busy'; psn: string; } export interface ProblemCommandTimeout extends Problem { code: 'command_timeout'; psn: string; } export interface ProblemNotImplemented extends Problem { code: 'not_implemented'; op?: string; } export interface ProblemWriteDelegationNotConfigured extends Problem { code: 'write_delegation_not_configured'; op: 'register' | 'update' | 'vehicle'; } export interface ProblemDelegateFailed extends Problem { code: 'delegate_failed'; op?: string; } /** code → problem shape (only codes that carry extension fields). Checked against the spec by `npm run sdk:types`. */ export interface ProblemByCode { insufficient_scope: ProblemInsufficientScope; quota_exceeded: ProblemQuotaExceeded; monthly_limit_exceeded: ProblemMonthlyLimitExceeded; subscription_inactive: ProblemSubscriptionInactive; camera_limit_exceeded: ProblemCameraLimitExceeded; cloud_only: ProblemCloudOnly; unknown_psn: ProblemUnknownPsn; invalid_field: ProblemInvalidField; unsupported_device: ProblemUnsupportedDevice; invalid_parameter: ProblemInvalidParameter; concurrency_limit_exceeded: ProblemConcurrencyLimitExceeded; webhook_not_suspended: ProblemWebhookNotSuspended; export_in_progress: ProblemExportInProgress; job_already_completed: ProblemJobAlreadyCompleted; device_busy: ProblemDeviceBusy; command_timeout: ProblemCommandTimeout; not_implemented: ProblemNotImplemented; write_delegation_not_configured: ProblemWriteDelegationNotConfigured; delegate_failed: ProblemDelegateFailed; } /** The problem body for a given code — a `Problem` shape when one exists, otherwise the plain body. */ export type ProblemFor = C extends keyof ProblemByCode ? ProblemByCode[C] : Problem & { code?: C }; /** `OpenApiError` narrowed to one code — the result of `error.is(code)`. `body` has no index signature here, so unknown fields are compile errors. */ export interface OpenApiErrorOf extends OpenApiError { code: C; body: ProblemFor; } /** Discriminated union over every documented code — for `switch (e.code)` after `e.isKnown()`. */ export type KnownOpenApiError = { [C in ProblemCode]: OpenApiErrorOf }[ProblemCode]; export class OpenApiError extends Error { status: number; /** Documented codes autocomplete; any other string is still accepted (additive-only). */ code: ProblemCode | (string & {}); detail: string; requestId?: string; /** Retry-After response header (seconds). Present only when the header was sent, e.g. 429 or 409 `device_busy`. */ retryAfter?: number; /** The full RFC 9457 problem+json body (indexable — the server may add extension fields). Use `is(code)` for typed access. */ body: Problem & Record; /** Type guard: `if (e.is('cloud_only')) e.body.psns` — runtime `this.code === code` (0.6.0+). */ is(code: C): this is OpenApiErrorOf; /** True when `code` is one of `ProblemCode` — narrows to the `KnownOpenApiError` union (0.6.0+). */ isKnown(): this is KnownOpenApiError; /** Documented codes (same list as `ProblemCode`) — kept in sync with the spec and the errors guide by `npm run sdk:types`. */ static readonly CODES: ReadonlySet; } type Query = Record | undefined | null>; /** `Query` plus the typed `entitlement` filter (0.7.0) — the lists that accept `?entitlement=covered|over_limit|all`. */ type EntitlementQuery = Query & { entitlement?: EntitlementFilter }; /** * `GET /v1/safety-events/stats`. `tz` decides the zone the `byHour` buckets are counted in — an IANA zone * name such as `'Asia/Seoul'`; omit it for `UTC`, which is what earlier releases did. A UTC offset * (`'+09:00'`) is rejected with `400 invalid_parameter`: it cannot express daylight saving. Only `byHour` * is affected — `from`/`to` and every timestamp in the response stay RFC 3339 UTC. Echoed as `hourTz`. */ type SafetyEventStatsQuery = EntitlementQuery & { tz?: string }; /* ─────────────── Shared value types ─────────────── */ /** Dashcam status — the public form of the active/mode codes */ export type DeviceStatus = 'driving' | 'parked' | 'offline'; /** * Connectivity. 'cloud' = cloud-connected dashcam (all features) · 'wifi' = Wi-Fi-only dashcam (listed by default since 2026-08-31). * A wifi dashcam is always status 'offline', and cloud-only features (location, trips, events, video, SD card, recall, settings, reboot, GPS export) * answer `422 cloud_only` for its psn — use `devices.list({ category: 'cloud' })` to pick only the dashcams those features apply to. */ export type DeviceCategory = 'cloud' | 'wifi'; /** * How the dashcam reaches the network (0.7.0) — 'lte' for a built-in cellular module (LTE Cat.4 / Cat-M1), 'wifi' otherwise. * Separate from `DeviceCategory`, which says whether the dashcam talks to the cloud at all. */ export type DeviceConnectivity = 'lte' | 'wifi'; /** * Camera-seat entitlement (0.7.0). Seats go to the most recently registered cloud dashcams, up to the subscription's * camera count. 'covered' = within it. 'over_limit' = registered beyond it — still listed and readable (location, trips, * events, thumbnails, statistics, geofences, GPS export), but video URLs, SD-card access, recall and remote commands * answer `403 camera_limit_exceeded` (see `ProblemCameraLimitExceeded`). Wi-Fi-only dashcams hold no seat and are * always 'covered'. Sandbox keys are never locked. */ export type Entitlement = 'covered' | 'over_limit'; /** * `entitlement` query filter — 'covered' / 'over_limit' keep one class, 'all' both. The default is 'all' on device, event * and location lists and **'covered'** on `insights.fleetSummary()` / `insights.drivingReports()` (matches the Fleeta * web viewer's report numbers — pass 'all' to include over-limit dashcams). Any other value answers `400 invalid_parameter`. */ export type EntitlementFilter = Entitlement | 'all'; /** * Organization camera-seat summary, returned **beside `data`** (as `cameras`) by the lists that carry dashcams — * `devices.list()`, `telemetry.fleetLocations()` / `fleetLocationsFeed()`, `insights.fleetSummary()` / * `drivingReports()`, `events.stats()` and `admin.usage()` (`UsageCameras` there). Lets you see the seat position * without a failing call. */ export interface CameraCoverage { /** Camera count covered by the subscription. Null when it could not be resolved for this request — nothing is locked then. */ limit: number | null; /** Cloud dashcams registered to the organization. */ registered: number; /** Registered cloud dashcams beyond the limit (0 when `limit` is null). */ overLimit: number; } /** Video channel (camera direction) */ export type Channel = 'front' | 'rear' | 'interior' | 'option'; /** HTTP status class (shared by api-logs and usage.statusBreakdown) */ export type HttpStatusClass = '1xx' | '2xx' | '3xx' | '4xx' | '5xx'; /** * Public safety event types (canonical list: core/eventTypes.js). * All 17 recording types are emitted by GET /v1/events and accepted as `type` filters (since 2026-09-01 the type * is read from the recording's own event code). * `speed_limit` (2026-09-03) is the **server-judged road speed-limit violation** — Fleeta compares the vehicle's GPS * track with map speed-limit data; no clip is recorded (see `SafetyEvent.speedLimit`). It is distinct from * `overspeed`, the dashcam's own fixed-threshold speed alert that comes with a recording. Included by default. * `unknown` is **output-only** — an internal code not yet translated to a public type; as a filter value it is a 400. */ export type EventType = | 'driving_impact' | 'parking_impact' | 'overspeed' | 'harsh_braking' | 'harsh_acceleration' | 'sharp_turn' | 'drowsy' | 'distracted' | 'seatbelt' | 'undetected' | 'calling' | 'smoking' | 'geofence_enter' | 'geofence_exit' | 'geofence_pass' | 'geofence_speed' | 'manual' | 'speed_limit'; /** * @deprecated 2026-09-10 - no response in this API uses the `{lat, lng}` shape any more. * Its last users, `Trip.startLocation` / `endLocation`, were unified on `GeoPoint`. * Kept only so existing type references still resolve - new code should use `GeoPoint`. */ export interface LatLng { lat: number; lng: number; } /** * A single coordinate — both values are **always numbers**. When the position is unknown the **object itself is null**, * not its fields (`GeofenceShape.center` · `GeofenceAlert.location` · `SafetyEvent.location`). Location snapshots (`FleetLocation`) * and track points (`TrackPoint`) are separate interfaces with field-level nulls. */ export interface GeoPoint { latitude: number; longitude: number; } /* ─────────────── devices ─────────────── */ export interface DeviceSummary { psn: string; name: string; model: string | null; category: DeviceCategory; status: DeviceStatus; lastConnectedAt: string | null; /** Reverse-mapped from the organization's group roster. Null when the roster is unavailable or the device is in no group. */ groupId: string | null; /** Firmware version as the dashcam last reported it. */ fwVersion: string | null; /** * When the dashcam was registered to this organization (0.7.0). Camera seats are assigned in this order, newest first — * see `cameraPosition`. Null in the sandbox, whose demo fleet carries no registration date. */ registeredAt: string | null; /** * Last cloud sign-in of the dashcam (0.7.0). `lastConnectedAt` is the last activity and falls back to this value when no * activity has been recorded. Null when the dashcam has never signed in (always null in the sandbox). */ lastLoginAt: string | null; /** Network path — 'lte' or 'wifi' (0.7.0). Null when the record does not say (always null in the sandbox). */ connectivity: DeviceConnectivity | null; /** Camera-seat entitlement (0.7.0). 'over_limit' dashcams are readable but locked for video, SD card, recall and commands. */ entitlement: Entitlement; /** * 1-based position in the organization's registration order, newest first (0.7.0). Seats cover positions * 1..`cameras.limit`; a position beyond that is 'over_limit'. Null for a Wi-Fi-only dashcam that holds no seat. */ cameraPosition: number | null; /** * For an 'over_limit' dashcam, the camera count the subscription must be raised to for it to be covered (equals * `cameraPosition` — the same value a `403 camera_limit_exceeded` carries). Null when the dashcam is 'covered' (0.7.0). */ requiredCameraLimit: number | null; /** * Vehicle assigned to the dashcam, `{}` when none is assigned. **Now on list rows too (0.8.0)** — `devices.list()` * returns this for every row, so a fleet table no longer needs one `devices.get()` per dashcam. */ vehicle: Vehicle | Record; /** SIM of the dashcam, null when none. **Now on list rows too (0.8.0).** */ sim: Sim | null; /** Auxiliary battery pack. **Now on list rows too (0.8.0).** */ battery: Battery; } export interface Vehicle { vin: string | null; plate: string | null; maker: string | null; model: string | null; year: number | null; /** * Free-text label the web viewer groups vehicles by — its camera list filters on this (0.8.0). * Read-only: `devices.update()` does not accept it. */ tag: string | null; } /** sim_info has no carrier field, so carrier is not provided. */ export interface Sim { apn: string | null; iccid: string | null; } /** { connected: false } is the real state when no battery pack is registered (not a placeholder). */ export type Battery = | { connected: false } | { connected: true; level: number | null; temperature: number | null; updatedAt: string | null }; /** * Alias of `DeviceSummary`, kept so existing code keeps compiling. Since 0.8.0 the list and the detail view return * the same field set — `devices.list()` rows already carry `vehicle`, `sim` and `battery`, so there is nothing here * that `DeviceSummary` does not have. */ export interface DeviceDetail extends DeviceSummary {} /** PUT /v1/devices/{psn} body — all optional. Only the fields you send change. */ export interface DeviceUpdateRequest { name?: string; vehicle?: { vin?: string; plate?: string; maker?: string; model?: string; year?: number }; } export interface Firmware { current: string | null; latest: string | null; /** false whenever current or latest is unknown (avoids false positives) */ updateAvailable: boolean; } /** Device-native settings section names (Tab1–3 = device settings tabs, Wifi, Cloud). */ export type DeviceSettingsSectionName = 'Tab1' | 'Tab2' | 'Tab3' | 'Wifi' | 'Cloud'; /** * Device-native settings section (a cloudSettings item of the GET /v1/devices/{psn}/settings response — read-only). * The `section_info` field set differs by model and firmware; Wi-Fi passwords are masked as '***'. */ export interface DeviceSettingsSection { section_name: DeviceSettingsSectionName; section_info: Record; } /** GET /v1/devices/{psn}/settings response (read-only — the PUT was removed from the spec on 2026-08-07). */ export interface DeviceSettings { psn: string; settingVersion: string | null; firmwareVersion: string | null; firmwareLanguage: string | null; updatedAt: string | null; cloudSettings: DeviceSettingsSection[]; } /** POST /v1/devices body — PSNs to pre-register (optionally with SIM APN credentials). */ export interface DevicePreRegisterRequest { devices: Array<{ psn: string; sim?: { apn?: string; username?: string; password?: string } }>; } /** * Why a pre-registration row was rejected — `invalid_psn` (shorter than 10 characters), `duplicated` (already registered on * the platform), `unknown_psn` (not a dashcam in the product database), `blacklisted`, `backend_error` (rejected for another * reason), or `camera_limit_exceeded` (the subscription's camera count is full — normally caught up front as a `403` for the * whole batch; this per-row form appears only when the limit changed within the last few minutes). Four values added in 0.7.0. */ export type DevicePreRegisterFailureCode = 'invalid_psn' | 'duplicated' | 'unknown_psn' | 'blacklisted' | 'backend_error' | 'camera_limit_exceeded'; export interface DevicePreRegisterFailure { psn: string; code: DevicePreRegisterFailureCode; } /** * POST /v1/devices 201 — registered PSNs and failed PSNs (with reason codes), separately. Not a `DeviceDetail`. * The seat check is all-or-nothing and runs first: when the batch would exceed the camera count the call throws * `403 camera_limit_exceeded` (`ProblemCameraLimitExceeded.psns` / `remainingSeats`) and nothing is registered. */ export interface DevicePreRegisterResult { registered: string[]; failed: DevicePreRegisterFailure[]; } /** Device command (reboot) acceptance. `delegated` means the call was delegated to the DashCam lambda. */ export interface DeviceCommandResult { psn: string; command: string; status: 'accepted'; result?: Record | null; delegated?: boolean; } /** Organization group (GET /v1/groups — not paginated). */ export interface Group { groupId: string; name: string | null; deviceCount: number; } /* ─────────────── media ─────────────── */ /** * What triggered an SD-card recording, decoded from the filename's category letter * (canonical table: core/eventTypes.js — the same one `GET /v1/events` reads). * Every event value is spelled exactly as in {@link EventType}, so a clip and its event carry one * name; `normal` and `parking` — routine driving and parking footage — are the only recording-only * values, the two the `/v1/events` population excludes. `manual` is an {@link EventType} as well: * a recording the driver started deliberately is published as an event. * `unknown` is **output-only** (a category letter not in the table — see `SdFile.typeCode`); as a * filter value it answers 400. * * 2026-09-03: `automatic` and `timelapse` were **wrong labels** for harsh acceleration and sharp * turns and are gone; twelve categories that used to come back as `null` (harsh braking, overspeed, * the DMS and geofence types) are now named. * 2026-09-04: `event` and `impact` are gone too — the same clip is now `driving_impact` / * `parking_impact` here and in {@link EventType}. Filtering on a retired name answers 400 with the * accepted values in `allowed`, never an empty list. See the changelog. */ export type RecordingType = | 'normal' | 'parking' | 'manual' | 'driving_impact' | 'parking_impact' | 'harsh_braking' | 'harsh_acceleration' | 'sharp_turn' | 'overspeed' | 'drowsy' | 'distracted' | 'undetected' | 'calling' | 'smoking' | 'seatbelt' | 'geofence_enter' | 'geofence_exit' | 'geofence_pass' | 'geofence_speed' | 'unknown'; /** Camera the clip came from. `option` is the 7-BOX option camera; `unknown` is output-only. */ export type RecordingDirection = Channel | 'left' | 'right' | 'unknown'; export type RecordingStream = 'main' | 'sub'; export interface SdFile { filename: string; /** Device-local time parsed from the filename (no offset). Null when the filename is off-convention. */ recordedAtLocal: string | null; type: RecordingType | null; /** Raw category letter from the filename (e.g. `B` for harsh_braking) — diagnoses a `type` of `unknown`. */ typeCode: string | null; direction: RecordingDirection | null; /** Raw camera letter from the filename (e.g. `O` for the option camera). */ directionCode: string | null; stream: RecordingStream | null; mainSizeBytes: number | null; subSizeBytes: number | null; } /** One GPS sample embedded in the video (decoded NMEA RMC) */ export interface MediaGpsSample { recordedAt: string; offsetSeconds: number; latitude: number; longitude: number; speedKmh: number | null; heading: number | null; } /** One G-sensor sample (decoded binary frame) */ export interface MediaSensorSample { offsetSeconds: number; x: number; y: number; z: number; } /** Only the sections requested via include are present (default thumbnail+gps+sensor). A section that failed to decode is null. */ export interface SdFileMetadata { filename: string; recordedAtLocal: string | null; type: RecordingType | null; typeCode: string | null; direction: RecordingDirection | null; directionCode: string | null; stream: RecordingStream | null; thumbnailBase64?: string | null; /** The device's GPS recording setting. Null when the device did not report it (unknown) — distinct from false. */ gpsRecordingEnabled?: boolean | null; gps?: MediaGpsSample[] | null; sensor?: MediaSensorSample[] | null; } /** DELETE /v1/devices/{psn}/sd-files/{filename} 200 — permanent deletion confirmed. */ export interface SdFileDeleteResult { psn: string; filename: string; deleted: boolean; } /** * Recall job status — a job is created only when the transfer actually started (ready, downloadUrl plays or downloads immediately). * While the device is busy with another transfer the API answers 409 `device_busy` (with Retry-After) without creating a job, * so there are no queued/failed states. */ export type RecallJobStatus = 'ready' | 'canceled'; /** A recording recalled from the SD card (POST /v1/media/recall-jobs). The file streams while the dashcam is still uploading. */ export interface RecallJob { jobId: string; psn: string; filename: string; quality: RecordingStream; status: RecallJobStatus; /** Signed link that plays or downloads the recording — fetch it exactly as returned (it already carries `filesize`). */ downloadUrl: string | null; /** Size of the requested stream (main or sub) as reported by the device. Null when the device did not report it. */ sizeBytes: number | null; /** Read from the link's signature. Null = deadline unknown (not unlimited — the uploaded file is kept for about a day). */ expiresAt: string | null; /** * Bytes received so far (optional) — filled on GET /{jobId} when the server can HEAD-probe the media server. * Assumes the growing file's current size equals the received bytes (not yet verified on hardware). Omitted when the probe fails (not an error). */ receivedBytes?: number; createdAt: string; updatedAt: string; } /** POST /v1/media/recall-jobs body. quality defaults to 'main' (download intent) — use 'sub' to save data. */ export interface RecallJobCreateRequest { psn: string; filename: string; quality?: RecordingStream; } /** * GET /v1/devices/{psn}/sd-files/{filename}/video 200 (0.7.0) — a playable link for one SD-card recording plus the recall * job created to produce it. `url` is the same value as that job's `downloadUrl`. Key order: url, expiresAt, jobId, sizeBytes, * receivedBytes, quality. */ export interface SdFileVideoUrl { /** Signed media link — play or fetch it exactly as returned (already carries `filesize`). Streams while the upload is still running. */ url: string; /** Read from the link's signature. Null = deadline unknown (not unlimited — the uploaded file is kept for about a day). */ expiresAt: string | null; /** The recall job behind this link — `media.recallJob(jobId)` for progress, `media.cancelRecallJob(jobId)` to stop the upload. */ jobId: string; /** Size of the requested stream (main or sub) as reported by the device. Null when the device did not report it. */ sizeBytes: number | null; /** Best-effort upload progress (HEAD probe of the media server). Null when the probe failed; always null for a sandbox key. */ receivedBytes: number | null; /** Stream the link plays — echoes the request (default sub). */ quality: RecordingStream; } /* ─────────────── telemetry ─────────────── */ export interface FleetLocation { psn: string; name: string; latitude: number | null; longitude: number | null; /** Null when not reported — 0 is a real value ("stopped") and cannot stand in for missing. */ speedKmh: number | null; /** Degrees (0–359, 0 = north). Null when not reported — 0 means north, so it cannot stand in for missing. */ heading: number | null; status: DeviceStatus; at: string | null; /** Camera-seat entitlement of the dashcam (0.7.0). Over-limit dashcams keep reporting positions; only media and commands are locked. */ entitlement: Entitlement; } /** * Trip summary. distanceKm/maxSpeedKmh/avgSpeedKmh/start·endLocation are not on the drive document and are * accumulated from the gps_track_new track — trips with no track, or beyond the scan budget, keep them null. * avgSpeedKmh = distance ÷ elapsed time (stops included). */ export interface Trip { tripId: string; psn: string; startedAt: string | null; endedAt: string | null; distanceKm: number | null; durationMin: number | null; maxSpeedKmh: number | null; avgSpeedKmh: number | null; /** 2026-09-10 - unified on GeoPoint. These two fields were the last `{lat, lng}` holdouts. */ startLocation: GeoPoint | null; endLocation: GeoPoint | null; trackCount: number; } /** Track point. Safety event types are not on the track — query GET /v1/events separately. */ export interface TrackPoint { at: string | null; latitude: number | null; longitude: number | null; speedKmh: number | null; heading: number | null; } /** `points` is the result after thinning to maxPoints; `totalPoints` is the count before thinning. */ export interface TripTrack { tripId: string; psn: string; totalPoints: number; points: TrackPoint[]; } /** How the output is packaged. `perDate` appears only on jobs created from the web viewer and cannot be created via the API. */ export type ExportPackaging = 'single' | 'perDevice' | 'perDate'; export type GpsExportFormat = 'gpx' | 'csv' | 'kml'; export type GpsExportJobStatus = 'queued' | 'running' | 'completed' | 'canceled' | 'failed'; /** POST /v1/gps/export-jobs body. Omit psns for every device in the organization. */ export interface GpsExportJobCreateRequest { psns?: string[]; /** Range start (inclusive), ISO-8601 UTC. Only the UTC calendar date is used — the time of day is ignored and the * export covers whole UTC days. */ from: string; /** Range end, ISO-8601 UTC. Only the UTC calendar date is used. Must be later than `from` and at most 90 days after it * (UTC calendar days); otherwise the server answers `422 invalid_field` with `maxRangeDays`/`rangeDays` on `OpenApiError.body`. */ to: string; format: GpsExportFormat; packaging?: 'single' | 'perDevice'; } /** POST /v1/gps/export-jobs 201 — the summary right after creation (usually `queued`). Full fields via `exportJob()`. */ export interface GpsExportJobCreated { jobId: string; status: GpsExportJobStatus; estimatedPoints: number; createdAt: string; } /** * GPS Export job. The `downloadUrl` fields are present **only when completed and the output exists** (presigned, 1 hour). * `estimatedPoints` is an estimate (devices × days) while queued and becomes the exact count once the worker has counted. * All timestamps are RFC 3339 UTC; the timestamps inside the exported files are UTC as well. */ export interface GpsExportJob { jobId: string; psns: string[]; /** Start of the exported period (UTC). API-created jobs cover whole UTC calendar days, so this is midnight UTC of the * first exported day; jobs created in the web console follow the console's own calendar day. */ from: string | null; /** End of the exported period (UTC, exclusive) — midnight after the last exported day, on the same calendar basis as `from`. */ to: string | null; format: GpsExportFormat | null; packaging: ExportPackaging | null; estimatedPoints: number; status: GpsExportJobStatus | null; /** 0–100. Live progress by chunks/points. */ progress: number; createdAt: string | null; completedAt: string | null; expiresAt: string | null; canceledAt?: string; downloadUrl?: string; downloadFileName?: string; /** downloadUrl validity (seconds). */ downloadUrlExpiresIn?: number; } /* ─────────────── events ─────────────── */ export interface SafetyEvent { /** * Opaque id of the **event**, not of one of its files — the front clip, the rear clip and their * thumbnails all share it. Stable for the life of the event: uploading another channel later does * not change it. Different namespace from the `eventId` inside a webhook payload. */ eventId: string; psn: string; /** * Camera-seat entitlement of the dashcam that produced the event (0.7.0). Events of an 'over_limit' dashcam are listed * like any other; `events.video()` for them answers `403 camera_limit_exceeded` (`events.thumbnail()` still works). */ entitlement: Entitlement; /** 'unknown' for an internal code not yet translated to a public type. Null when there is no code at all. */ type: EventType | 'unknown' | null; /** Recordings: upload time of the event's first file — can trail the incident by seconds. `speed_limit`: when the vehicle * first exceeded the limit. Also the list sort key. */ occurredAt: string | null; /** Null for recordings — bv_mp4_record_info carries no coordinates. Set for `speed_limit` events (position at peak speed). */ location: GeoPoint | null; /** Null for recordings, for the same reason as above. Set for `speed_limit` events (peak GPS speed, km/h). */ speedKmh: number | null; /** Union of the event's playable channels — a front+rear event returns both on one row. Always empty for `speed_limit`. */ channels: Channel[]; /** Always equal to `channels.length > 0`. */ hasVideo: boolean; /** Only present when the request asked for `include=thumbnail`. Presigned URL for `channels[0]`, valid 5 minutes. */ thumbnailUrl?: string | null; /** Road speed-limit violation details — set only when `type` is `'speed_limit'`, null for every other type. */ speedLimit: SpeedLimitDetail | null; } /** Details of a server-judged road speed-limit violation (`SafetyEvent.speedLimit`, 2026-09-03). */ export interface SpeedLimitDetail { /** Posted speed limit that applied on the last road segment of the violation (km/h); null when it could not be determined. */ limitKmh: number | null; /** Peak excess over the limit (km/h). */ overKmh: number | null; /** How long the vehicle stayed above the limit (seconds). */ durationSec: number | null; /** Road name on the last segment of the violation, when the map data has one. */ roadName: string | null; /** Country code of the road, as recorded by the speed-limit data source. */ country: string | null; /** When the vehicle dropped back under the limit (RFC3339 UTC). */ endedAt: string | null; /** When the peak speed was reached (RFC3339 UTC) — `SafetyEvent.location` is the position at this moment. */ peakAt: string | null; /** True only when a school-zone limit was actually in force at the time (its time/calendar conditions were evaluated). */ schoolZone: boolean; } export interface EventTypeCount { type: EventType | 'unknown'; count: number; } export interface SafetyEventStats { kpi: { totalEvents: number; activeDevices: number; /** Currently always null — the source collection has no distance/speed aggregates. */ totalDistanceKm: number | null; avgSpeedKmh: number | null; }; byType: EventTypeCount[]; /** Fixed 24 slots of hour-of-day (0–23), counted in `hourTz`. `hour` is a number (the insights eventTrend uses ISO string labels — they differ). */ byHour: Array<{ hour: number; count: number }>; /** Time zone the `byHour` buckets were counted in — `'UTC'` unless the request passed `tz` (added 2026-09-20). */ hourTz: string; /** Over-limit dashcams are counted like any other (the figures match the web viewer's dashboard); `entitlement` marks each row (0.7.0). */ topDevices: Array<{ psn: string; name: string; count: number; entitlement: Entitlement }>; /** Currently always an empty array — the events collection has no coordinates. */ hotspots: never[]; } /** Thumbnail presigned URL (GET /v1/events/{eventId}/thumbnail). 403 after `expiresAt`. */ export interface EventMediaUrl { url: string; expiresAt: string; } /** Video presigned URL — the requested channel (default front) is echoed back as `channel`. */ export interface EventVideoUrl extends EventMediaUrl { channel: Channel; } /* ─────────────── geofences ─────────────── */ export type GeofenceShapeType = 'circle' | 'polygon' | 'rectangle' | 'polyline'; /** The shape keys depend on the type (circle = center + radiusM, otherwise coordinates or polygons). */ export interface GeofenceShape { type: GeofenceShapeType | null; center?: GeoPoint | null; radiusM?: number | null; coordinates?: GeoPoint[]; polygons?: GeoPoint[][]; } /** Recording (SD) and live-upload flags for one event kind. */ export interface GeofenceRecordingFlags { sdCard: boolean; liveUpload: boolean; } /** * Per-event recording/upload options. A polyline shape carries only `pass`; every other shape only * `enter` / `exit`. `speedKmh` comes back when the stored zone carries a speed threshold — zones * written through this API or the web viewer since 2026-08-31 do, older records and sandbox zones do not. */ export interface GeofenceRecordingEvents { enter?: GeofenceRecordingFlags; exit?: GeofenceRecordingFlags; pass?: GeofenceRecordingFlags; /** Speed threshold in km/h that arms the `speed` alert. Absent when the zone stores none. */ speedKmh?: number; } /** * Map colour and fill opacity stored with the geofence — the same values the web viewer and the mobile * apps draw it with. Both are `null` when the stored record carries none (the viewer then picks a palette * colour by list position, which the API cannot predict). */ export interface GeofenceStyle { color: string | null; opacity: number | null; } export interface Geofence { geofenceId: string | null; name: string | null; shape: GeofenceShape; style: GeofenceStyle; /** PSNs of the dashcams this geofence applies to. */ devices: string[]; recordingEvents: GeofenceRecordingEvents; createdAt: string | null; updatedAt: string | null; } /** * POST /v1/geofences body. The input shape differs from the output shape — * circle uses `shape.circle{ center{lat,lng}, radiusM }`, everything else `shape.geometry` (GeoJSON, [lng,lat] order). */ export interface GeofenceWriteShape { type: GeofenceShapeType; /** Write form — polygon/rectangle/polyline as GeoJSON geometry. Points may be `[lng, lat]` pairs or `{latitude, longitude}` objects; closing the ring is optional. Ignored for `circle`. */ geometry?: Record; /** Write form — circle centre + radius. Ignored for the other types. */ circle?: { center: { lat: number; lng: number }; radiusM: number }; /** Read form (0.7.0) — the outline/path exactly as `GET` returns it; used when `geometry` is absent. Closing point optional. */ coordinates?: GeoPoint[]; /** Read form (0.7.0) — several outlines of a multi-polygon, as `GET` returns them; used when `geometry` is absent. */ polygons?: GeoPoint[][]; /** Read form (0.7.0) — circle centre as `GET` returns it; used with `radiusM` when `circle` is absent. */ center?: GeoPoint; /** Read form (0.7.0) — circle radius in metres, paired with `center`. */ radiusM?: number; } /** * Map colour (`#RRGGBB`) and fill opacity (0–1) to store. Each property is applied on its own: on create * an omitted one takes the web viewer default (`#6B2FD9` / `0.24`), on update it keeps the stored value. */ export interface GeofenceStyleInput { color?: string; opacity?: number; } /** Recording options to store. `pass` is accepted only for a polyline. */ export interface GeofenceRecordingEventsInput { enter?: { sdCard?: boolean; liveUpload?: boolean }; exit?: { sdCard?: boolean; liveUpload?: boolean }; /** Polyline only. */ pass?: { sdCard?: boolean; liveUpload?: boolean }; /** Speed threshold (km/h, > 0). */ speedKmh?: number; } export interface GeofenceCreateRequest { name: string; shape: GeofenceWriteShape; style?: GeofenceStyleInput; devices?: string[]; recordingEvents?: GeofenceRecordingEventsInput; } /** PUT /v1/geofences/{id} body — a subset of the create body. */ export type GeofenceUpdateRequest = Partial; /** Short form because the endpoint path already says geofence (= geofence_enter/exit/pass/speed). */ export type GeofenceAlertType = 'enter' | 'exit' | 'pass' | 'speed'; /** One geofence alert (GET /v1/geofence-alerts — cursor). `speedKmh` only on speed alerts. */ export interface GeofenceAlert { alertId: string; geofenceId: string | null; geofenceName: string | null; psn: string | null; /** Camera-seat entitlement of the dashcam that raised the alert (0.7.0). Alert history is never locked. */ entitlement: Entitlement; type: GeofenceAlertType; occurredAt: string | null; location: GeoPoint | null; speedKmh?: number; } /* ─────────────── insights ─────────────── */ /** * Driving style — same definition as the in-house report screen (60 events per hour = 100%). * `safePct` = 100 − inattentivePct − recklessPct; the components are not capped, so it **can be negative** (not clamped). * All three are 0 when the period has under 5 m of driving or zero driving time (missing data is not disguised as a perfect score). */ export interface DrivingStyle { safePct: number; inattentivePct: number; recklessPct: number; } export interface EventBreakdown { total: number; byType: EventTypeCount[]; } export interface DrivingReport { psn: string; name: string; /** * Camera-seat entitlement (0.7.0). `drivingReports()` lists **'covered' dashcams by default** (the web viewer's report * numbers) — pass `entitlement: 'all'` to include 'over_limit' rows; `drivingReport(psn)` always answers for the * requested dashcam (200, `entitlement: 'over_limit'` when it is beyond the limit). */ entitlement: Entitlement; distanceKm: number; drivingMin: number; parkedMin: number; events: EventBreakdown; drivingStyle: DrivingStyle; maxSpeedKmh: number; avgSpeedKmh: number; } /** Bucket keyed by UTC hour label. Buckets are rounded individually, so their sum may differ from the daily total in the decimals. */ export interface HourlyBucket { hour: string; distanceKm: number; drivingMin: number; events: number; } export interface DrivingReportDetail extends DrivingReport { date: string; trips: number; /** Stops counted / minutes stopped by the dashcam while in driving mode — same figures as the web viewer's Idle stops / Idle time */ idling: { count: number; min: number }; /** Always 24 buckets (0 for idle hours). */ hourly: HourlyBucket[]; /** Driving days of the month (YYYY-MM-DD), ascending. */ calendar: string[]; } /** * `fleetSummary()` counts **'covered' dashcams by default** (0.7.0 — the web viewer's numbers); pass `entitlement: 'all'` * to include over-limit dashcams. Device rows carry `entitlement`; the envelope's `cameras` block gives the seat position. */ export interface FleetSummary { fleet: { total: number; online: number; driving: number; parked: number; offline: number }; disconnectedDevices: Array<{ psn: string; name: string; entitlement: Entitlement; lastConnectedAt: string | null }>; drivingStats: { distanceKm: number; durationMin: number; avgSpeedKmh: number; maxSpeedKmh: number; trips: number; }; /** Always 24 buckets with UTC hour labels; the current hour is last. */ eventTrend: Array<{ hour: string; count: number }>; topDevicesByEvents: Array<{ psn: string; name: string; entitlement: Entitlement; count: number }>; } /* ─────────────── admin ─────────────── */ export type AuditLogCategory = 'auth' | 'device' | 'geofence' | 'video' | 'export'; export type AuditLogStatus = 'success' | 'fail' | 'request' | 'accept' | 'delete' | 'start' | 'enable' | 'disable'; /** Audit log target — the populated keys depend on eventType and others may appear (additionalProperties). */ export interface AuditLogTarget { psn?: string; type?: string; email?: string; role?: string; filename?: string; geofenceId?: string; name?: string; [key: string]: any; } export interface AuditLogSummary { id: string; at: string | null; /** Currently always null — the source has no actor type (reserved). */ actorType: string | null; actorId: string | null; category: AuditLogCategory | null; eventType: string | null; status: AuditLogStatus | null; /** The ACL mapping always builds an object, but the source may be empty, so null is allowed conservatively. */ target: AuditLogTarget | null; ip: string | null; } /** * before/after are recorded only for some eventTypes (settings changes, firmware, geofences, groups, notifications, permissions). * For the rest the source itself is absent, so they are **null** (= not recorded) rather than an empty object (= no change). */ export interface AuditLogDetail extends AuditLogSummary { userAgent: string | null; before: Record | null; after: Record | null; } export type ApiLogMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; /** One API call log entry (GET /v1/api-logs — cursor). Null when the log source did not populate a field. */ export interface ApiLog { id: string | null; at: string | null; method: ApiLogMethod | null; route: string | null; status: number | null; statusClass: HttpStatusClass | null; errorCode: string | null; latencyMs: number | null; keyId: string | null; ip: string | null; userAgent: string | null; } export type UsageTier = 'free' | 'starter' | 'standard' | 'pro' | 'enterprise'; export interface UsageApiCalls { used: number; limit: number; /** Human-readable description of the limit formula (kept in step with the plan definitions) */ limitBasis: string; /** Whether this is the self-metered real counter — not an estimate */ measured: boolean; source: 'self'; } export interface UsageRate { limitRps: number; burst: number; } /** `transfer_bytes` — video delivered to the account this month (event video URLs at the clip size, SD-card recalls at the * recording size, both metered when the link is issued), in bytes. `export_job` — GPS export jobs created. `export_concurrent` * — exports allowed to run at once. `video_url` (a count of issued URLs) was replaced by `transfer_bytes` on 2026-09-19. */ export type UsageVolumeQuotaBucket = 'transfer_bytes' | 'export_job' | 'export_concurrent'; /** Unit of `limit` and `used` — `bytes` for `transfer_bytes`, `count` for the others. */ export type UsageVolumeQuotaUnit = 'bytes' | 'count'; /** One volume quota — `period` is the month the counter covers (yyyy-mm, UTC). The real response is an **array** (not a Record). */ export interface UsageVolumeQuota { bucket: UsageVolumeQuotaBucket; limit: number; used: number; period: string; unit: UsageVolumeQuotaUnit; } export interface UsageStatusClassCount { statusClass: HttpStatusClass; count: number; } /** ratio is null when total is 0. */ export interface UsageStatusClassShare extends UsageStatusClassCount { ratio: number | null; } export interface UsageCallHistoryDay { date: string; calls: number; avgLatencyMs: number | null; statusClasses: UsageStatusClassCount[]; } /** Rollup of the last 30 UTC days. `days` lists only days that had calls (empty days are not zero-filled). */ export interface UsageCallHistory { windowDays: number; from: string; to: string; aggregatedDays: number; avgLatencyMs: number | null; days: UsageCallHistoryDay[]; } export interface UsageStatusBreakdown { windowDays: number; aggregatedDays: number; total: number; classes: UsageStatusClassShare[]; } export interface Usage { period: string; tier: UsageTier; cameraLimit: number; apiCalls: UsageApiCalls; rate: UsageRate; volumeQuotas: UsageVolumeQuota[]; /** Null when there is no rollup source (sandbox, or logs not recorded). */ callHistory: UsageCallHistory | null; statusBreakdown: UsageStatusBreakdown | null; } /** * The `cameras` block beside `data` on `admin.usage()` (0.7.0) — `CameraCoverage` plus the serial numbers of the dashcams * currently beyond the camera count, so an integration can tell exactly which devices answer `403 camera_limit_exceeded`. */ export interface UsageCameras extends CameraCoverage { /** PSNs beyond the limit, in camera-position order (position `limit + 1` onward). Empty when nothing is over the limit or `limit` is null. */ overLimitPsns: string[]; } /* ─────────────── webhooks ─────────────── */ export type WebhookEventType = | 'safety_event' | 'geofence_enter' | 'geofence_exit' | 'device_disconnected' | 'export_completed'; /** The server sets `suspended` after consecutive delivery failures accumulate. */ export type WebhookStatus = 'active' | 'suspended'; export interface Webhook { webhookId: string; url: string; events: WebhookEventType[]; status: WebhookStatus; createdAt: string; /** When the subscription was suspended automatically; null while active (0.6.0). */ suspendedAt: string | null; /** Until when the previous secret still co-signs after `rotateSecret()`; null outside a grace window (0.6.0). */ previousSecretExpiresAt: string | null; } /** POST /v1/webhooks 201 — the signing `secret` (64 hex) is returned **only in this response**. It is not readable via GET afterwards. */ export interface WebhookCreated extends Webhook { secret: string; } export interface WebhookCreateRequest { url: string; events: WebhookEventType[]; } /** POST /v1/webhooks/{id}/rotate-secret body (optional) — `graceSeconds` 0..86400, default 86400 (24 h). */ export interface WebhookRotateSecretRequest { graceSeconds?: number; } /** POST /v1/webhooks/{id}/rotate-secret 200 — the **new** `secret`, returned only here. `previousSecretExpiresAt` = end of the co-signing window. */ export interface WebhookSecretRotated extends Webhook { secret: string; } /** * POST /v1/webhooks/{id}/test — result of the test delivery. Only one of `statusCode` / `error` is present. * A test delivery is a diagnostic: it is not written to the delivery history and never counts towards * automatic suspension. */ export interface WebhookTestResult { delivered: boolean; statusCode?: number; error?: string; /** * The `X-Fleeta-Signature` **value** sent with this test — `v1=`, or `v1=,v1=` during a * secret-rotation grace window. Not the header name: compare it with the value your endpoint computed * to confirm your verification code agrees with ours. */ signatureHeader: string; /** * Present and `true` only for sandbox keys (`flt_test_…`). The sandbox performs no outbound requests, so * `delivered` is `false` and `error` says nothing was sent. The signature above is still computed for real — * check your HMAC against it, then re-test with a live key to exercise your receiver (0.8.0). */ sandbox?: boolean; } /** One delivery record (last 30 days, offset page). */ export interface WebhookDelivery { deliveryId: string; /** Stable across queue redeliveries (= `X-Fleeta-Event-Key`); null on records older than the field (0.6.0). */ eventKey: string | null; event: WebhookEventType; eventId: string | null; delivered: boolean; statusCode: number | null; attempts: number; error: string | null; /** True for a record written by `POST /v1/webhooks/{webhookId}/test` (one attempt, never counts towards suspension). 0.7.0. */ test: boolean; at: string | null; } /* ─────────────── client ─────────────── */ export class FleetaClient { constructor(opts: ClientOptions); apiKey: string; baseUrl: string; /** * Low-level request (escape hatch). The 3rd argument is an **envelope** — always wrap the request body in `body`. * * Passing the payload flat, like `request('POST', path, { psn })`, fails immediately with a `TypeError` at runtime * (it used to be dropped silently and the server answered 422). * * @throws {TypeError} when `opts` is not an object or has keys other than `query` and `body` */ request(method: string, path: string, opts?: { query?: Query; body?: any }): Promise; paginateCursor(fn: (p: any) => Promise>, params?: Query): AsyncGenerator; paginateOffset(fn: (p: any) => Promise>, params?: Query): AsyncGenerator; ping(): Promise; devices: { /** * List (GET /v1/devices). Both cloud and wifi — pass `{ category: 'cloud' }` for the dashcams cloud features apply to. * Every row carries `entitlement` / `cameraPosition` and the envelope a `cameras` seat summary (0.7.0); `entitlement` * filters one class (default `'all'`). */ list(query?: EntitlementQuery): Promise & { cameras: CameraCoverage }>; iterate(query?: EntitlementQuery): AsyncGenerator; /** Organization groups (GET /v1/groups). Not paginated — `data` is the full list. */ groups(): Promise>; /** Pre-register (POST /v1/devices). Returns registered/failed PSN lists — not a DeviceDetail. Answers 501 in production until write delegation is configured. */ create(body: DevicePreRegisterRequest): Promise>; get(psn: string): Promise>; update(psn: string, body: DeviceUpdateRequest): Promise>; battery(psn: string): Promise>; firmware(psn: string): Promise>; /** Settings read (read-only). PUT /settings was removed from the spec on 2026-08-07 — change settings in the web viewer/app. */ getSettings(psn: string): Promise>; /** Reboot. 409 when the device is offline; 403 `camera_limit_exceeded` for an 'over_limit' dashcam (0.7.0). */ reboot(psn: string): Promise>; }; /** * SD-card recordings · play and download. Even listing triggers IoT traffic to the device; `sdFileVideo` and * `createRecallJob` both start an upload over the vehicle's mobile data and count toward the recall allowance. * The SD-card methods (`sdFiles` · `sdFileMetadata` · `deleteSdFile` · `sdFileVideo`) and `createRecallJob` answer * `403 camera_limit_exceeded` for an 'over_limit' dashcam (0.7.0 — see `ProblemCameraLimitExceeded`); * `recallJobs()` / `recallJob()` / `cancelRecallJob()` are never locked (cancelling always works). */ media: { sdFiles(psn: string, query?: Query): Promise>; iterateSdFiles(psn: string, query?: Query): AsyncGenerator; sdFileMetadata(psn: string, filename: string, query?: Query): Promise>; /** Permanent deletion. */ deleteSdFile(psn: string, filename: string): Promise>; /** * Playable link for one recording (0.7.0) — one-call form of the recall flow: a recall job is created behind the scenes * (`jobId` → `recallJob()` / `cancelRecallJob()`) and `url` equals its `downloadUrl`. `quality` defaults to `'sub'` * (playback intent; `createRecallJob` keeps `'main'`). A GET with side effects — LTE data and recall allowance per call — * so request once and reuse until `expiresAt`. `filename` is the main recording name from `sdFiles()` (`…_NF.mp4`); * a name ending in `S.mp4` answers `422 invalid_field`, an unsupported `quality` answers `400 invalid_parameter`, * a busy device `409 device_busy` (Retry-After, no job created). */ sdFileVideo(psn: string, filename: string, query?: { quality?: RecordingStream }): Promise>; /** * Create a recall job. 409 `device_busy` (no job) while the device runs another transfer — with `retryOnBusy` the SDK * retries at the Retry-After interval (default 5 s) and rethrows the last 409 once the total timeout (default 60 s) passes. */ createRecallJob( body: RecallJobCreateRequest, opts?: { retryOnBusy?: boolean | { timeoutMs?: number } }, ): Promise>; recallJobs(query?: Query): Promise>; iterateRecallJobs(query?: Query): AsyncGenerator; recallJob(jobId: string): Promise>; /** Cancel answers 200 with the job (`status: 'canceled'`), not 204. */ cancelRecallJob(jobId: string): Promise>; }; telemetry: { /** Latest position per dashcam. Rows carry `entitlement`, the envelope a `cameras` seat summary; `entitlement` filter defaults to `'all'` (0.7.0). */ fleetLocations(query?: EntitlementQuery): Promise & { cameras: CameraCoverage }>; /** Cursor tail polling — pass the last nextCursor as after to receive only the increment. Keep the same `entitlement` between polls. */ fleetLocationsFeed(query?: EntitlementQuery): Promise & { cameras: CameraCoverage }>; trips(psn: string, query?: Query): Promise>; track(tripId: string, query?: Query): Promise>; createExportJob(body: GpsExportJobCreateRequest): Promise>; exportJobs(query?: Query): Promise>; exportJob(jobId: string): Promise>; /** 204 — cancelling an already-cancelled job is a no-op. */ cancelExportJob(jobId: string): Promise; }; events: { /** Rows carry `entitlement` (0.7.0); the `entitlement` filter defaults to `'all'`. No `cameras` block on event lists. */ list(query?: EntitlementQuery): Promise>; iterate(query?: EntitlementQuery): AsyncGenerator; /** Cursor tail polling — keep the same `entitlement` between polls. */ feed(query?: EntitlementQuery): Promise>; get(eventId: string): Promise>; /** * Video presigned URL. Channel defaults to front; a channel not listed in the event's `channels` answers 404. * 403 `camera_limit_exceeded` when the event's dashcam is 'over_limit' (0.7.0) — the thumbnail still works. */ video(eventId: string, query?: { channel?: Channel }): Promise>; /** Thumbnail presigned URL. Defaults to `channels[0]`. For a whole page use `list({ include: 'thumbnail' })`. Never locked by the camera limit. */ thumbnail(eventId: string, query?: { channel?: Channel }): Promise>; /** All dashcams by default (matches the web viewer's dashboard); `entitlement` restricts the aggregate. Envelope carries `cameras` (0.7.0). */ stats(query?: SafetyEventStatsQuery): Promise & { cameras: CameraCoverage }>; }; geofences: { list(query?: Query): Promise>; iterate(query?: Query): AsyncGenerator; create(body: GeofenceCreateRequest): Promise>; get(id: string): Promise>; update(id: string, body: GeofenceUpdateRequest): Promise>; /** 204. */ delete(id: string): Promise; alerts(query?: Query): Promise>; /** Cursor auto-iteration over alert history. */ iterateAlerts(query?: Query): AsyncGenerator; }; insights: { /** * GET /v1/fleet/summary. Counts **'covered' dashcams by default** (0.7.0 — the web viewer's numbers); pass * `{ entitlement: 'all' }` to include over-limit dashcams. The envelope carries a `cameras` seat summary. */ fleetSummary(query?: EntitlementQuery): Promise & { cameras: CameraCoverage }>; /** * GET /v1/reports/driving — one row per device over an inclusive `from`–`to` period (default: last 7 days, max 92 days). * `from` / `to` take `YYYY-MM-DD` **or** an RFC 3339 date-time (e.g. `new Date().toISOString()`); a date-time is * reduced to the UTC calendar date of that instant. Send an offset-bearing RFC 3339 string — the form the events * `from`/`to` also accept; offset-less or free-form strings are a 400 `invalid_parameter` here even though the * events endpoints tolerate them. * Rows are **'covered' dashcams by default** (0.7.0 — the web viewer's report numbers); pass `{ entitlement: 'all' }` * to include over-limit dashcams. The envelope carries a `cameras` seat summary. */ drivingReports(query?: EntitlementQuery): Promise & { cameras: CameraCoverage }>; /** * GET /v1/reports/driving/{psn} — a single day. Takes only `date` (`YYYY-MM-DD`, default yesterday UTC). * Passing `from` / `to` here is a 400 `invalid_parameter` (not silently ignored); for a range use `drivingReports`. * An over-limit dashcam is still reported (200, `entitlement: 'over_limit'`) — only the list's default filter leaves it out. */ drivingReport(psn: string, query?: Query): Promise>; }; admin: { auditLogs(query?: Query): Promise>; iterateAuditLogs(query?: Query): AsyncGenerator; auditLog(id: string): Promise>; /** API call logs (GET /v1/api-logs — cursor). Filters: from · to · status · method · route · keyId. */ apiLogs(query?: Query): Promise>; iterateApiLogs(query?: Query): AsyncGenerator; /** GET /v1/usage. The envelope's `cameras` block (0.7.0) adds `overLimitPsns` — the dashcams currently locked by the camera limit. */ usage(): Promise & { cameras: UsageCameras }>; }; webhooks: { list(query?: Query): Promise>; /** 201 — `data.secret` is visible only in this response. */ create(body: WebhookCreateRequest): Promise>; get(id: string): Promise>; /** 204. */ delete(id: string): Promise; test(id: string): Promise>; deliveries(id: string, query?: Query): Promise>; /** 200 — suspended → active, same URL/events/secret. 409 `webhook_not_suspended` when it is not suspended (0.6.0). */ reactivate(id: string): Promise>; /** 200 — `data.secret` is the **new** secret, visible only in this response; the previous one co-signs until `previousSecretExpiresAt` (0.6.0). */ rotateSecret(id: string, body?: WebhookRotateSecretRequest): Promise>; }; }