Skip to content

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:

  1. Sign in at /console/sign-in with your email. Requesting your first code creates the account — there is no password, deliberately, and no separate registration step.
  2. Create an organisation at /console/new, or accept an invitation into an existing one.
  3. 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:

  • verification is unverified. 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_id and no publisher pseudonym on a published event. → Privacy
  • expires_at is one hour out, because that is the TTL for stopped_vehicle. → Event types
  • environment is sandbox, 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 envelopechange 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 cities

make dev-seed prints three keys:

KeyOrganisationWhy it exists
relay_dev_keyorg_devMaster key, accepted only when ENVIRONMENT=local.
relay_demo_keyorg_demo_fleetA publisher.
relay_partner_keyorg_demo_partnerA 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_key

Then read these two

Everything else is reference, but these two contain traps that cost real time:

  1. Publishingheading is 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.
  2. Streaming — the envelope above, plus: deltas can arrive older than the snapshot, and natural expiry sends no delta at all.