# Changelog

All notable changes to `@fleeta/sdk`. The API itself is documented at https://developers.fleeta.io.

## 0.9.0 — 2026-09-20

**Added**

- **`events.stats()` takes `tz`, and `SafetyEventStats` gained `hourTz`.** `byHour` has always been counted
  in UTC, which left every caller outside UTC to shift the 24 buckets themselves — a multilingual audit on
  2026-09-20 found clients getting that shift wrong (off-by-one hours, am/pm flips) often enough to misread
  the daily peak. Pass `tz` — an IANA zone name such as `'Asia/Seoul'` (a UTC offset is rejected: it cannot
  express daylight saving) — and the
  aggregation groups the buckets in that zone, with daylight saving applied per event; the response echoes
  the zone as `hourTz`. Omitting `tz` keeps the previous behaviour exactly (`hourTz: 'UTC'`), so this is
  additive. Only `byHour` is affected: `from`/`to` and every timestamp stay RFC 3339 UTC.

**Changed (breaking)**

- **`UsageVolumeQuotaBucket` no longer has `video_url`; it has `transfer_bytes`, and `UsageVolumeQuota` gained
  `unit`.** The monthly video allowance is metered in **bytes delivered** rather than in issued URLs from
  2026-09-19: `GET /v1/usage` reports `{ bucket: 'transfer_bytes', limit, used, period, unit: 'bytes' }`
  (event video URLs count at the clip size, SD-card recalls at the recording size, both when the link is
  issued), and `export_job` / `export_concurrent` carry `unit: 'count'`. TypeScript code that switches on the
  bucket name or formats `limit` / `used` has to be updated — divide by 2^30 for GB when `unit` is `bytes`.
  `ProblemQuotaExceeded.limit` / `used` are bytes for that bucket too, and the human-readable `detail` says GB.

**Changed**

- **`GeofenceRecordingEvents.speedKmh` is returned by read endpoints.** The type said it was
  write-only and never read back; that was true of the older stored record but not of zones written
  through this API or the web viewer since 2026-08-31, which carry a speed threshold and return it.
  Found on 2026-09-18 by calling production with a real key — the sandbox models the older shape, so
  it never showed there. `GeofenceRecordingEvents` now declares `speedKmh?: number`.

## 0.8.0 — 2026-09-14

> ⚠️ **0.7.0 is already on npm** (published 2026-09-02) and carries only the `media.sdFileVideo`
> addition from this list — it went out while this block still said "unpublished", and four rounds of
> changes were appended to it afterwards. Everything else below therefore shipped as **0.8.0**; the
> entries are cumulative against 0.6.0. `@fleeta/mcp-server` now pins `@fleeta/sdk` at `^0.8.0`
> (widened when this release went out).
>
> Published to npm on 2026-09-14 and current `latest`. **Not a drop-in**: no method signature changed incompatibly, but `RecordingType`
> lost four of its seven values and `SdFile` / `SdFileMetadata` gained two fields, so TypeScript
> code that reads an SD-card recording type has to be updated. The camera-limit gate (2026-09-08)
> adds required fields to the device, location, event, alert and report rows and makes the driving
> reports and fleet summary count **covered dashcams only by default**. Read **Changed (breaking)**
> and **Changed** first — the rest of the release is additive.

**Changed (breaking)**

- **SD-card recording types now use the `GET /v1/events` vocabulary.** `RecordingType` no longer
  contains `'event'`, `'impact'`, `'automatic'` or `'timelapse'`. `automatic` and `timelapse` were
  **wrong labels** — they meant harsh acceleration and a sharp turn (2026-09-03). `event` and
  `impact` meant the right thing but were a **second set of words for the same clip**: the SD-card
  list called it `event` while `GET /v1/events` called that very recording `driving_impact`
  (2026-09-04). The four are replaced by `harsh_acceleration`, `sharp_turn`, `driving_impact` and
  `parking_impact`, spelled exactly as in `EventType`, so one recording carries one name across the
  API and no translation table is needed between the two endpoints.
  What this breaks in a consumer's build: `file.type === 'event'` no longer compiles (TS2367 — the
  types no longer overlap), and a `Record<RecordingType, …>` map or an exhaustive `switch` now
  misses the new members. Filter values are **not** type-checked (`sdFiles(psn, query)` takes a
  loose `Query`), so a stale `{ type: 'event' }` still compiles — at runtime the API answers
  `400 invalid_parameter` with the accepted values in `allowed`, never an empty list you might read
  as "no such footage".
- **Twelve categories that used to be `null` are now named**, so `RecordingType` grows from 7 values
  to 20: `harsh_braking`, `overspeed`, the driver-monitoring types (`drowsy`, `distracted`,
  `undetected`, `calling`, `smoking`, `seatbelt`) and the four `geofence_*` types. They were absent
  from the old table, so those clips came back with `type: null` and **disappeared from every `type`
  filter** while still counting toward the unfiltered total.
- `RecordingType` gains **`'unknown'`** — a category letter outside the table. It is **output-only**:
  as a filter value it answers `400`. `RecordingDirection` gains `'unknown'` the same way. It has
  allowed `'option'` (the 7-BOX option camera) through `Channel` since 0.6.0, but the endpoint used
  to answer `null` for those clips, so they vanished from `direction` filters — they are now named.
- `SdFile` and `SdFileMetadata` gain **`typeCode`** and **`directionCode`** (`string | null`) — the
  raw letters from the filename, which say *why* a `type` came back as `unknown`. A filename that is
  off-convention entirely still leaves every decoded field `null`.

**Changed**

- **Driving reports and the fleet summary count covered dashcams by default** (2026-09-08).
  `insights.drivingReports()` and `insights.fleetSummary()` now leave dashcams registered beyond the
  subscription's camera count (`entitlement: 'over_limit'`) out of their rows and totals — the same
  numbers the Fleeta web viewer's reports show. An organization with over-limit dashcams (they only
  arise after a plan downgrade or lapse) sees smaller totals than with 0.6.0; pass
  `{ entitlement: 'all' }` for the previous, everything-included figures. `drivingReport(psn)` is
  unchanged — it answers for any dashcam (`entitlement: 'over_limit'` on the row). Device, event and
  location lists keep listing everything (default `'all'`), and `events.stats()` keeps counting every
  dashcam.
- Row types gain a **required** `entitlement: Entitlement` field: `DeviceSummary`, `FleetLocation`,
  `SafetyEvent`, `GeofenceAlert`, `DrivingReport` (so `DrivingReportDetail` too), and the inline rows
  of `FleetSummary.disconnectedDevices` / `topDevicesByEvents` and `SafetyEventStats.topDevices`.
  `DeviceSummary` also gains `registeredAt`, `lastLoginAt`, `connectivity` and `cameraPosition`
  (required-nullable) and `DeviceDetail` gains `requiredCameraLimit`. Additive for code that reads
  responses; code that builds these literals by hand must add the fields.
- `DevicePreRegisterFailureCode` grows from 2 to 6 values (`unknown_psn`, `blacklisted`,
  `backend_error`, `camera_limit_exceeded`, matching the spec enum) — an exhaustive `switch` over it
  now misses the new members.

**Added**

- **`vehicle`, `sim`, `battery` and `requiredCameraLimit` on `DeviceSummary`** — `devices.list()` rows now carry the
  same fields `devices.get()` returns, so a fleet table with licence plates is one call instead of one per dashcam.
  `Vehicle` gains `tag` (the label the web viewer groups vehicles by; read-only — `devices.update()` does not accept it).
  `DeviceDetail` stays as an alias of `DeviceSummary` so existing code keeps compiling.

- **`WebhookTestResult.sandbox`.** A sandbox key performs no outbound requests, so
  `POST /v1/webhooks/{webhookId}/test` now answers truthfully: `sandbox: true`, `delivered: false` and an
  `error` saying nothing was sent. It used to hardcode `delivered: true, statusCode: 200` without sending
  anything, so a receiver verified only in the sandbox had never actually been reached (2026-09-08 QA).
  The `signatureHeader` is still computed exactly as a live delivery computes it, and the attempt is now
  recorded in the delivery list with `test: true`.

- **Camera-limit gate** (2026-09-08). New problem code **`403 camera_limit_exceeded`**, typed as
  `ProblemCameraLimitExceeded` — `cameraLimit`, `registeredCameras`, `requiredCameraLimit` (always);
  `psn`, `cameraPosition` (single-device routes); `psns`, `remainingSeats` (`devices.create()`).
  `e.is('camera_limit_exceeded')` narrows to it; `OpenApiError.CODES` / `ProblemCode` grow to 53.
  A dashcam registered beyond the subscription's camera count (`entitlement: 'over_limit'` — seats go
  to the most recently registered dashcams) stays **listed and readable**: location, trips, events,
  thumbnails, statistics, geofence alerts and GPS export answer as before. Only **`events.video()`,
  the SD-card `media.*` methods (list / metadata / delete / `sdFileVideo`) plus `media.createRecallJob()` and
  `devices.reboot()`** throw it for such a dashcam, and `devices.create()` throws it when the batch
  would exceed the count (all-or-nothing — nothing is registered). Raise the camera count in the
  Fleeta web viewer (Account › Subscription): the same key picks it up within 5 minutes, no reissue.
  No `Retry-After`, and the SDK never retries it. Sandbox keys are never locked.
- Types `Entitlement` (`'covered' | 'over_limit'`), `EntitlementFilter` (`Entitlement | 'all'`),
  `CameraCoverage` (`{ limit: number | null; registered: number; overLimit: number }`),
  `UsageCameras` (`CameraCoverage` + `overLimitPsns: string[]`) and `DeviceConnectivity`
  (`'lte' | 'wifi'`).
- **`cameras` envelope block** beside `data` on `devices.list()`, `telemetry.fleetLocations()` /
  `fleetLocationsFeed()`, `insights.fleetSummary()` / `drivingReports()` and `events.stats()`
  (`CameraCoverage`), and on `admin.usage()` (`UsageCameras`, whose `overLimitPsns` names the dashcams
  currently locked). The declared return types are `… & { cameras: CameraCoverage }` — still
  assignable to `Page<T>` / `Single<T>`, so `iterate()` helpers and existing destructuring keep
  working. `limit` is `null` when the subscription could not be resolved for the request; nothing is
  locked then. Event lists carry no `cameras` block.
- Typed **`entitlement` query filter** (`'covered' | 'over_limit' | 'all'`) on `devices.list()` /
  `iterate()`, `events.list()` / `iterate()` / `feed()` / `stats()`, `telemetry.fleetLocations()` /
  `fleetLocationsFeed()` and `insights.fleetSummary()` / `drivingReports()`. Any other value answers
  `400 invalid_parameter`. Keep the same value between feed polls.
- Device fields: `DeviceSummary.registeredAt` / `lastLoginAt` (`string | null`), `connectivity`
  (`DeviceConnectivity | null` — separate from `category`), `cameraPosition` (`number | null`,
  1 = most recently registered; `null` for a Wi-Fi-only dashcam that holds no seat) and
  `DeviceDetail.requiredCameraLimit` (`number | null` — the count to raise the subscription to, set
  only when `over_limit`). Spec now 52 operations / 85 schemas.
- `WebhookDelivery.test` — `true` for records written by `webhooks.test()` (`POST /v1/webhooks/{webhookId}/test`). Test deliveries are now recorded in `webhooks.deliveries()`; they still never count towards automatic suspension.

- `media.sdFileVideo(psn, filename, { quality? })` — `GET /v1/devices/{psn}/sd-files/{filename}/video`
  (`getSdFileVideo`, scope `media:recall`). A playable link for one SD-card recording, returned as
  `SdFileVideoUrl` `{ url, expiresAt, jobId, sizeBytes, receivedBytes, quality }` — the one-call form
  of the recall flow, mirroring `events.video()`. A recall job is created behind the scenes: `url`
  equals that job's `downloadUrl`, and `jobId` works with `media.recallJob()` / `cancelRecallJob()`.
  `quality` defaults to `'sub'` (playback intent) — `createRecallJob()` keeps its `'main'` default.
  `expiresAt` is `string | null` (null = deadline unknown, not unlimited); `receivedBytes` is
  `number | null` and always present (always `null` for a sandbox key).
  **A `GET` with side effects** — each call spends the vehicle's LTE data and counts toward the recall
  allowance, so request the link once and reuse it until `expiresAt`.
  Errors: `400 invalid_parameter` (bad `quality`, `allowed: ['main','sub']`), `422 invalid_field`
  (a filename ending in `S.mp4` or off-convention — pass the main recording name and select the
  sub-stream with `quality`), `409 device_busy` (Retry-After, no job created), plus the usual
  `device_not_found` / `file_not_found` / `device_offline` / `recall_limit_exceeded` / `cloud_only` /
  `501` / `502 delegate_failed` / `504 command_timeout`. One new problem code, `geofence_unsupported_shape`
  (`409` on `PUT /v1/geofences/{id}` — already thrown by the server, now documented), so `OpenApiError.CODES` grows to 50
  (then 52 on 2026-09-04 — `unsupported_device` added, `validation_error` merged into `invalid_parameter`, `invalid` extension
  field dropped — and 53 with `camera_limit_exceeded` above).
- `SdFileVideoUrl` type. Spec 52 operations (was 51).
- **Server-judged speed-limit events** (2026-09-03). `EventType` gains `'speed_limit'` — a road speed-limit
  violation Fleeta judges from the vehicle's GPS track and map speed-limit data (distinct from `'overspeed'`,
  the dashcam's own fixed-threshold alert). `SafetyEvent.speedLimit: SpeedLimitDetail | null` — set only on
  `speed_limit` events: `{ limitKmh, overKmh, durationSec, roadName, country, endedAt, peakAt, schoolZone }`.
  On those events `location` and `speedKmh` are real values (position and speed at the peak); `channels` is
  empty and `hasVideo` is `false` — `events.video()` / `events.thumbnail()` answer `404 video_not_available`.
  `events.list()`, `events.feed()` and `events.stats()` include them by default; pass `type: 'speed_limit'`
  to select them alone. This item is additive — it changes no existing field, type or enum value
  (the recording-type vocabulary above is the release's one breaking change).

**Docs**

- `RecallJob.sizeBytes` is the size of the **requested stream** (main or sub) as reported by the
  device — the previous "full size of the recording" wording was wrong for `quality: 'sub'`.
- Recall comments reframed: `downloadUrl` is a link that **plays or downloads** the recording (the file
  streams while the dashcam is still uploading); `POST /v1/media/recall-jobs` is "Recall a recording
  from the SD card". The `downloadUrl` field name is unchanged (job resources — GPS export too — keep
  `downloadUrl`; the video endpoints return `url`).

## 0.6.0 — 2026-09-01

> ⚠️ Not a pure drop-in: the type signatures are unchanged, but the **runtime meaning** of
> `SafetyEvent.eventId`, `channels`, `hasVideo` and of a short event page changed with the
> API release of the same date. Read **Changed** before upgrading.

**Changed**

- `SafetyEvent` now describes **one event, not one uploaded file**. `eventId` identifies the
  event (stable for its life — uploading another channel later never changes it), `channels`
  is the union of its playable channels (`["front","rear"]` on a single row), and `hasVideo`
  always equals `channels.length > 0`. IDs stored before the release still resolve.
- `Channel` gained `'option'` (7-BOX option camera).
- `SafetyEvent.thumbnailUrl?: string | null` — present only when the request asked for
  `include: 'thumbnail'` on `events.list()` / `events.feed()`. It is the same URL
  `events.thumbnail()` issues, costs no extra request and no quota.
- `events.thumbnail(eventId, { channel? })` — second argument added; defaults to `channels[0]`.
- **Pagination:** a page can hold fewer items than `limit` — even zero — while `hasMore` is
  `true`. `paginateCursor` (used by `events.iterate()`) already follows `hasMore`, so
  auto-iteration is unaffected; hand-rolled loops that stopped on a short page must not.
- Event `type` values are read from the recording's own event code now. An event that used to
  arrive as `distracted` may arrive as `manual`, and `undetected` / `calling` / `seatbelt` /
  `geofence_speed` filters return rows for the first time.

**Added**

- `OpenApiError.is(code)` — type guard that narrows `error.body` to the per-code shape:
  `if (e.is('cloud_only')) e.body.psns`. Backed by 17 new `Problem<Code>` interfaces
  (`ProblemCloudOnly`, `ProblemQuotaExceeded`, `ProblemInvalidField`, …) that mirror the spec's
  `components.schemas` one-to-one (75 schemas now; `npm run sdk:types` keeps them in sync).
  `ProblemQuotaExceeded.bucket` / `.limit` are optional — the API-edge usage-plan variant of
  `429 quota_exceeded` is generated at the gateway and carries no extension fields.
- `OpenApiError.isKnown()` — narrows to `KnownOpenApiError`, a discriminated union over every
  documented code, for exhaustive `switch (e.code)`.
- `OpenApiError.CODES` — the documented code list (49) as a `ReadonlySet`.
- `webhooks.reactivate(id)` — resume a subscription the dispatcher suspended (`POST /v1/webhooks/{id}/reactivate`);
  `409 webhook_not_suspended` (typed `ProblemWebhookNotSuspended`, `subscriptionStatus`) when it is not suspended.
- `webhooks.rotateSecret(id, { graceSeconds? })` — new signing secret, returned once as `WebhookSecretRotated.secret`;
  the previous secret co-signs (`X-Fleeta-Signature: v1=<new>,v1=<previous>`) until `previousSecretExpiresAt`
  (default 24 h, `0` = revoke now). Update your verifier to accept any matching part **before** rotating.
- `Webhook.suspendedAt` / `Webhook.previousSecretExpiresAt` (both `string | null`) and `WebhookDelivery.eventKey`
  (`string | null` — the stable `X-Fleeta-Event-Key`, dedupe on it instead of `deliveryId`). Required-nullable,
  so code that builds `Webhook` literals by hand must add the two fields; code that only reads responses is unaffected.
- `ProblemInvalidField.min` / `.max` — bounds of a numeric field that failed a range check.
- Spec now 51 operations / 75 schemas (`WebhookRotateSecretRequest`, `WebhookSecretRotated`, `ProblemWebhookNotSuspended`).
- Types: `ProblemCode`, `ApiScope`, `ProblemByCode`, `ProblemFor<C>`, `OpenApiErrorOf<C>`, `KnownOpenApiError`.
- `OpenApiError.code` is now typed `ProblemCode | (string & {})` — documented codes autocomplete,
  unknown strings still accepted (additive-only).

**Docs**

- `GpsExportJobCreateRequest.to` now documents the 90-day cap; the API answers `422 invalid_field`
  (`maxRangeDays`, `rangeDays`) instead of `502 delegate_failed` for wider ranges.

Drop-in: the runtime additions are `is()` / `isKnown()` / `CODES` on `OpenApiError` and the two
`webhooks` methods; no existing method changed. Type-level, `Webhook` gains two required-nullable fields.

## 0.5.1 — 2026-08-31

**Changed**

- Documentation and messages are now English throughout: README, the type declaration
  comments shown by editors (`index.d.ts`), source comments, and the runtime error messages
  thrown by the client (`apiKey is required.`, `fetch is not available…`, the `request()`
  envelope `TypeError`). No API or behavior change — a drop-in replacement for 0.5.0.

## 0.5.0 — 2026-08-31

**Changed (2026-08-31)**

- `DeviceCategory` is `'cloud' | 'wifi'` again. `GET /v1/devices` lists Wi-Fi-only
  dashcams alongside cloud-connected ones (they are always `offline`), and accepts
  `category=cloud|wifi`. Cloud-only features — location, trips, events, video,
  SD-card access, recall, settings, reboot, GPS export — answer `422 cloud_only`
  for a Wi-Fi-only PSN (`error.psn` / `error.psns`, `error.category: 'wifi'`)
  instead of `404 device_not_found` / `422 unknown_psn`.

Type declarations are now synchronized field-by-field with `openapi.yaml`
(56 component schemas + operation-inline schemas, 2026-08-24 spec), and every
one of the 49 operations has an SDK method. Two checks keep it that way:
`npm run sdk:types` (spec ↔ `index.d.ts` drift + operation coverage) and
`npm run sdk:typecheck` (`index.test-d.ts` compile fixture).

**Breaking**

- `RecallJobStatus` is `'ready' | 'canceled'` (American spelling). The server,
  spec and SDK were unified on `canceled` on 2026-08-26; code comparing against
  `'cancelled'` no longer compiles and never matched the runtime value anyway.
- `request(method, path, opts)` throws `TypeError` when `opts` is not a
  `{ query, body }` envelope — e.g. `request('POST', p, { psn, filename })`.
  Before 0.5.0 the payload was silently dropped and the server answered 422.
  The declaration rejects the same misuse at compile time.
- `devices.create()` returns `Single<DevicePreRegisterResult>`
  (`{ registered: string[], failed: { psn, code }[] }`) — this was always the
  runtime shape; the previous `DeviceDetail` declaration was wrong.
- `Usage.volumeQuotas` is `UsageVolumeQuota[]` (an array — the runtime shape),
  not `Record<string, any>`. `Usage.rate` is `UsageRate { limitRps, burst }`.
- Automatic 429 retry no longer waits longer than 60 s and never retries
  `monthly_limit_exceeded` / `quota_exceeded` — those are thrown immediately.

**Added**

- `devices.groups()` (`GET /v1/groups`), `admin.apiLogs(query)` /
  `admin.iterateApiLogs(query)` (`GET /v1/api-logs`, cursor) and
  `geofences.iterateAlerts(query)` — the SDK now wraps all 49 operations.
- Response types: `GeofenceAlert`, `GpsExportJob`, `GpsExportJobCreated`,
  `EventMediaUrl`, `EventVideoUrl`, `DeviceSettings` /
  `DeviceSettingsSectionName`, `DevicePreRegisterResult` /
  `DevicePreRegisterFailure`, `DeviceCommandResult`, `Group`, `ApiLog`,
  `AuditLogTarget`, `SdFileDeleteResult`, `WebhookCreated` (with `secret`),
  `WebhookTestResult`, `WebhookDelivery`, `Problem`, `NoContent`, and the
  `Usage` sub-types (`UsageApiCalls`, `UsageRate`, `UsageVolumeQuota`,
  `UsageCallHistory`, `UsageCallHistoryDay`, `UsageStatusBreakdown`,
  `UsageStatusClassCount`, `UsageStatusClassShare`).
- `Usage.callHistory` / `Usage.statusBreakdown` (30-day rollup, `null` when no
  log source) and `Usage.tier` as the `UsageTier` union.
- Request types: `WebhookCreateRequest`, `GpsExportJobCreateRequest`,
  `GeofenceCreateRequest` / `GeofenceUpdateRequest`, `DevicePreRegisterRequest`,
  `DeviceUpdateRequest`, `RecallJobCreateRequest` — `body: any` is gone from
  every write method.
- Enum unions: `WebhookEventType`, `WebhookStatus`, `GpsExportFormat`,
  `GpsExportJobStatus`, `AuditLogCategory`, `AuditLogStatus`, `ApiLogMethod`,
  `HttpStatusClass`, `UsageVolumeQuotaBucket`, `DevicePreRegisterFailureCode`.
- `CursorPage<T>` / `OffsetPage<T>` aliases (same shape as `Page<T>`) so every
  list method's signature says which pagination it uses. The three 204
  operations (`webhooks.delete`, `geofences.delete`,
  `telemetry.cancelExportJob`) return `NoContent` (`{ data: null }`).
- `OpenApiError.body` is typed as `Problem & Record<string, any>`.
- `GeoPoint` is `{ latitude: number; longitude: number }` — coordinates are
  never `null` field-by-field; an unknown location is a `null` object
  (`GeofenceAlert.location`, `GeofenceShape.center`, `SafetyEvent.location`).

**Fixed**

- `devices.getSettings(psn)` is declared (it existed at runtime since 0.3.0 but
  was missing from `index.d.ts`, so TypeScript callers had to fall back to
  `request()`).
- `webhooks.create()` returns `Single<WebhookCreated>` so `data.secret` is
  reachable from TypeScript.
- `devices.reboot()`, `events.video()` / `events.thumbnail()`,
  `webhooks.test()` / `webhooks.deliveries()`, `media.deleteSdFile()` and the
  GPS export methods no longer return `any`.
- `AuditLogSummary.category` / `status` are enum unions; `target` is
  `AuditLogTarget | null`.

## 0.4.0 — 2026-08-11

**Breaking** — recall jobs are now created only when the device transfer actually starts.

- **Changed** `POST /media/recall-jobs`: if the dashcam is busy the API returns
  `409 device_busy` (with `Retry-After`) and **no job is created**. On success the job is
  born `ready` with a working `downloadUrl` (HTTP 201).
- **Removed** `RecallJob` statuses `queued` / `failed` (never reachable now) and the
  `attempts` / `error` fields. Status set is `ready | cancelled` (spelling changed to
  `canceled` in 0.5.0).
- **Added** `createRecallJob(body, { retryOnBusy })` — retries on `409 device_busy`
  honouring `Retry-After` (default 5 s) up to a total timeout (default 60 s).
- **Added** `RecallJob.receivedBytes?` — upload progress probe (present only when the
  server could verify it).
- **Added** `OpenApiError.retryAfter?` — seconds from the `Retry-After` header (429/409).

## 0.3.0 — 2026-08-08

First release published from the public repository ([fleeta-io/sdk-js](https://github.com/fleeta-io/sdk-js)).

- **Added** `devices.getSettings(psn)` — read current device settings (read-only).
- **Added** full response typings (~30 interfaces) — previously most responses were `any`:
  nullable telemetry fields (`speedKmh` / `heading`), trip-derived metrics, SD-file GPS &
  G-sensor metadata, driving-style scores, hourly report buckets and more.
- **Changed** event `type` vocabulary grown to 17 values (adds `calling`, `smoking`).
- **Removed** `devices.updateSettings` — the settings write endpoint was withdrawn before GA
  (settings changes are made in the viewer app, which validates every field).
- Package metadata: `repository` / `bugs` now point at the public GitHub repository.

## 0.2.0 — 2026-07

- Fleeta branding (`FleetaClient`, `flt_` keys, `X-Fleeta-*` headers).
- Coverage for media recall, SD-card listings, groups, driving reports and webhooks.
- Automatic cursor pagination (`iterate()`), 429 retry with `Retry-After`.

## 0.1.x — 2026-07

- Initial internal releases.
