Getting started
The path with no paperwork: sign in to the Console with just an
email, mint yourself a key, and talk to the real, deployed network at
https://api.cudatus.com. Start at Getting a key, then follow the
three steps below: publish, read, stream.
If you are working inside the Relay repository itself rather than evaluating it from
outside, a local stack also exists — see
Local stack for contributors. A partner integrates
against https://api.cudatus.com.
Getting a key
Keys are issued from the Relay Console, self-service:
- Sign in at
/console/sign-inwith your email. Requesting your first code creates the account — there is no password, deliberately, and no separate registration step. - Create an organisation at
/console/new, or accept an invitation into an existing one. - Issue a key from the organisation's keys page. Issuance is an owner act, and every other owner of the organisation is emailed within seconds. If your own account has enrolled a TOTP authenticator, issuance asks for a code at that moment — a step-up, not the sign-in from thirty days ago, and the check is per-account rather than per-organisation. If it has not, issuance is protected by the session alone: you do not have to set up an authenticator before your first key, and enrolling one upgrades every future issuance to a step-up.
The raw key is shown once, and only its SHA-256 is stored. Nobody, support included,
can recover it afterwards; a lost key is reissued, never retrieved. Store it where you
store your other server secrets, and pick live or sandbox when you issue it.
Pick sandbox while you are integrating, and know what it does. A sandbox key is
served both networks, so your filters run against real traffic; nothing it publishes can
ever reach a live consumer, and it cannot confirm or deny a live event (403). Going live
is a new key, not a flag: the environment is fixed on the credential at issuance. One
consequence to plan for — event_id is derived from your client_event_id, so the same
value cannot be reused across the two environments (409). Namespace them.
Revocation is self-service too. An owner revokes a key from the console, and the flag is honoured on the next request that presents it. Revocation asks for no TOTP code, on purpose: stopping a leak should never wait on an authenticator.
An operator CLI (make issue-key) still exists for the acts that have no human owner —
the system organisation behind ingested public feeds, bootstrapping, lockout support. It
is not how a partner gets a key.
Step 1 — publish an event
export RELAY=https://api.cudatus.com
export RELAY_KEY=<the sandbox key you just issued>
curl -sX POST "$RELAY/v1/events" \
-H "Authorization: Bearer $RELAY_KEY" \
-H 'Content-Type: application/json' \
-d '{
"type": "stopped_vehicle",
"lat": 45.7640,
"lon": 4.8357,
"heading": 180,
"road_ref": "A7",
"severity": "medium",
"description": "Broken-down van on the hard shoulder",
"client_event_id": "my-incident-42"
}'201 Created, and the response is the event as the network now holds it:
{
"event_id": "3d5174f7ca2a5e60b7b32e4fa7713174",
"schema_version": 2,
"type": "stopped_vehicle",
"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,
"status": "active",
"verification": "unverified",
"confidence": 0.3,
"confirm_count": 0,
"deny_count": 0,
"updated_at": "2026-08-03T13:15:51.594177Z",
"merged_into": null,
"environment": "sandbox",
"subtype": null
}Four things to notice, all of them normal:
verificationisunverified. It will stay that way until enough other VEHICLES confirm it — the quorum counts reporters, so your own fleet corroborating counts. → Trust- Nothing identifies you. There is no
org_idand no publisher pseudonym on a published event. → Privacy expires_atis one hour out, because that is the TTL forstopped_vehicle. → Event typesenvironmentissandbox, because that is what a sandbox key publishes onto. Nothing you publish this way can ever reach a live consumer.
Every example on these pages is curl, and every one of them is a plain HTTP call from any language — there is no SDK, deliberately. From Python, the same publish is standard library only:
import json
import os
import urllib.request
req = urllib.request.Request(
os.environ["RELAY"] + "/v1/events",
data=json.dumps({
"type": "stopped_vehicle",
"lat": 45.7640,
"lon": 4.8357,
"heading": 180,
"road_ref": "A7",
"severity": "medium",
"description": "Broken-down van on the hard shoulder",
"client_event_id": "my-incident-43", # a reused id replays; it never updates
}).encode(),
headers={"Authorization": "Bearer " + os.environ["RELAY_KEY"],
"Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as res:
print(res.status, json.load(res)["event_id"])Step 2 — read it back
curl -s -H "Authorization: Bearer $RELAY_KEY" \
"$RELAY/v1/events?lat=45.7640&lon=4.8357&radius=2000"A JSON array, nearest first. radius is in metres and is capped at 9 780 m — see
Consuming for why, and what to do about a bigger area.
Step 3 — receive it live
Open a stream in one terminal:
curl -sN -H "Authorization: Bearer $RELAY_KEY" \
"$RELAY/v1/stream?lat=45.7640&lon=4.8357&radius=5000"You get a snapshot frame immediately, then a delta for every change. Publish again from
another terminal (Step 1, with a different client_event_id) and watch it arrive.
event: snapshot
data: {"events": [...], "watch": {"lat": 45.764, "lon": 4.8357, "radius_m": 5000.0}, "max_seconds": 3300}
event: event
data: {"change": "created", "event": {"event_id": "f3c3...", "type": "accident", ...}}Read that second frame carefully before you write a client. The SSE event name is
event and its payload is an envelope — change plus event. A client that reads
event_id off the delta itself gets undefined, silently ignores every change, and shows
a connection that is open, authenticated, receiving bytes and delivering nothing. That is
the single most expensive mistake in this API, and our own tooling made it first. Do not
write that client from scratch: Streaming ends with
complete JavaScript and Python clients — the envelope, the ordering rule and the
silent-expiry sweep already implemented.
Local stack for contributors
Everything above talks to the real, deployed API. This section is for a different
reader — someone working inside the Relay repository itself, not an external partner
evaluating the product: a partner integrates against https://api.cudatus.com.
If that is you, the local stack is the whole API — same code, same routes, Datastore and Pub/Sub emulators underneath:
make dev-up # api on :6082, Swagger on :6082/docs
make dev-seed # two demo orgs, their API keys, and sample events in two citiesmake dev-seed prints three keys:
| Key | Organisation | Why it exists |
|---|---|---|
relay_dev_key | org_dev | Master key, accepted only when ENVIRONMENT=local. |
relay_demo_key | org_demo_fleet | A publisher. |
relay_partner_key | org_demo_partner | A second organisation — evidence arriving from a fleet the publisher does not operate. |
One key can promote its own event now: the quorum counts vehicles, so confirming with
distinct reporter_tokens under the same key is real evidence — only the publishing
vehicle is excluded. The second organisation exists to demonstrate cross-fleet
corroboration, which is what the network looks like in production. See Trust.
The local stack does not carry the French DATEX II base map by default; make ingest pulls it once if you want real incidents on real roads.
Re-run Step 1 through Step 3 against it by pointing the same two variables at it instead:
export RELAY=http://localhost:6082
export RELAY_KEY=relay_dev_keyThen read these two
Everything else is reference, but these two contain traps that cost real time:
- Publishing —
headingis the direction of the drivers at risk, not of whatever caused the event. On a wrong-way alert, getting it backwards warns the only driver who already knows. - Streaming — the envelope above, plus: deltas can arrive older than the snapshot, and natural expiry sends no delta at all.