Skip to content

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

  1. A reporter is never evidence for its own report. The publishing VEHICLE is excluded; every other vehicle counts.
  2. 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.
  3. 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.

FieldMeaning
verificationunverified or verified. The gate.
confidence0…1. The network's score, recomputed on every signal.
confirm_countDistinct REPORTERS, other than the publishing one, whose latest signal is a confirmation.
deny_countDistinct 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_count

VERIFY_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 decimals

With 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 reporters

A 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/json
curl -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}'
FieldRequiredNotes
actionyesconfirm or deny.
lat, lonnoYour position when you observed it — not the event's.
reporter_tokennoWhich of your vehicles is signalling. Omit it and your whole organisation counts as one.
client_event_idnoAccepted 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_kindMeaningfeed_idStarting confidence
networkA Relay participant published it.null0.30, unverified
publicIngested from a public feed — the base map.e.g. datex2.fr.rrn0.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_confidence was 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. merged and merged_into are reserved in the contract for cross-report merging and are null on 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.