Skip to content

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.

ParameterDefaultNotes
latRequired. −90…90.
lonRequired. −180…180.
radius1000Metres. Must be > 0 and <= 9780.
typeallRepeatable: &type=accident&type=congestion. An unknown value is a 422.
min_confidence0.00…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 status is expired, invalidated or merged, with 200. 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 status and expires_at yourself on this route. Treat anything other than status == "active" with expires_at in 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 429 on reads is not a budget — see Errors and limits.
  • Key your local state by event_id and apply an incoming record only if its updated_at is 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.