Safety events — two populations
The API reports safety events in two places, and they answer two different questions. Both are correct; they are simply not the same set of things.
GET /v1/events · GET /v1/safety-events/stats | GET /v1/reports/driving · GET /v1/fleet/summary | |
|---|---|---|
| What is counted | Recordings uploaded to the cloud — a clip the dashcam sent because the event matched the device's event-upload settings | Detections counted by the dashcam in its 1-minute telemetry — every event the camera detected, whether or not a clip was uploaded |
| Depends on upload settings | Yes — an event type that is switched off for upload never appears | No |
| Event types | All 17 recording types (driving_impact … manual, geofence_*, seatbelt) plus speed_limit — server-judged road speed-limit violations, see below | 11 DMS counters — manual, the four geofence_* types, seatbelt and speed_limit never appear |
| Has a video | Yes for recordings (hasVideo, channels, GET /v1/events/{eventId}/video); never for speed_limit | No — counts only |
| Answers | "Which events have footage?", "show me the impact at 14:07", "where did Van 07 break the speed limit?" | "How many harsh-braking events did Van 07 have last week?", "riskiest vehicles" |
| MCP tools | list_events, safety_event_stats, get_event_video | driving_report, fleet_summary |
The uploaded recordings are always a subset of the detections: a detection
becomes an event in /v1/events only when the device's event-upload settings
told the dashcam to upload it. Comparing byType across the two families is
therefore not an error check — the report side is expected to be larger.
Dashcams registered beyond the subscription's camera count
(entitlement: over_limit — see Rate Limits › Camera seats)
appear in both families, with different defaults. GET /v1/events, its feed
and GET /v1/safety-events/stats include them (entitlement=all);
GET /v1/reports/driving and GET /v1/fleet/summary leave them out
(entitlement=covered, the web viewer's report numbers) — pass
entitlement=all to count them. Their events, thumbnails and statistics stay
readable; only GET /v1/events/{eventId}/video answers
403 camera_limit_exceeded for such a dashcam.
Server-judged speed-limit events (speed_limit)
GET /v1/events, GET /v1/events/feed, GET /v1/events/{eventId} and
GET /v1/safety-events/stats include a second kind of event by default,
exactly as the Fleeta web viewer's safety-event screen does: a road
speed-limit violation judged by the server. Fleeta evaluates each vehicle's
GPS track against map speed-limit data for the road it was on and records one
event per violation — from the moment the vehicle went over the limit until it
dropped back under it.
- What you get.
typeisspeed_limit.occurredAtis when the vehicle first exceeded the limit (not an upload time),locationis the position at peak speed andspeedKmhthe peak GPS speed. ThespeedLimitobject carries the rest:limitKmh(the posted limit that applied),overKmh(how far above it the vehicle got),durationSec,roadName,country,endedAt,peakAtandschoolZone.schoolZoneistrueonly when a school-zone limit was actually in force at the time — a school-zone road outside its hours reportsfalse. For every other event typespeedLimitisnull. - Why there is no video. The judgement comes from the GPS track, not from
the dashcam, so nothing was recorded:
channelsis empty,hasVideoisfalse,thumbnailUrlisnull, and the video / thumbnail endpoints answer404 video_not_available— the same answer as for any event without a clip. speed_limitvsoverspeed.overspeedis the dashcam's own speed alert: a fixed threshold configured on the device, uploaded as a recording with a clip.speed_limitcompares the vehicle against the legal limit of the road it was actually on, road segment by road segment, and needs no device setting. A single burst of speeding can therefore appear as both — oneoverspeedrecording and onespeed_limitevent — and they keep separateeventIds.- Filtering. Leave
typeout to get both kinds.type=speed_limitreturns the violations alone; anytypelist withoutspeed_limit(for exampletype=harsh_braking,overspeed) leaves them out.from/to,psnandgroupIdapply to both kinds. The statistics endpoint has no type filter and always merges both:kpi.totalEventsadds them,byTypecarries aspeed_limitrow,byHouruses the moment the violation began andtopDevicesranks devices on both kinds together. - Hour buckets and time zones.
byHourcounts by hour of day, in UTC by default. Passtzwith an IANA zone name (tz=Asia/Seoul) and the aggregation groups the buckets in that zone instead, applying daylight saving per event. A UTC offset (+09:00) is rejected with400 invalid_parameterprecisely because it cannot do that — use the zone name. The response echoes the zone ashourTz, so you can label the chart from the payload alone rather than shifting 24 buckets yourself. Nothing else changes:from/toand every timestamp in the response stay RFC 3339 UTC. - Pagination. The two kinds come from different stores and are merged in
occurredAtorder. A page can end early — fewer rows thanlimit, or none — whilehasMoreistrue; follownextCursorand stop onhasMore: false, as for every cursor-paginated list. Cursors issued before this addition stay valid and simply include the violations from the beginning of history. - Feed. In
GET /v1/events/feeda violation is delivered once the server has recorded it, which happens shortly after it ends; its position in the feed isoccurredAt(when it began). As with a recording that is still uploading, a poll that already moved past that moment can skip it.
Which one to read
- Counting — how often something happened, per vehicle or per type:
use the driving reports.
events.totalandevents.byType[]onGET /v1/reports/driving(ranking,from–to) andGET /v1/reports/driving/{psn}(one day, withhourly[].events), pluseventTrendandtopDevicesByEventsonGET /v1/fleet/summary. - Looking at evidence — a clip, a thumbnail, when exactly a recorded event
happened: use
GET /v1/eventsand issue the video URL from there.GET /v1/safety-events/statsaggregates that same population. - Both families use the same
byTypevocabulary (typevalues in lowersnake_case, the same names asEvent.type). The shape is shared on purpose so your code can reuse it; only the population behind the numbers differs. occurredAtis the upload time of the event's first file, not the instant of the incident. It trails the incident by the device's upload delay — usually under a minute, but up to tens of minutes when the vehicle is out of coverage (measured 1–36 min). It is also the sort key ofGET /v1/eventsand the field itsfrom/tofilters apply to.
Geofence alerts (geofence_enter / exit / pass / speed) and manual
recordings (manual) are produced by the cloud or by the driver's button, not
by the dashcam's DMS counters, so they exist only as uploaded recordings and in
GET /v1/geofence-alerts — never in the driving reports.
Date parameters of the report endpoints
The two driving-report endpoints take their period differently, and the server enforces the difference instead of silently ignoring a parameter it does not read.
| Endpoint | Period parameters | Accepted values |
|---|---|---|
GET /v1/reports/driving (fleet ranking) | from, to — inclusive UTC calendar days | YYYY-MM-DD or an RFC 3339 date-time such as 2026-07-01T09:00:00+09:00. A date-time is reduced to the UTC calendar date of that instant; its time of day is not used. Default: to = today (UTC), from = to − 6 days. At most 92 days. |
GET /v1/reports/driving/{psn} (one vehicle, one day) | date only | YYYY-MM-DD (UTC day). Default: yesterday (UTC). from / to are rejected with 400 invalid_parameter — for a range call GET /v1/reports/driving?from&to and read that vehicle's row (one row per device; page with limit/after), or call this endpoint once per date. |
GET /v1/events · GET /v1/safety-events/stats | from, to | RFC 3339 date-times, applied to occurredAt — an upload time, see the note above. |
One client helper that formats new Date().toISOString() therefore works for
every from/to in the API. Mind the calendar-date reduction on the ranking
report: the instant 2026-07-02T08:00:00+09:00 selects the UTC day
2026-07-01, because that moment is 23:00 UTC on the 1st.
Field-level descriptions live in the API Reference under events and insights.