Consuming by area
Two read routes. For anything continuous you want the stream instead — polling an area is for a page load, a batch job or a health check, not for a moving vehicle.
Events around a point
GET /v1/events?lat=&lon=&radius=&type=&min_confidence=
Authorization: Bearer <key>curl -s -H "Authorization: Bearer $RELAY_KEY" \
"$RELAY/v1/events?lat=48.8698&lon=2.3078&radius=5000&type=accident&min_confidence=0.5"Returns a JSON array of event objects, nearest first.
| Parameter | Default | Notes |
|---|---|---|
lat | — | Required. −90…90. |
lon | — | Required. −180…180. |
radius | 1000 | Metres. Must be > 0 and <= 9780. |
type | all | Repeatable: &type=accident&type=congestion. An unknown value is a 422. |
min_confidence | 0.0 | 0…1. Drops events the network trusts less than this. |
Only active, unexpired events are returned. An event that has expired, been invalidated
or merged is not in this array — you never have to filter status yourself here.
Distance is measured to the event's whole extent, not its reference point. A 40 km queue passing 100 m from you is returned even though it starts 20 km away:
# the queue starts at 44.93,4.89 — this reads from its middle and still finds it
curl -s -H "Authorization: Bearer $RELAY_KEY" \
"$RELAY/v1/events?lat=44.74&lon=4.82&radius=1000"Result cap. One query returns at most 500 events after distance filtering (a
deployment setting, not a wire guarantee, and not published by /v1/meta/limits). There
is no pagination and no cursor — if you are hitting the cap, narrow the radius or
filter by type. Do not assume the array is complete when its length equals a round
number.
The radius cap: 9 780 m
curl -s "$RELAY/v1/meta/limits"{
"max_query_radius_m": 9780.0,
"query_level": 5,
"note": "beyond max_query_radius_m, cover the area with several overlapping queries; route-corridor subscriptions are planned and not available"
}Ask for more and you get a 422, not a truncated answer:
{"detail": "radius 20000 m exceeds MAX_QUERY_RADIUS_M (9780 m) — cover a larger area with several overlapping queries"}Where the number comes from. Events are located by geohash cells at level 5 (~4.89 km
square). An area query reads the block of cells around you, and the largest block the
store will serve in one shot is 5×5 — two cells of margin in every direction, so
2 × 4 890 m. The cap is arithmetic, not a policy dial. Read it from
/v1/meta/limits; do not hardcode 9780.
What to do instead. One of:
- Tile it. Issue several queries on a grid of centres, each within the cap, and
deduplicate by
event_id— a segment event legitimately appears in more than one. This is what we would do. - Narrow the question. Most "I need 50 km" requests are really "I need the road ahead", which is a corridor and not a disc. A 9.78 km radius around each of five points along your route covers a route far better than one enormous circle, and returns a fraction of the irrelevant events.
A corridor is not a disc, and the contract keeps them apart. A driver asking for the next 50 km of an itinerary is describing a corridor; the API expresses areas as discs, covered by tiling as above. A corridor subscription, when introduced, will be a new parameter or route, never a change to this one.
One event by id
GET /v1/events/{event_id}
Authorization: Bearer <key>curl -s -H "Authorization: Bearer $RELAY_KEY" "$RELAY/v1/events/3d5174f7ca2a5e60b7b32e4fa7713174"200 with the event, 404 if the id is unknown — or known and not on your network: a
live key asking for a sandbox event gets the same 404, never a hint that it exists.
This route does not filter status or expiry. Unlike the area query, it returns an event whose
statusisexpired,invalidatedormerged, with200. That is deliberate — you asked for a specific event and the accurate answer is its current state, not a 404 that makes you doubt your id handling.So check
statusandexpires_atyourself on this route. Treat anything other thanstatus == "active"withexpires_atin the future as gone.
Filters, and what they do not do
type and min_confidence are applied server-side, on both this route and the stream,
with identical semantics. A safety feed that ships everything and lets the client sort it
out is a feed that wakes a driver for an incident 400 km away.
Everything else is a consumer-side filter, deliberately: heading, severity,
road_ref, source_kind, verification, time window. Filter those client-side.
heading is worth calling out. Relay serves you every event in the radius regardless of
which way its traffic runs, so direction filtering is yours to do — compare
event.heading against your own bearing, and treat null as "concerns both directions".
That is also why publishers getting heading backwards is dangerous rather than merely
untidy; see Publishing.
min_confidence filters on the network's confidence score, not on verification. If
what you want is "only events more than one vehicle vouches for", the right filter
is verification == "verified" client-side, or a min_confidence above what a
single-reporter event can reach on its own. See Trust for the arithmetic.
Polling advice
If you poll:
- Poll no faster than you can act. The absence of a
429on reads is not a budget — see Errors and limits. - Key your local state by
event_idand apply an incoming record only if itsupdated_atis newer than the one you hold. Two polls can race. - Drop events on your own clock, at
expires_at. The array will stop containing them too, but not necessarily on the same tick. - Really, use the stream. It is the same filter semantics, one connection, and sub-second delivery instead of your poll interval.