Event Schema
Event Schema
The event schema is the contract between the badge firmware and the cloud ingestion
layer. All events written to the cloud — regardless of type — share a common envelope.
Type-specific fields are in the payload object.
This schema is founder-owned. The firmware engineer implements to it. The backend developer validates against it. Neither party changes it unilaterally.
Envelope (All Event Types)
{
"device_id": "string",
"firmware_version": "string",
"event_type": "string",
"timestamp_utc": "string (ISO 8601)",
"sequence_number": "integer",
"payload": "object"
}
Field Definitions
| Field | Type | Required | Description |
|---|---|---|---|
device_id |
string | always | Badge serial number, assigned at provisioning. Immutable. Format: ACT-XXXXXXXX (8 hex chars). |
firmware_version |
string | always | Semantic version of the firmware that generated this event. Format: major.minor.patch (e.g. 0.3.1). |
event_type |
string | always | One of the values in the Event Types table below. |
timestamp_utc |
string | always | ISO 8601 UTC timestamp at event origin. Not at upload time — at the moment the event was generated on the badge. Format: 2026-03-14T08:32:15Z. |
sequence_number |
integer | always | Monotonically increasing integer, per device, starting at 1. Never resets across reboots (persisted to flash). Used for deduplication: (device_id, sequence_number) is globally unique per device. |
payload |
object | always | Event-type-specific fields. May be an empty object {} for types with no payload. |
Event Types
event_type |
Description | Trigger |
|---|---|---|
shift_start |
Worker assigned to badge for a shift | Foreman input on badge at start of workday |
shift_end |
Worker unassigned from badge | Manual input or auto-detect: extended stationary + time threshold |
gps |
Badge GPS position sample | Periodic: every 15 seconds (configurable) when GPS fix available |
gps_no_fix |
GPS sample attempted but no fix obtained | Periodic: GPS polled but fix not acquired within timeout |
ble_proximity |
Equipment tag detected within RSSI threshold | BLE scanner detects a known tag MAC above threshold |
ble_lost |
Previously detected equipment tag no longer detected | Tag RSSI drops below threshold for N consecutive scans |
motion |
Accelerometer motion state change | Moving → stationary or stationary → moving |
upload_heartbeat |
Badge is alive and connected; no other events in this batch | Sent if no events generated in the last upload window |
low_battery |
Battery level below threshold | On detection; once per threshold crossing (10%, 5%) |
reboot |
Device restarted | On power-on or watchdog reset; captures reboot reason |
Payload Definitions by Event Type
shift_start
{
"worker_id": "string",
"assigned_by": "string (optional)"
}
| Field | Type | Required | Description |
|---|---|---|---|
worker_id |
string | yes | Operator-assigned worker identifier. Format is customer-defined; Actual does not enforce a format. Example: W-042, jsmith. |
assigned_by |
string | no | If a foreman assigned the badge (rather than self-assignment), the foreman’s worker ID. |
shift_end
{
"worker_id": "string",
"end_reason": "manual | auto_stationary | low_battery | reboot"
}
gps
{
"lat": "float",
"lon": "float",
"accuracy_m": "float",
"altitude_m": "float (optional)",
"speed_kmh": "float (optional)",
"fix_type": "string"
}
| Field | Type | Required | Description |
|---|---|---|---|
lat |
float | yes | WGS84 decimal degrees. 6 decimal places minimum (~0.1m precision). |
lon |
float | yes | WGS84 decimal degrees. |
accuracy_m |
float | yes | Estimated horizontal accuracy in meters from GPS module. |
altitude_m |
float | no | Altitude in meters above sea level. Not required for geofencing. |
speed_kmh |
float | no | Speed from GPS module if available. |
fix_type |
string | yes | One of: 3d, 2d, dead_reckoning. |
gps_no_fix
{
"timeout_s": "integer",
"last_known_lat": "float (optional)",
"last_known_lon": "float (optional)"
}
last_known_lat / last_known_lon included only if a valid GPS fix was obtained
within the last 5 minutes. Helps the cloud reconstruct likely location during
no-fix gaps.
ble_proximity
{
"tag_mac": "string",
"rssi": "integer",
"detection_count": "integer"
}
| Field | Type | Required | Description |
|---|---|---|---|
tag_mac |
string | yes | Bluetooth MAC address of detected tag. Format: AA:BB:CC:DD:EE:FF (uppercase). |
rssi |
integer | yes | Signal strength in dBm at time of event generation. Negative integer. |
detection_count |
integer | yes | Number of consecutive scans the tag was detected above threshold before this event was generated (for filtering spurious detections). |
Event generation rule: A ble_proximity event is generated when a tag is detected
at or above RSSI threshold for 3 consecutive scans (configurable). A single detection
does not generate an event. This prevents spurious events from passing trucks.
ble_lost
{
"tag_mac": "string",
"last_rssi": "integer",
"duration_s": "integer"
}
duration_s is the number of seconds from the corresponding ble_proximity event
to this ble_lost event — i.e., how long the equipment was detected as present.
motion
{
"previous_state": "moving | stationary",
"new_state": "moving | stationary",
"duration_s": "integer"
}
duration_s is how long the device was in previous_state before this change.
upload_heartbeat
{}
Empty payload. Used by the cloud to confirm the device is alive and uploading even when no field events have occurred (e.g., badge sitting in the truck during an office day, charged and active but not moving).
low_battery
{
"battery_pct": "integer",
"voltage_mv": "integer (optional)"
}
Generated at two thresholds: 10% and 5%. Not generated again at the same threshold unless battery charges back above it and drops again.
reboot
{
"reason": "power_on | watchdog | user_reset | firmware_update",
"previous_uptime_s": "integer",
"events_in_queue": "integer"
}
events_in_queue at reboot time helps diagnose if events were pending upload.
Batch Upload Format
The Blues Notecard uploads events as a JSON array. Each upload is one HTTP POST.
[
{
"device_id": "ACT-3F8A21B4",
"firmware_version": "0.3.1",
"event_type": "shift_start",
"timestamp_utc": "2026-03-14T06:47:00Z",
"sequence_number": 1041,
"payload": {
"worker_id": "W-007"
}
},
{
"device_id": "ACT-3F8A21B4",
"firmware_version": "0.3.1",
"event_type": "gps",
"timestamp_utc": "2026-03-14T06:47:15Z",
"sequence_number": 1042,
"payload": {
"lat": 33.749001,
"lon": -84.387982,
"accuracy_m": 4.2,
"fix_type": "3d"
}
},
{
"device_id": "ACT-3F8A21B4",
"firmware_version": "0.3.1",
"event_type": "ble_proximity",
"timestamp_utc": "2026-03-14T06:47:20Z",
"sequence_number": 1043,
"payload": {
"tag_mac": "F4:A7:2E:11:03:BC",
"rssi": -62,
"detection_count": 4
}
}
]
Batch constraints:
- Maximum events per batch: 500 (enforced by ingest API; Blues Notecard default upload window is 30 min at ~4 events/min = ~120 events typical)
- Minimum: 1 event per upload (heartbeat counts)
- All events in a batch must share the same
device_id; mixed-device batches are rejected
Schema Versioning
The firmware_version field in every event allows the cloud to apply version-specific
validation logic when the schema changes.
Versioning rules:
- Additive changes to
payload(new optional fields) do not require a version bump - Removing or renaming fields requires a firmware version bump and a cloud migration
- The cloud ingest API maintains a compatibility matrix of firmware versions to schema versions
- Events from firmware versions older than the last 3 minor versions may be rejected
(generates a
schema_deprecatedresponse in the API; device must update firmware)