Trust
Your event will be published unverified with a confidence of about 0.3, and it will
stay that way no matter how many times the same vehicle confirms it. That is correct
behaviour.
If you read one section on this page, read that one. It is the single most common thing a new publisher reports as a bug.
The rules
- A reporter is never evidence for its own report. The publishing VEHICLE is excluded; every other vehicle counts.
- The quorum counts vehicles, not organisations. Your own fleet corroborating your own event is real evidence — a second vehicle is a second observation of the road, whoever operates it.
- Clearing an event is never cheaper than promoting one.
Everything below is arithmetic on top of those. The model is deliberately crude rather than clever: a score nobody can audit is worse than a simple one everybody can.
The fields the network writes
Every event carries these. They are the only mutable layer — the observation itself never changes after publication.
| Field | Meaning |
|---|---|
verification | unverified or verified. The gate. |
confidence | 0…1. The network's score, recomputed on every signal. |
confirm_count | Distinct REPORTERS, other than the publishing one, whose latest signal is a confirmation. |
deny_count | Distinct reporters whose latest signal is a denial. |
Identifying a vehicle
Send reporter_token on a publish or a signal — your own opaque name for one of your
vehicles. We salt it with your organisation before storing it; the raw value is never
stored, never mirrored to analytics and never published, and no field on a published event
identifies a vehicle. Only counts are public.
Omitting it is safe and is the pre-existing behaviour: your whole organisation then counts as one reporter. Nothing breaks, you simply get one voice instead of a fleet.
Do not reuse client_event_id for this. That identifies a MESSAGE, so a retry would look
like a new vehicle and your retries would become votes.
Promotion to verified
asserting reporters = 1 (the publishing vehicle) + confirm_count
verified ⟺ asserting >= VERIFY_MIN_REPORTERS AND asserting > deny_countVERIFY_MIN_REPORTERS is a deployment setting, currently 3. That is why a lone
publisher's event is unverified: it has one asserting reporter and needs three.
The publishing vehicle is excluded from its own evidence. Confirming from the same
reporter_token you published with is accepted with 200 and does exactly nothing — no
count, no score change, no TTL extension. A different vehicle of yours does count.
One exception, and it is explicit: an event from an ingested public feed is verified on
arrival. Its source is an identified public road authority publishing under an open
licence, which is a different situation from the anonymous reporting the quorum weighs.
You can tell those apart with source_kind — see Provenance.
The confidence score
confidence = base(source_kind)
+ CONFIDENCE_PER_REPORTER × confirm_count
− CONFIDENCE_PER_DENY × deny_count clamped to [0, 1], 3 decimalsWith the current defaults — base 0.30 for a network event and 0.85 for a public feed,
+0.10 per confirming vehicle, −0.15 per denying one — a fresh network event lands at
0.3.
Those numbers are configuration, not contract. They are not served by any endpoint and
they can be tuned. Filter with min_confidence and thresholds you can change; do not
hardcode 0.3 as "unconfirmed".
The one property that is guaranteed: confidence is monotonically non-increasing in denials. There is no reachable transition that adds a denial and raises the score. Only distinct confirming reporters lift it, so ten confirmations from one vehicle are worth exactly one.
Denials end an event, they do not condemn it
cleared ⟺ deny_count >= CLEAR_MIN_REPORTERS AND deny_count > asserting reportersA denial means "it is not there any more" — the one thing a vehicle driving past can
actually witness. When the quorum is reached the event expires immediately: status
becomes expired, it stops appearing in area queries, and subscribers get a delta with
change: "expired". It is not marked invalidated; that status means "this was never
true", which is a verdict about a publisher and nothing this route can reach.
Both conditions matter. CLEAR_MIN_REPORTERS is currently 3 and can never be lower
than the promotion threshold — removing a live hazard from a driver's screen is more
destructive than promoting one. And a minority never clears: three vehicles at the cleared
tail of a 40 km queue cannot delete an event fifty vehicles are still confirming.
A contested event stays alive with a low score. That is the reversible outcome — a
subscriber filters on min_confidence and decides for itself.
wrong_way_vehicle cannot be cleared by denials at all. It is the only mobile hazard
we carry: a vehicle reaching the reported point three minutes later legitimately sees
nothing because the hazard moved, so the reporters who would clear it are the good-faith
ones arriving behind it. Denials on it are still accepted and still lower confidence — they
simply never end it. For the same reason confirmations do not extend it either: a type the
network cannot vote down must not be one it can hold open. Its 20-minute TTL does the work.
Confirming and denying
POST /v1/events/{event_id}/signals
Authorization: Bearer <key>
Content-Type: application/jsoncurl -sX POST "$RELAY/v1/events/$EVENT_ID/signals" \
-H "Authorization: Bearer $RELAY_KEY" \
-H 'Content-Type: application/json' \
-d '{"action": "confirm", "lat": 45.7641, "lon": 4.8358}'| Field | Required | Notes |
|---|---|---|
action | yes | confirm or deny. |
lat, lon | no | Your position when you observed it — not the event's. |
reporter_token | no | Which of your vehicles is signalling. Omit it and your whole organisation counts as one. |
client_event_id | no | Accepted and unused; signalling is already idempotent by key. |
Returns 200 with the full updated event, so you see the new verification,
confidence and counts immediately.
lat/lon are stored with the signal as evidence. A confirmation sent from the scene is
not worth the same as one sent 40 km away, and distance weighting needs history to
compute from — send it if you have it.
The four rules of signalling
One signal per REPORTER counts, and the latest one wins. A repeat from the same
reporter_token is accepted with 200 and changes nothing at all — no counter, no score,
no TTL, and no delta to subscribers. Retries over a flaky link are free.
Changing your mind moves you across. A vehicle that denied and later confirms (it drove past again) moves from one tally to the other rather than being counted in both.
A confirmation extends the TTL, once per reporter. The first time a given vehicle ever confirms, the event's life is extended to at least 15 minutes from that moment, capped at 12 hours from creation. It is never shortened — so for a fresh event with a two-hour TTL, an early confirmation changes nothing visible.
Note what that formula does and does not do: it tracks the LAST new confirmer rather than accumulating, so a hundred vehicles confirming a live pile-up hold it open, and it dies fifteen minutes after traffic stops confirming — which is when the road cleared. Granting it once per vehicle is what stops one reporter holding an event open forever by alternating deny and confirm.
A dead event takes no signals. Signalling something expired, invalidated or
merged is a 409:
{"detail": "event 7fce24f051e857bfacad005c3f497973 is expired and no longer takes signals"}A 409 is not a client bug — the hazard is simply over. Do not go looking for a fault in
your id handling; that is exactly why it is not a 404. Expect it in bulk: once a quorum
clears an event, every vehicle arriving behind them and reporting the same cleared hazard
gets one.
Provenance
source_kind tells you where an event came from, and it is public on purpose.
source_kind | Meaning | feed_id | Starting confidence |
|---|---|---|---|
network | A Relay participant published it. | null | 0.30, unverified |
public | Ingested from a public feed — the base map. | e.g. datex2.fr.rrn | 0.85, verified |
datex2.fr.rrn is the French national road network's DATEX II feed, published under
Licence Ouverte 2.0. It exists so the network is not empty on your first request. The
contrast between the two kinds is the visible proof of what Relay adds on top of open data.
Scope of the score
- The score reads counts, not history. Every reporter's word weighs the same, by design in v1 — a score nobody can audit is worse than a simple one everybody can. With a vehicle-granularity quorum, nothing in the arithmetic bounds how many reporters one organisation declares; that is a deliberate trade — the alternative made real fleets worthless as evidence — and it is policed outside the score (the per-organisation signal ceiling in Errors and limits), never by the arithmetic.
- Confidence is the network's verdict, never the publisher's claim. A self-declared
reported_confidencewas rejected for v1: feeding the score, every publisher pins it at the maximum; not feeding the score, it does nothing. It only regains meaning alongside calibration. - Reporter positions are recorded with every signal — the evidence base distance weighting computes from. Send them (see above).
- Each report is its own event. Two reporters publishing the same accident produce
two independent events, each
unverified— they do not confirm each other.mergedandmerged_intoare reserved in the contract for cross-report merging and arenullon every event. The intended shape from a fleet is therefore one event with N confirmations, not N events: have the first vehicle publish and the rest confirm. - Events are immutable observations — there is no retraction. A publisher does not withdraw its own report; denying it from the publishing vehicle does nothing, since that reporter is excluded from evidence in both directions. Another of your vehicles denying it does count.