Skip to main content

Changelog

The API change history. Each entry carries one of the following labels.

LabelMeaning
ADDEndpoint / field / feature addition (backward compatible)
CHANGEDChange to existing behavior (backward compatibility preserved)
REMOVEDEndpoint / feature withdrawal — only before general availability, or after the BREAKING process
BREAKINGBreaks backward compatibility — applied only after advance notice plus a migration period

/v1 follows an additive-only policy from general availability onward — we only add fields and endpoints; we never remove, rename, or change types. Implement clients to ignore unknown fields.

v1 — General availability

The first public release of the Fleeta Open API. Everything below is live on https://openapi.fleeta.io/v1 and https://mcp.fleeta.io/.

  • ADD GET /v1/safety-events/stats can count the hour buckets in your own time zone. byHour has always been counted in UTC, leaving every caller outside UTC to shift all 24 buckets themselves — and a review of AI-assistant answers on 2026-09-20 found that shift going wrong often enough (off-by-one hours, am/pm flips) to misreport the daily peak. Pass tz with an IANA zone name (tz=Asia/Seoul) and the aggregation groups the buckets in that zone, applying daylight saving per event, so a period that crosses a transition is still counted correctly. A UTC offset (+09:00) is rejected with 400 invalid_parameter for exactly that reason — it cannot express the transition. The response now carries hourTz, the zone the buckets were counted in, so a chart can be labelled from the payload alone. Omitting tz keeps the previous behaviour byte for byte (hourTz: "UTC"), and nothing else moves: from/to and every timestamp in the response stay RFC 3339 UTC. @fleeta/sdk adds tz to events.stats() and hourTz to SafetyEventStats; the MCP safety_event_stats tool takes tz and is told to pass the viewer's zone whenever it will show an hour-of-day breakdown. See Safety events › Hour buckets and time zones.

  • CHANGED Video is metered by volume, not by link count — the video_url quota bucket is replaced by transfer_bytes. GET /v1/usage no longer reports video_url; it reports { bucket: "transfer_bytes", limit, used, period, unit: "bytes" }, and every bucket now carries unit (bytes or count). The allowance is the video delivered to you in a month: an event video URL counts at the clip's size (a flat 6 MB) and an SD-card recall (POST /v1/media/recall-jobs, GET /v1/devices/{psn}/sd-files/{filename}/video) at the recording's size (sizeBytes, or a flat 8 MB sub / 150 MB main when the dashcam does not report it), both when the link is issued; thumbnails, listings, metadata, job status and export files do not count. It is 0.8 GB per subscription dollar on Standard, 1 GB on Pro and 1.2 GB on Enterprise — 5.6 / 10 / 18 GB per camera and month. Until now the cap was a count of issued links (20,000 per camera) that treated a 5 MB clip and a 400 MB recording alike, and SD-card recalls had no monthly cap at all. Recalls are now checked before the dashcam is contacted: when the remaining allowance cannot cover the file, the call answers 403 quota_exceeded and no job is created. On that error limit and used are bytes and detail states the limit in GB. export_job moves from 20,000 to 50 jobs per camera a month (Pro and above — the most any fleet has created is 25 a month). If your code switches on the bucket name or formats limit / used, update it; the SDK types (UsageVolumeQuotaBucket, UsageVolumeQuota.unit) change with the next @fleeta/sdk release. See Plans & Limits and Rate Limits.

  • ADD Road speed-limit violations are now delivered over webhooks. GET /v1/events has returned speed_limit — the server's judgement that a vehicle exceeded the posted limit for the road it was on, made by comparing its GPS track against map data — since the type was introduced, but that event never reached webhook subscribers. safety_event carried only the dashcam's own fixed-threshold overspeed, so an integration built on webhooks saw a different set of speeding events than the same account saw through the read API, with nothing in the response to indicate the gap. safety_event deliveries now include subType: "speed_limit", using the same value GET /v1/events uses for the same event. The delivery carries a data.speedLimit object — limitKmh, overKmh, durationSec, roadName, named exactly as Event.speedLimit is on the read API so the two compare directly — and data.location.speedKmh holds the peak speed reached. Fields the map data could not determine are omitted rather than sent as null: a road with no posted limit has no limitKmh, and without it there is no overKmh. There are no coordinates on this subType; GET /v1/events remains the place to get the violation's location, endedAt, peakAt, country and schoolZone. overspeed is unchanged and still arrives separately — the two are different events and a vehicle can trigger both on one drive, so route them separately if you act on them. Existing subscriptions receive the new subType without any change on your side; the webhook guide has always said to ignore subType values you do not recognise.

  • FIXED A malformed request to a path that does not exist answered 500. A request whose method and path match no route already answers 404 route_not_found — but if it also carried a body with a content type other than JSON (some HTTP clients set one by default), the gateway answered a bare 500 {"message": "Internal server error"} with no requestId, so a typo in a URL looked like an outage on our side. It now answers the same 404 route_not_found regardless of the body or its content type. Nothing about valid requests changed, and no request body was ever echoed back.

  • FIXED GET /v1/events/feed never reported that you had caught up. The reference tells you to keep polling until hasMore is false; on a sandbox key it stayed true forever, because a tail feed keeps its resume cursor even when there is nothing left and hasMore was derived from the cursor rather than from the backlog. A client that followed the instruction polled empty pages indefinitely. hasMore is now false once the feed is caught up — the cursor is still returned, and you resume with it on the next poll, exactly as GET /v1/fleet/locations/feed already behaved. start=latest also returns hasMore: false, since attaching to the tail means there is no backlog by definition.

  • FIXED Sandbox GPS exports ignored the requested format. A job created with format: "gpx" reported format: "gpx" and then handed back the same CSV, and the finished job was missing downloadFileName and downloadUrlExpiresIn — both of which live keys return. The sandbox now produces a zip archive in the requested format (csv / gpx / kml) laid out the way a live export is — one export.<ext> entry, the live CSV column set, the live GPX/KML structure — with the same suggested filename convention live keys use, and a running job carries completedAt / expiresAt as null instead of omitting the keys, so code that branches on them behaves the same in both places.

  • BREAKING Trip coordinates use latitude / longitude like every other coordinate in the API. GET /v1/devices/{psn}/trips returned startLocation and endLocation as {lat, lng} — the only two fields in the whole API that used the short names. Reading location.latitude worked everywhere except there, so code written against one endpoint broke on the other, and the response standard had specified the unification since the mock data was replaced with real data. Both fields are now {latitude, longitude}; nothing else about them changed (still null when the trip has no usable GPS fix, still no address). @fleeta/sdk types Trip.startLocation / endLocation as GeoPoint, and the MCP list_trips description follows. LatLng remains in the SDK, deprecated, so existing type references keep compiling. This is the last coordinate-shape change before general availability.

  • CHANGED The fleet summary and driving reports count cloud-connected dashcams only, and now say so. GET /v1/fleet/summary and GET /v1/reports/driving left Wi-Fi-only dashcams out — correctly, since those never reach the cloud and so have no status, location or driving data — but nothing in the reference said it, so a fleet of 12 dashcams read as 12 in GET /v1/devices and 10 on the dashboard with no explanation. The exclusion is now documented on FleetSummary.fleet.total and in the MCP fleet_summary tool. One real inconsistency went with it: a dashcam registered as Wi-Fi inside the main device registry was counted anyway, so the same kind of dashcam was included or excluded depending on where its record happened to live. It is now excluded everywhere, matching what GET /v1/devices reports as its category. Camera seats are unaffected — such a dashcam still consumes one, exactly as the Fleeta web viewer and the mobile apps count it (cameras.registered documents this).

  • FIXED POST /v1/devices answered 500 when a psn was not a string. A JSON number, boolean or null row (an unquoted serial, most often) hit a type error instead of validation. That row now fails with invalid_psn in failed[] like any other malformed serial, and the rest of the batch is unaffected. A numeric psn is rejected rather than silently accepted: psn is a string in the schema, and a missing pair of quotes should not register a device.

  • CHANGED Sandbox writes are kept for 24 hours instead of living in one server's memory. Creating a geofence or a webhook and then reading it back could return 404 — the write stayed on whichever instance served it, and the next request often landed elsewhere (measured: 60 reads of a just-created geofence answered 200 31 times and 404 29 times). Writes are now stored per key with a 24-hour expiry, so create → list → update → delete behaves as it does on a live key, and yesterday's experiments do not pile up. PUT /v1/devices/{psn} also persists in the sandbox now (it used to answer 200 with the new name and keep the old one everywhere else) and applies the live path's validation: empty body, Wi-Fi-only PSN, vehicle.year range, and vehicle.tag being read-only.

  • CHANGED sim is now scoped to your organization and picked deterministically. sim_info can hold more than one row for the same dashcam — two upstream writers use different upsert keys, so re-registering a dashcam under another account inserts a second row. The API used to look the SIM up by serial number alone, which meant the list and the detail view could pick different rows (reported 2026-09-09: GET /v1/devices returned {apn: null, iccid: null} while GET /v1/devices/{psn} returned a SIM that had since moved to another dashcam), and a previous owner's APN and masked ICCID could be returned to the current one. Both views now filter by the owning account and take the most recently updated row, so they always agree and only ever show your own data. If a dashcam's SIM record still carries a previous owner, sim now reads null instead of that owner's values — the record is repaired the next time the dashcam reports its SIM. vehicle was never affected: both views resolve the same reference stored on the dashcam.

  • ADD vehicle, sim and battery on the device list — the list and the detail view now return the same fields. GET /v1/devices used to omit them, so building a fleet table that shows a licence plate meant one GET /v1/devices/{psn} per row: 101 calls to refresh a 100-dashcam view, which on a Standard plan with 10 cameras exceeded the monthly call limit at a five-minute refresh. The Fleeta web viewer has always shown plate, VIN and vehicle tag as columns of its camera list, fetched in one call — the API now does the same. Every row carries vehicle (vin / plate / maker / model / year / tag, {} when no vehicle is assigned), sim, battery and requiredCameraLimit, joined once per page rather than once per row. vehicle.tag is new on both views — the free-text label the web viewer groups vehicles by; it is read-only, PUT /v1/devices/{psn} does not accept it. Nothing was removed and no field changed type, so existing clients are unaffected. DeviceDetail remains as an alias of Device in the spec and the SDK. q still searches the dashcam name and PSN only — match a plate on the rows this endpoint returns. In MCP, list_devices carries the vehicle on every row, so the assistant no longer calls get_device per candidate to resolve a plate.

  • CHANGED Wi-Fi-registered dashcams answer 422 cloud_only instead of an empty result. A dashcam can be registered as Wi-Fi-only while its record still lives in the cloud device collection. Cloud-only endpoints used to let those PSNs through and then return nothing, so GET /v1/devices/{psn}/trips on such a dashcam answered 200 with an empty list while GET /v1/devices described it as category: wifi — the reference said one thing and the API did another. Trips, events filtered by psn, geofence alerts and GPS export now answer 422 cloud_only for them, the same as dashcams registered through the Wi-Fi-only path.

  • CHANGED GET /v1/devices/{psn}/trips rejects a reversed range. from later than to used to return 200 with an empty list, which reads as "no trips in that period". It now answers 400 invalid_parameter, matching GET /v1/events, the event feed, GET /v1/safety-events/stats and the driving reports. (POST /v1/gps/export-jobs keeps 422 invalid_field because there the range is a request-body field, not a query parameter.)

  • ADD Camera seats — camera_limit_exceeded, entitlement, cameraPosition, the cameras block and the entitlement filter. A subscription covers a fixed number of cameras. An organization can end up with more cloud dashcams registered than that — normally only after a plan is downgraded or lapses, since neither the API nor the mobile app registers past the count — and then seats go to the most recently registered dashcams while the rest are over-limit, the dashcams the Fleeta web viewer shows greyed out. The API now says so instead of behaving as if the seat existed. Every dashcam row (GET /v1/devices, the detail, GET /v1/fleet/locations and its feed, events, geofence alerts, driving reports, the topDevices / disconnectedDevices lists) carries entitlement: covered | over_limit; the device list and detail add cameraPosition (1 = most recently registered) and an over-limit detail adds requiredCameraLimit; and every organization-wide response — GET /v1/devices, GET /v1/fleet/locations, …/feed, GET /v1/fleet/summary, GET /v1/reports/driving, GET /v1/safety-events/stats, GET /v1/usage — returns a cameras block beside data (limit · registered · overLimit; /v1/usage adds overLimitPsns). An over-limit dashcam is readable, not hidden: location, trips, events, thumbnails, statistics, geofences and geofence alerts, GPS export, settings snapshot, battery and firmware all work as before. Only what pulls new media off the dashcam or moves it answers the new 403 camera_limit_exceededGET /v1/events/{eventId}/video (before any quota is spent), the four SD-card routes (GET …/sd-files, …/{filename}/metadata, …/{filename}/video, DELETE …/{filename}), POST /v1/media/recall-jobs and POST /v1/devices/{psn}/reboot — and so does POST /v1/devices when the batch would exceed the count (all-or-nothing, with remainingSeats). The problem body carries cameraLimit, registeredCameras and requiredCameraLimit, plus psn + cameraPosition on single-device routes or psns on registration. Nothing needs re-configuring: raise the camera count in the Fleeta web viewer (Account › Subscription) and the same key picks it up within 5 minutes — no reissue. ?entitlement=covered|over_limit|all narrows any of those lists (default all on devices, locations, events and statistics). Sandbox test keys never lock a seat — every demo dashcam is covered and cameras.limit equals registered. @fleeta/sdk lists the code in CODES and adds the fields to Device; the MCP tools carry the same fields and explain the 403 (see the AI docs). Full rules in Rate Limits › Camera seats; the code in Errors › camera_limit_exceeded.

  • ADD registeredAt, lastLoginAt and connectivity on devices. GET /v1/devices and GET /v1/devices/{psn} now carry when the dashcam was registered to the organization (registeredAt — the order camera seats are assigned in, newest first), its last cloud sign-in (lastLoginAt; lastConnectedAt is the last activity and falls back to this value when none is recorded), and how it reaches the network (connectivitylte for a built-in cellular module, wifi otherwise; separate from category, which says whether the dashcam talks to the cloud at all). Each is null when the record does not say, and all three are null in the sandbox.

  • CHANGED GET /v1/fleet/summary and GET /v1/reports/driving count covered dashcams by default. Both now default to entitlement=covered, so their numbers are the ones the Fleeta web viewer shows — the viewer's driving report and online count have always left over-limit dashcams out. An organization with over-limit dashcams therefore sees smaller totals and fewer report rows than before; pass entitlement=all for the previous figures (or over_limit for the locked ones alone). The cameras block on both responses always describes the whole organization regardless of the filter, and GET /v1/reports/driving/{psn} is unchanged — an over-limit dashcam still answers 200, with its entitlement. No other default changes.

  • CHANGED A dashcam registered over Wi-Fi reads category: wifi, not cloud. A dashcam whose cloud record was created through the Wi-Fi registration path (reg_category: wifi) used to be reported as category: cloud, indistinguishable from a cloud-connected unit although it has no cloud connectivity. It now reads category: wifi, so category=cloud leaves it out and every cloud-only feature — location, trips, events, video, SD-card access, recall, settings, reboot, GPS export — answers 422 cloud_only for its PSN, exactly as for a dashcam from the Wi-Fi-only inventory. Unlike those, it still holds a camera seat and a cameraPosition.

  • CHANGED POST /v1/devices documents every failed[].code. The per-row reason enum used to list only invalid_psn and duplicated, while the registration backend can reject a row for reasons the reference did not name. unknown_psn (not a dashcam in the product database), blacklisted, backend_error (rejected for another reason) and camera_limit_exceeded are now declared and described. The last is the per-row form of the new seat check: a batch that would exceed the camera count is refused up front as 403 camera_limit_exceeded before anything is registered, so a row-level camera_limit_exceeded appears only when the count changed within the last few minutes. Accepted rows behave as before.

  • CHANGED Audit log outcomes use the API's lower-case vocabulary, and the filter accepts all of them. GET /v1/audit-logs declared status as SUCCESS | FAIL while passing the source value straight through, so entries recorded by invite, password-reset, firmware and GPS-tracking actions carried values the schema did not allow. Both the response field and the status filter now use success | fail | request | accept | delete | start | enable | disable, in line with every other enum in the API.

  • ADD fwVersion on the device list. GET /v1/devices now carries the firmware version each dashcam last reported, so an outdated-firmware sweep no longer needs one detail call per device.

  • CHANGED validation_error is gone — a malformed groupId answers invalid_parameter. Query-parameter format, range and enum failures all use one code now; validation_error was only ever raised for groupId and start, which meant clients had to branch on two codes for the same class of error.

  • ADD Firmware updates are actually reported. GET /v1/devices/{psn}/firmware returned latest: null and updateAvailable: false for every dashcam because the model-to-latest catalogue was never configured. It is wired now, and the model lookup no longer matches a shorter name by accident (DR770X BOX PRO used to be able to pick up DR770X's version). Models the catalogue does not list still answer latest: null.

  • CHANGED recordingEvents is merged key by key on update. PUT /v1/geofences/{geofenceId} used to replace the whole object, so flipping one switch meant resending every block. Sending recordingEvents: { enter: { sdCard: true } } now keeps enter.liveUpload and the entire exit block as stored — the same rule style already followed. A devices list still replaces wholesale.

  • CHANGED Every 404 names the code it returns, and 400 lists only the causes that apply to that endpoint. The reference used to print phrases like "Not found" while the body carried event_not_found, and endpoints with no cursor and no body still advertised invalid_cursor and invalid_body. Both are generated from what each operation actually accepts now.

  • CHANGED Geofence shapes that cannot be stored are rejected instead of quietly trimmed. A Polygon with interior rings (holes), a rectangle sent as a multi-ring geometry, and a polyline sent as several rings used to be accepted with the extra rings silently dropped. They now answer 422 invalid_field naming the reason. Empty coordinates are reported as a point-count problem rather than a format problem.

  • CHANGED Multi-ring geofences read back and round-trip correctly. A polygon made of several rings came back with empty coordinates — polygons: [[], []] — so sending a GET response straight back answered 422. The read view now unwraps the multi-ring form properly, and rectangle no longer accepts a multi-ring geometry at all (it needs one ring of exactly 4 corners).

  • CHANGED Geofence shapes are validated the same way in the sandbox and with a live key. A shape with empty or too-few coordinates used to be accepted by a sandbox key (201) and rejected by a live one (422). The point-count and point-format rules now run before the store is touched, so both answer 422 invalid_field with the same message. Read-form points ({latitude, longitude}) are also accepted inside shape.geometry.coordinates, not only in shape.coordinates.

  • ADD Dashcams that cannot do SD-card commands are named, not timed out. Older models — DR750S-2CH, DR900S-2CH, DR590X-2CH Plus and the rest of that generation — have no cloud SD-card command channel, so GET /v1/devices/{psn}/sd-files, the per-recording metadata / video / delete routes and POST /v1/media/recall-jobs used to wait out the device timeout and answer 504. They now answer 422 unsupported_device immediately, carrying psn and model. Everything that does not touch the SD card — the device list and detail, events, trips, GPS export — is unaffected on those dashcams.

  • CHANGED Recalling a recording that is not on the card answers 404, and no longer spends a recall. POST /v1/media/recall-jobs and GET /v1/devices/{psn}/sd-files/{filename}/video used to answer 422 invalid_field for a well-formed filename the card does not hold — after issuing a download token, which counts against the plan's recall allowance. The pre-check now confirms the file exists before the token is issued and answers 404 file_not_found, matching what the reference always documented.

  • CHANGED include on SD-card metadata is a query parameter, so it answers 400. GET /v1/devices/{psn}/sd-files/{filename}/metadata rejected an unsupported include with 422 invalid_field; every other query parameter in the API answers 400 invalid_parameter with the accepted values in allowed, and this one now does too. The invalid extension field it used is gone — read allowed instead.

  • CHANGED SD-card recording types were mislabelled, and most of them were missing. GET /v1/devices/{psn}/sd-files decoded the filename's category letter with a table of its own that held only seven letters. Twelve categories — harsh braking, overspeed, the driver-monitoring types (drowsy, distracted, undetected, calling, smoking, seatbelt) and the four geofence types — were not in it at all, so those clips came back with type: null and disappeared from every type filter while still being counted in the unfiltered total (a real card: 651 files, 624 across all type filters — the 27 missing ones were all hard-braking clips). Worse, two letters carried wrong names: automatic was harsh acceleration and timelapse was a sharp turn. The endpoint now reads the same table GET /v1/events reads, so a clip's type is the same word in both places: automaticharsh_acceleration, timelapsesharp_turn, eventdriving_impact and impactparking_impact (all four old values are gone and are refused as filters, with the accepted values in allowed, rather than answering an empty list). automatic and timelapse were plainly wrong names; 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 it driving_impact — so one file needed two vocabularies and a translation table to move between them. There is now one word per recording across the whole API. Alongside that, harsh_braking, overspeed, drowsy, distracted, undetected, calling, smoking, seatbelt, geofence_enter, geofence_exit, geofence_pass and geofence_speed are now returned and filterable. normal, parking and manual keep their names. Of those, normal and parking are the only two recordings that are not safety events — a manual recording (the driver pressed record) is published as an event as well, and GET /v1/events has used that same word for it since 2026-09-01. A category letter outside the table is now reported as unknown instead of null — silence was what hid this — and the raw letters are exposed as the new typeCode / directionCode fields. direction gains option (the 7-BOX option camera), which used to be null and so vanished from direction filters the same way. type, direction and stream values outside their enum now answer 400 invalid_parameter instead of an empty list. @fleeta/sdk RecordingType / RecordingDirection and the MCP list_sd_recordings tool follow the same vocabulary.

  • CHANGED SD-card metadata for a file that is not on the card answers 404. GET /v1/devices/{psn}/sd-files/{filename}/metadata used to return 200 for any well-formed filename, with recordedAtLocal, type, direction and stream synthesized from the filename you sent and the rest null — a file that did not exist read as "it exists, it just has no thumbnail, GPS or sensor data". The dashcam does not report a missing file as an error (it answers success with an empty payload), so the API now treats an answer that carries no recording data at all as 404 file_not_found, the response the specification and the sandbox already described. A clip that exists but genuinely has no samples for the part you asked for still answers 200 with that part null. This makes the sandbox and production behave the same way, so error handling built against a test key now works with a live one.

  • CHANGED GET /v1/devices?status= rejects values outside the enum. An unrecognized status was silently ignored and the whole organization came back with 200 — including status=online, the most natural way to ask for connected vehicles, which returned every device rather than none. The accepted values are unchanged (driving, parked, offline; driving and parked are both connected now, so add the two for "online"), and anything else answers 400 invalid_parameter with them in allowed, exactly as the sibling category parameter already did.

  • ADD Geofence map colour and opacity. Every geofence now carries stylecolor (#RRGGBB) and opacity (0–1) — on GET /v1/geofences, GET /v1/geofences/{geofenceId} and the create/update responses. These are the values the Fleeta web viewer and the mobile apps actually draw the zone with, so a map you build yourself can match them; both are null when the stored zone carries no value (the viewer then falls back to a palette colour chosen by list position, which this API will not guess). POST /v1/geofences and PUT /v1/geofences/{geofenceId} accept the same object: each property is applied on its own, so style: { "color": "#F21212" } on an update keeps the stored opacity, and an omitted property takes the web viewer default (#6B2FD9, 0.24) on create. Previously a geofence created through the API was always drawn in the default purple with no way to change it from the API. @fleeta/sdk adds GeofenceStyle / GeofenceStyleInput, and the MCP create_geofence tool takes optional color and opacity. An invalid value answers 422 invalid_field.

  • ADD Event thumbnails through MCP. The list_events tool takes includeThumbnails: true and puts a thumbnailUrl on the rows it returns — the same presigned still image GET /v1/events/{eventId}/thumbnail issues, valid 5 minutes, costing no extra requests and no quota. Because each presigned link is about 1.6 KB of text, the tool attaches images to the first 25 events of a call and says so in summary.thumbnails; the REST parameter include=thumbnail is uncapped and stays the right choice when you are assembling the gallery yourself. There is still no per-event thumbnail tool; assistants used to have no way to show event images at all. The AI docs gain a Not available through MCP section listing what genuinely has no tool.

  • CHANGED The error schema no longer shows a phantom propertyName field. Problem (and every 4xx/5xx that extends it) allows RFC 9457 extension members, which the API reference rendered as a row literally named propertyName — readers took it for a field of ours. It is now labelled extension and described. Five schema keys across three schemas, which a YAML quoting mistake had turned into nameless properties (one in ProblemInvalidField.allowed, two in ProblemInvalidParameter.allowed, two in ProblemExportInProgress), are gone as well, and the allowed description — which said when the field is present — is no longer truncated. Spec and documentation only; responses are unchanged.

  • ADD Geofence shapes accept the read form. POST /v1/geofences and PUT /v1/geofences/{geofenceId} now take the geometry exactly as GET returns it — coordinates as {latitude, longitude} points (closing point optional; polygons for a multi-polygon) and center + radiusM for a circle — alongside the original write form (GeoJSON geometry / circle). A geofence you fetched can be edited and sent back unchanged; the 422 invalid_field message names both forms when neither is present. The Try-it panel also gained an Example ▾ picker with one body per shape (circle, polygon, rectangle, polyline) and, for the update endpoint, rename / colour / resize / replace-geometry presets — changing type alone in the circle example used to leave the circle's centre in the body and answer 422.

  • CHANGED Webhook test deliveries now appear in delivery history. POST /v1/webhooks/{webhookId}/test used to leave no trace, so a subscription that had only been tested showed an empty GET /v1/webhooks/{webhookId}/deliveries — which read as a fault when the receiver had clearly been called. Each test call is now recorded as one delivery flagged test: true (single attempt, status code and network error included), and WebhookDelivery gained the test boolean (false on real event deliveries and on records written before this change). Test records still never count towards automatic suspension — that rule is unchanged, see Webhooks › Automatic suspension.

  • ADD Server-judged speed-limit events. GET /v1/events, GET /v1/events/feed, GET /v1/events/{eventId} and GET /v1/safety-events/stats now include road speed-limit violations that Fleeta judges from each vehicle's GPS track and map speed-limit data — the same events the web viewer's safety-event screen shows — by default, alongside uploaded recordings. They carry the new type value speed_limit (distinct from overspeed, the dashcam's own fixed-threshold alert with a clip), a real location (position at peak speed) and speedKmh (peak speed), and a new speedLimit object — limitKmh, overKmh, durationSec, roadName, country, endedAt, peakAt, schoolZone — that is null on every other event. They have no clip: channels is empty, hasVideo is false, and the video / thumbnail endpoints answer 404 video_not_available as for any event without one. type=speed_limit selects them alone; a type list without it leaves them out. Statistics merge them into kpi.totalEvents, kpi.activeDevices, byType (a speed_limit row), byHour and topDevices, so counts grow for organizations with speeding vehicles. Existing cursors stay valid. @fleeta/sdk adds 'speed_limit' to EventType, SafetyEvent.speedLimit and the SpeedLimitDetail type; the MCP list_events and safety_event_stats tools describe the new type. See Safety events › Server-judged speed-limit events.

  • CHANGED Webhook deliveries no longer follow redirects. A 3xx answer from your endpoint now counts as a failed delivery (it used to be followed). Point the subscription at the final URL.

  • CHANGED Normal and parking recordings are not events. GET /v1/events, GET /v1/events/feed and GET /v1/safety-events/stats exclude routine driving (N) and parking (P) recordings — they are not safety events, so counts drop for organizations whose dashcams upload them.

  • CHANGED Two event populations, named. GET /v1/reports/driving and GET /v1/fleet/summary count detections reported by the dashcam in its 1-minute telemetry (11 counter types — manual, the geofence_* types and seatbelt never appear there), whether or not a clip was uploaded. GET /v1/events and GET /v1/safety-events/stats list recordings uploaded to the cloud — a clip the dashcam sent because the event matched the device's event-upload settings (all 17 types, with video). The uploaded recordings are a subset of the detections, so byType differs between the two families by design. No schema or behaviour change — descriptions only, plus the new Safety events guide; each MCP tool description now says which population it reads.

  • CHANGED GPS Export splits per device by default. Omitting packaging on POST /v1/gps/export-jobs used to mean single — every device and every day merged into one export.csv inside the zip. A CSV holds one row per GPS point and has no row cap, so a 70-vehicle, 90-day export ran to millions of rows and would not open in a spreadsheet (Excel stops at 1,048,576 rows and cuts the surplus off without saying so). The default is now the same rule the web console has always used: perDevice when the job targets more than one device, single for a single device. An explicit packaging is honoured exactly as before, and the accepted values are unchanged. The GPS Export guide now carries the row-count arithmetic.

  • ADD Speed in the exported files. GPS Export never carried speed, even though the same GPS records power speedKmh on GET /v1/fleet/locations and GET /v1/trips/{tripId}/track — customers had to call the track endpoint in bulk to fill the gap. CSV now ends with a speedKmh column (km/h, one decimal; empty when the device reported no speed — 0 means stationary and is a real value), appended after the existing columns so parsers that read by position are unaffected. GPX carries the same value in the standard Garmin extension <gpxtpx:TrackPointExtension><gpxtpx:speed>, whose unit is metres per second; points without a reported speed get no <extensions> block. KML has no standard place for a per-point scalar and therefore carries no speed. The archive's full field list — columns, units and the UTC basis — is now documented in GPS Export › What is inside the archive.

  • CHANGED GPS Export ranges are UTC calendar days. POST /v1/gps/export-jobs reduces from / to to the UTC calendar date of each instant and exports whole UTC days — the time of day is ignored — and the timestamps inside the exported files are UTC. GpsExportJob.from / to echo the job's own calendar basis (midnight UTC for jobs created through the API; jobs created in the web viewer keep the console's local calendar day). Previously the export backend applied a Korea-time calendar day to the same dates, so the exported window could be shifted by nine hours against the request. Every other timestamp in the API was already UTC; Getting started now states it once for all.

  • ADD 404 group_not_found for a groupId that does not exist. A well-formed groupId that no longer exists in your organization — a typo, or a group someone deleted — used to answer 200 with an empty list, so a mistake looked exactly like "this group has no vehicles" and a GET /v1/fleet/locations/feed poller kept receiving zero events with a fresh nextCursor forever. The nine operations that accept groupId (GET /v1/fleet/locations, …/feed, /v1/devices, /v1/events, /v1/events/feed, /v1/safety-events/stats, /v1/fleet/summary, /v1/reports/driving, /v1/geofence-alerts) now answer 404 group_not_found, completing the three-way split with 400 validation_error (wrong format) and 403 group_not_allowed (outside the key's pinned groups). A group that exists but has no vehicles still answers 200 with an empty list, and nothing about other organizations is revealed — a group belonging to someone else is indistinguishable from one that never existed. @fleeta/sdk lists the code in CODES; see Tenant Isolation.

  • CHANGED 400 is declared on every operation that can raise it. The shared router validates query parameters and request bodies (page size ranges, dates, cursors, groupId format, JSON shape) before an operation runs, so any operation that takes a query parameter or a body can answer 400 — but only 10 of them said so in the OpenAPI spec. All 22 remaining operations now declare it, referencing a shared BadRequest response that covers invalid_parameter, invalid_cursor, validation_error and invalid_body. The page-size parameters (limit, perPage) also declare minimum: 1, which the API always enforced — limit=0 was legal per the spec and rejected in practice. Generated clients and contract tests stop treating a 400 as an unexpected 5xx. Behaviour is unchanged.

  • CHANGED Common error responses on every operation. All 52 operations now declare 401 unauthorized, 403 (insufficient_scope, subscription_inactive), 429 (monthly_limit_exceeded, rate_limited, the API-edge quota_exceeded) and 500 internal_error in the OpenAPI spec — they could always occur, since the shared router raises them, but only a handful of operations listed them. Three operation-specific responses were missing as well and are now declared: 400 invalid_parameter on GET /v1/reports/driving, 404 device_not_found on GET /v1/devices/{psn}/battery and …/firmware, and 409 geofence_unsupported_shape on PUT /v1/geofences/{geofenceId} — the errors guide now documents 50 codes and @fleeta/sdk 0.7.0 lists it in CODES. ProblemQuotaExceeded.bucket / limit are no longer marked required (the API-edge 429 variant carries neither). Behaviour is unchanged.

  • CHANGED Webhook suspension and retries. A subscription is now suspended only when both hold: at least 10 consecutive events failed (each event counts once, on its first delivery burst) and the failures have lasted 15 minutes since the first one — a short outage can no longer suspend you (previously the counter was per burst, so ten failed bursts within seconds were enough). Events that arrive while a subscription is suspended are no longer dropped: they are parked in the 14-day dead-letter store, where Fleeta operations can replay them once you have reactivated. A receiver 408 or 429 is no longer retried inside the burst; the event is returned to the queue and redelivered, honouring a Retry-After longer than 60 s (up to 300 s). Suspension writes a webhook.suspended entry to your audit log. X-Fleeta-Event-Key is now derived from the event's own ID, so it also survives a dead-letter replay. Signatures and secrets are unchanged. See Webhooks › Automatic suspension.

  • CHANGED Sandbox webhooks are per organization. The seeded subscriptions wh_demo0001 / wh_demo0002 are copied for each sandbox key's organization on first access, so a DELETE, rotate-secret or reactivate from one key no longer changes what another key sees.

  • ADD API-key actions in the audit log. Changes made with an API key — geofence and webhook changes, device renames and pre-registration, reboots, GPS exports, recording recalls, SD-card deletions, video URL issuance — are written to your organization's audit trail, the same timeline administrators see in the web viewer's Audit Log screen. Entries returned by GET /v1/audit-logs carry actorType (api_key; system for an action the platform took on its own, such as a webhook suspension; null for actions people took) and actorId (the key ID key_…); a category outside the documented enum is returned as null. Writing is best-effort — an unreachable audit pipeline never fails the API call. Sandbox keys are not recorded. See API logs › API-key actions in the audit log.

  • CHANGED Driving report dates. GET /v1/reports/driving accepts from / to as YYYY-MM-DD or an RFC 3339 date-time with offset (the UTC calendar date of the instant is used) — the same form the event endpoints take. An offset-less date-time or a date that does not exist answers 400 invalid_parameter. GET /v1/reports/driving/{psn} now answers 400 invalid_parameter when from / to are passed (it takes date only); previously they were silently ignored and yesterday's report was returned.

  • CHANGED MCP play_sd_recording is sub-stream only. The tool's quality input was removed; it always requests the low-resolution sub-stream. Full-resolution files go through recall_sd_recording, which asks for approval first.

  • CHANGED One event is now one row. Safety events used to be returned per uploaded file: a single recording that uploads a front clip, a rear clip and their thumbnails produced four to six separate rows, each with its own eventId — and two of those IDs could issue a presigned URL for the very same .mp4. GET /v1/events, GET /v1/events/feed, GET /v1/events/{eventId} and GET /v1/safety-events/stats now group the files of one recording into a single item. eventId is redefined — it identifies the event, not one of its files, and is stable for the life of the event (uploading another channel later never changes it); IDs stored before this release still resolve, but re-read them from GET /v1/events when you can. channels is the union of the event's playable channels — a front + rear event returns ["front","rear"] on one row, and option (7-BOX option camera) was added to the enum. hasVideo now equals channels.length > 0 (a thumbnail-only file used to report hasVideo: true with an empty channels, and every video request for it answered 404). Deleted events are excluded from the list, the feed and the statistics. GET /v1/safety-events/stats counts events, not filestotalEvents, byType, byHour and topDevices drop by roughly 4-6x; this is a correction, not a reduction, and activeDevices is unchanged. Pagination: a page may hold fewer items than limit, or none, while hasMore is true — always follow nextCursor and stop on hasMore: false. Cursors issued before this release stay valid. Feed: an event is delivered once, when its first file arrives; channels uploaded seconds later are not re-delivered, so re-read the event when you need the full channels.

  • CHANGED Event types were mislabelled. type was derived from a numeric table that one of the writers fills from a stale mapping: manual (emergency) recordings were reported as distracted, and undetected, calling, seatbelt and geofence_speed events came back as unknown. The type is now read from the recording's own event code — the same source the BlackVue apps use. An event that used to arrive as distracted may now arrive as manual. type= filters for manual, undetected, calling, seatbelt and geofence_speed, which always returned an empty list, now work.

  • ADD include=thumbnail on the event list and feed — each row gains thumbnailUrl, the same presigned URL GET /v1/events/{eventId}/thumbnail would issue, valid for 5 minutes. It is built from data the row already loaded, so a page of 50 events costs no extra requests and no quota. GET /v1/events/{eventId}/thumbnail also accepts an optional channel (defaults to the first entry of channels), so a rear-only event no longer answers with a front thumbnail.

  • CHANGED occurredAt is documented as the upload time of the event's first file — it can trail the moment of the incident by the device's upload delay (usually under a minute; 1–36 minutes measured when the vehicle was out of coverage). Behaviour is unchanged; only the documentation was wrong.

  • ADD Webhook reactivation and secret rotationPOST /v1/webhooks/{webhookId}/reactivate resumes a subscription the dispatcher suspended (same URL, events and secret; 409 webhook_not_suspended otherwise) — no more delete-and-re-register. POST /v1/webhooks/{webhookId}/rotate-secret issues a new signing secret, returned once, while the previous one keeps co-signing for graceSeconds (default 24 h): X-Fleeta-Signature: v1=<new>,v1=<previous> — accept any matching part. Subscriptions that never rotate keep the single-signature header. Webhook gains suspendedAt and previousSecretExpiresAt. @fleeta/sdk 0.6.0: webhooks.reactivate() / webhooks.rotateSecret().

  • ADD Stable webhook event key — every delivery carries X-Fleeta-Event-Key (also eventKey in the body and in GET …/deliveries). Unlike deliveryId, which is new for every burst, it does not change when the event is redelivered from the queue — dedupe on it.

  • CHANGED Webhook retries — a receiver 4xx is final at both levels: the burst stops and the event is no longer returned to the delivery queue (it used to be redelivered up to 4 more times). The queue schedule is now documented: about 60 s between bursts, at most 5 bursts per event, then a 14-day dead-letter store. See Webhooks › Retries.

  • ADD Typed error extension fields — every error code that carries extension fields now has its own Problem<Code> schema in the OpenAPI spec (ProblemCloudOnly, ProblemQuotaExceeded, ProblemInvalidField, ProblemMonthlyLimitExceeded, … 17 in total, listed under Models), referenced from the responses that produce them. Generated clients see psns, bucket, requiredScope, resetsAt, … as typed fields instead of reading them off the examples. @fleeta/sdk 0.6.0 adds error.is(code) / isKnown() on top of these types.

  • CHANGED Internal diagnostics removed from error bodies502 delegate_failed, 422 invalid_field (device command/settings), 403 recall_limit_exceeded and 504 command_timeout no longer include undocumented upstream values (upstreamStatus, statusCode, backendStatus, resultcode, message, waitedMs). They were never part of the contract; quote requestId and we match it to our logs.

  • CHANGED GPS Export range over 90 days now answers 422 invalid_fieldPOST /v1/gps/export-jobs validates the fromto span (UTC calendar days, to at most 90 days after from) at the API edge and returns invalid_field with maxRangeDays: 90 and rangeDays, before any export_job quota is charged. Previously the export backend rejected it and the API surfaced a retryable-looking 502 delegate_failed; sandbox keys accepted any span. No successful request changes.

  • CHANGED Scopes follow the subscription at request time — after a plan change a key's scopes are recomputed from the current tier on the next authorization (≤ 5 minutes of gateway cache), instead of waiting for the usage-plan move to be stamped on the key. Explicit-scope keys remain capped by their stored list. Fixes the window where usage showed the new tier while audit:read was still refused.

  • ADD Retry-After and X-Request-Id declared as response headers in the OpenAPI spec (429, 409 device_busy and the shared error responses), and exposed to browsers on edge responses via Access-Control-Expose-Headers. Behaviour is unchanged — the headers were already sent; generated clients and the Try-it panel can now see them.

  • ADD Playable link for an SD-card recording in one callGET /v1/devices/{psn}/sd-files/{filename}/video?quality=main|sub (default sub, scope media:recall) returns { url, expiresAt, jobId, sizeBytes, receivedBytes, quality }; the recall job behind it is created exactly as with POST /v1/media/recall-jobs and url equals that job's downloadUrl. Although a GET, it uploads over the vehicle's LTE and counts toward the recall allowance. Asking for the same file (same quality) again within 48 hours reuses the media server's copy when it is complete — the API now resends the size the device reported, so no second upload is needed. A filename ending in S.mp4 is rejected with 422 invalid_field on both routes (select the sub-stream with quality), and RecallJob.sizeBytes is documented as the size of the requested stream.

  • ADD Eight domains, 52 operations — devices, telemetry (live locations, trips, GPS tracks, GPS Export), safety events (18 event types, video and thumbnails), SD-card media (list, metadata, recall, delete), geofences and alerts, insights (fleet summary, driving reports), webhooks, and account (usage, API logs, audit logs). See the API Reference.

  • ADD Bearer API keys that follow your subscription — issue keys from Management › Open API in the web viewer; the plan tier, camera limit and scopes are read from your Fleeta subscription on every request, so a plan change never requires a new key. Scoped keys and per-organization tenant isolation throughout.

  • ADD Public sandbox keys — five test keys (one per plan) return a 12-vehicle demo fleet from the real endpoints, so you can build without a dashcam. See the Sandbox guide.

  • ADD Fleeta MCP — a remote MCP server at mcp.fleeta.io with 28 tools; connect ChatGPT, Claude, Cursor or any MCP client with a Bearer key or OAuth 2.1. Write actions sit behind a human approval step. See AI Assistants (MCP).

  • ADD Webhooks — HMAC-signed push for safety events, device connectivity, geofence alerts and GPS Export completion, with delivery history and a test endpoint. See Webhooks.

  • ADD Official SDK@fleeta/sdk for JavaScript/TypeScript with typed responses, automatic pagination and OpenApiError mapping. See SDKs.

  • ADD Uniform response standard — camelCase keys, RFC 3339 UTC timestamps, metric units, hybrid cursor/offset pagination, and RFC 9457 problem+json errors with a stable code on every failure, including 422 cloud_only for Wi-Fi-only dashcams.

  • ADD Device writesPUT /v1/devices/{psn} renames a dashcam and updates its vehicle profile, POST /v1/devices pre-registers dashcams (pending until they first connect), through the same backend the web viewer uses.

  • ADD Geofence writesPOST / PUT / DELETE /v1/geofences create, edit and remove geofences through the same backend the web viewer uses, and read the persisted result back. The MCP create_geofence / delete_geofence tools run through the approval gate.

  • ADD Plan-based limits — rate limits, monthly call allowances and volume quotas derived from your plan and camera count; the API itself is included in the subscription. See Plans & Limits.

Upcoming

  • SOON Dashcam Wi-Fi API — the overview is up; the endpoint reference and guides follow.
  • SOON Python SDK · Postman collection.