The event object
The same shape is returned by POST /v1/events, GET /v1/events, GET /v1/events/{id},
the snapshot frame and every stream delta. There is one event shape, everywhere.
{
"event_id": "3d5174f7ca2a5e60b7b32e4fa7713174",
"schema_version": 2,
"type": "stopped_vehicle",
"subtype": null,
"lat": 45.764,
"lon": 4.8357,
"geometry": null,
"heading": 180,
"road_ref": "A7",
"severity": "medium",
"description": "Broken-down van on the hard shoulder",
"occurred_at": "2026-08-03T13:15:51.594177Z",
"created_at": "2026-08-03T13:15:51.594177Z",
"expires_at": "2026-08-03T14:15:51.594177Z",
"source_kind": "network",
"feed_id": null,
"license": null,
"upstream_updated_at": null,
"fetched_at": null,
"environment": "live",
"status": "active",
"verification": "unverified",
"confidence": 0.3,
"confirm_count": 0,
"deny_count": 0,
"updated_at": "2026-08-03T13:15:51.594177Z",
"merged_into": null
}Every timestamp on an event is RFC 3339 in UTC, with microsecond precision and a Z
suffix — the stream-ticket response included. Parse with a real RFC 3339 parser rather
than a fixed format string, and do not assume a fixed number of fractional digits.
Identity
| Field | Type | Notes |
|---|---|---|
event_id | string | Opaque. Never parse it. 32 hex characters today; that is not a promise. Use it as a key and nothing else. |
schema_version | int | Which envelope rules produced this row. Currently 2. See Compatibility. |
client_event_id is deliberately not published. Your internal identifiers are not
public data. Correlate a publish with its deltas using the event_id the POST returns.
The observation
Set by the publisher and immutable after write, except on public-feed events, which the upstream feed may revise.
| Field | Type | Notes |
|---|---|---|
type | string | One of /v1/meta/event-types. Treat an unknown value as opaque. |
subtype | string | null | Optional fine detail under type, from /v1/meta/taxonomy. null means the publisher did not distinguish — always allowed, since type alone must stay sufficient to act on. Refine on it, never filter on it: unlike type it grows without notice. |
lat, lon | float | WGS84 reference point, 5 decimals (~1 m). Always present. For a segment event this is the start of the segment. |
geometry | object | null | GeoJSON LineString, or null for a point event. A structured object, not a string — never JSON.parse this field. Coordinates are [lon, lat]. |
heading | int | null | 0–359, 0 = north. Direction of the road users at risk — not of whatever caused the event. null means both directions. Relay does not filter on it; direction filtering is the consumer's. |
road_ref | string | null | Free text as the publisher knows it (A7, N118). Display and filtering only; never parsed for routing. |
severity | string | null | low, medium, high, or null when unknown. Publisher's own assessment. Vocabulary may grow. |
description | string | null | Short free text, ≤ 280 chars. Never required to understand the event. |
geometry may be null on an event that was published with one, if what was stored
cannot be read back. That is deliberate: losing an event's extent is a degraded answer,
losing the event is an outage. lat/lon are always there to fall back to.
Time
| Field | Type | Notes |
|---|---|---|
occurred_at | timestamp | When the publisher observed it. Defaults to created_at. Never use it for ordering. |
created_at | timestamp | Server-trusted publication time. Immutable, and the ordering authority. |
expires_at | timestamp | When Relay stops serving this event. It moves — confirmations push it back. Drop the event yourself when it passes. |
updated_at | timestamp | null | Stamped on every write, creation included. This is the row's version. Reconcile deltas on it. null only on rows written before the rule existed. |
Provenance
| Field | Type | Notes |
|---|---|---|
source_kind | string | network (a participant) or public (an ingested feed). May grow. |
feed_id | string | null | Which public feed, when source_kind is public. e.g. datex2.fr.rrn. null on network events. |
license | string | null | A key of the upstream's licence, for a public row — null on network events. The field you most need: it says whether you may redistribute what we just handed you. |
upstream_updated_at | timestamp | null | When the SOURCE last revised this, for a public row — null on network events. Not the same as updated_at: this is the upstream's own freshness, not ours. |
fetched_at | timestamp | null | When our sweep last saw this row in the upstream feed, for a public row — null on network events. fetched_at minus upstream_updated_at is the source's lag; now minus fetched_at is ours — published so you can verify our latency claim rather than take it. |
environment | string | live or sandbox — which network the event belongs to. Taken from the key that published it; you cannot set it. |
environment is why your sandbox key is useful. A live credential is served live
events and nothing else. A sandbox credential is served both, so you can develop
against real traffic with your own test events mixed into it — and this field is how you
tell them apart. The asymmetry is deliberate and it runs one way only: nothing you publish
with a sandbox key can ever reach a live consumer. You can read a live event with a sandbox
key; you cannot signal one (403). Our own home page publishes through a sandbox key for
exactly this reason.
No field identifies the publisher — not org_id, and since schema_version 2 not a
pseudonym either. Provenance tells you what KIND of source an event came from, never which
one. If you need to recognise your own events in a stream you also consume, use the
event_id the POST response returned: it is a deterministic function of your
(org_id, client_event_id), so you hold it before the delta arrives. client_event_id
itself never appears on a published event. → Privacy
What the network says
The only layer that mutates after write. Full semantics in Trust.
| Field | Type | Notes |
|---|---|---|
status | string | active, expired, invalidated, merged. May grow. |
verification | string | unverified or verified. unverified on write for network events. |
confidence | float | 0…1, 3 decimals. The network's score. |
confirm_count | int | Distinct REPORTERS — vehicles — whose latest signal is a confirmation. The publishing vehicle is excluded. |
deny_count | int | Distinct reporters whose latest signal is a denial. |
merged_into | string | null | The event_id this one was deduplicated into, when status is merged. Reserved for cross-report merging and null on every event — never build on it being populated. |
confirm_count and deny_count counted ORGANISATIONS before schema_version 2 and count
vehicles now, so the same road reality yields a much larger number. They are participation,
not a verdict: threshold on verification or confidence. → Trust
Fields that are not there, and will not be
org_id— removed, and so waspublisher_id, the pseudonym that briefly replaced it. See Privacy.org_count,confirm_org_countanddeny_org_count— removed with the organisation-granularity quorum they belonged to. They had stopped being maintained, and a number that is frozen without saying so is worse than one that is absent. Threshold onverificationorconfidence.client_event_id— your identifier, not public data.- Geo index cells — an implementation detail of our storage. Publishing them would make our index part of a contract we promised never to break.
Reading rules
Three obligations, stated here because this is the page a client author has open:
- Ignore fields you do not know. New ones will appear. Never fail on an unexpected key.
- Tolerate values you do not know in
type,status,severity,source_kindandverification. Skip the event; never crash, never coerce it onto a value you do know. - Never parse
event_idorfeed_id. They are opaque strings whose shape is not part of the contract.
The reasoning behind all three: Compatibility.