LIVE NOW · VERIFIED PUBLIC CAMERA FEEDS

You built the map.
We have the feeds.

datum (n.) — the surveyed reference surface every other measurement is judged true against. That's the job: everyone else's camera list is a claim; this one is checked.

In the worst registry we've measured, half the cameras reported “active” weren't sending a picture — we checked. ~8,500 cameras are active right now, across six registries — Caltrans (California, SF to San Diego), Washington State, Austin, Ottawa, Ontario 511 and TfL London (~9,000 cameras listed in total) — fetched by us, coordinate-sanity-checked, health-polled on a loop and trust-scored, so you can tell the ones that are actually live from the ones their city only says are live. Free key in one request. No dashboard, no sales call, no waitlist.

~8,500 ACTIVE 6 REGISTRIES HEALTH-POLLED ROUND THE CLOCK FREE TIER 3,600 REQ/H
point-your-agent
# hand your agent the whole API in one file:
$ curl -s /llms.txt

# it comes back knowing:
GET /api/registries
GET /api/cameras?city=austin&minTrust=70
GET /api/cameras?bbox=…
GET /api/cameras/{id}
POST /api/keys
→ plus every field, rate limit and gotcha,
in plain text, written for agents.

Two commands. No signup flow.

Copy the block. It runs as-is — change the email, and step 2 picks the key up automatically (needs jq; or paste the key by hand). The GET also works with no key at all at 60 requests/hour, so you can skip step 1 entirely if you just want a look.

first-call.sh
# 1 · free key — one request, no confirmation email, no dashboard
$ KEY=$(curl -s -X POST /api/keys \
-H 'content-type: application/json' \
-d '{"email":"you@example.com"}' | jq -r .key)
 
# the response: {"key":"gsk_…","note":"Store this key; it is shown once."}

# 2 · three clean, verified Austin cameras — filtered on the way in
$ curl -s "/api/cameras?city=austin&verificationStatus=auto_checked&limit=3" \
-H "authorization: Bearer $KEY"
 
{ "count": 3, "cameras": [ { "id": "atx-1", "name": "830 BLK W RUNDBERG LN (Little Walnut Creek Library, HEB)", "lat": 30.363686, "lon": -97.698158, "feedUrl": "https://cctv.austinmobility.io/image/1.jpg", "feedType": "jpeg_poll", "verificationStatus": "auto_checked", "verificationNotes": "bbox ok; unique coords; image ok", "registry": { "attribution": "City of Austin Transportation Department", "licenseName": "Public Domain (data.austintexas.gov)", "commercialOk": true, "pixelReadable": false }, "stats": { "uptime24h": 1, "lastOkAt": "2026-07-23T19:22:05.572Z", "fresh": true, "trustScore": 90 } }, // …2 more, same shape ], "total": 542, "limit": 3, "offset": 0, "hasMore": true }
 
# real response, 2026-07-23 UTC. Trimmed for width: bearingDeg, active,
# registry.slug/name/city/country/licenseUrl/proxyOk and
# stats.uptime7d/lastCheckedAt are in the real payload too.

total: 542 — what's left of Austin's 817 active cameras once the filter keeps only the ones that pass every check; the other 273 this run were caught serving a placeholder, same as below. Austin throttles our polling, so that count moves cycle to cycle — see Ottawa's 207-of-411 for the reproducible version of this same finding. hasMore: true means it's still a slice, not the set — page with &offset=, limit maxes at 500. minTrust=N filters the same way, on stats.trustScore instead.

Ask what's there before you ask for it.

GET /api/registries is the whole fleet in one call — coverage, licensing, and whether a browser can actually read the pixels — with zero camera rows downloaded.

registries.sh
$ curl -s /api/registries
 
[ { "slug": "austin-traffic", "activeCameras": 817, "totalCameras": 1005, "commercialOk": true, "pixelReadable": false }, { "slug": "caltrans-cctv", "activeCameras": 3304, "totalCameras": 3476, "commercialOk": null, "pixelReadable": true }, { "slug": "ontario-511", "activeCameras": 1663, "totalCameras": 1663, "commercialOk": true, "pixelReadable": false }, { "slug": "ottawa-traffic", "activeCameras": 411, "totalCameras": 411, "commercialOk": null, "pixelReadable": false }, { "slug": "tfl-jamcams", "activeCameras": 779, "totalCameras": 882, "commercialOk": true, "pixelReadable": true }, { "slug": "wsdot-cameras", "activeCameras": 1533, "totalCameras": 1533, "commercialOk": null, "pixelReadable": false } ]
 
# real response, 2026-07-29 UTC. Trimmed for width: name, city, country,
# licenseName/licenseUrl/attribution/proxyOk/tosNotes are in the real
# payload too — no query params, no pagination, one call for the fleet.

pixelReadable: true on TfL London and Caltrans California — verified in a real headless browser, not asserted. A <img src="{feedUrl}"> renders any camera in this API. Reading actual pixels — fetch(), <canvas>, deck.gl's BitmapLayer — only works cross-origin where pixelReadable is true: ~4,000 cameras across those two registries today, no proxy required. Everywhere else, fetch feedUrl server-side.

“Active” is a claim. We check it.

A published camera list is a snapshot of intent. Ottawa's has no status field at all; Austin's says which of its cameras are switched on. Neither can tell you which cameras are returning actual pixels right now — and both are already out of date by the time you read them. So we fetch instead of trusting.

411 cameras Ottawa reports active — the registry we've measured longest
207 of them answer with the exact same byte-identical “Camera Video Unavailable” card when we actually fetch the feed
50.4% 207 of 411 when we first measured the full registry, 2026-07-22 UTC. The number moves as the city fixes and breaks feeds — the query below always returns today's count, receipts included — but on the registry we've checked longest, half of what was marked “active” wasn't sending a picture the day we looked.
five cameras ottawa lists as active
$ MAP=https://traffic.ottawa.ca/map/camera
$ for i in 100 101 102 105 108; do
curl -s "$MAP?id=$i" -o f \
-w "$i %{http_code} %{content_type} %{size_download}B "
md5sum < f
done
 
100 200 image/jpg 22379B 97c918910fe0…
101 200 image/jpg 22379B 97c918910fe0…
102 200 image/jpg 22379B 97c918910fe0…
105 200 image/jpg 22379B 97c918910fe0…
108 200 image/jpg 22379B 97c918910fe0…

Five of the 207, run 2026-07-22 UTC against Ottawa's own feed URLs. Five HTTP 200s carrying one byte-identical, 22,379-byte JPEG — a bilingual “Camera Video Unavailable / Caméra vidéo non disponible” card, not a street.

  • A status column is a promise. A fetch is evidence. Every active camera in the directory is polled on a loop, and what comes back is recorded: uptime24h, uptime7d, lastOkAt.
  • Frozen frames are caught too. fresh compares the image hash between polls — a camera stuck on a placeholder answers 200 forever but stops changing, and that shows up.
  • It rolls into one number. trustScore (0–100) folds uptime, verification status and freshness together, so you can sort by "actually worth rendering" instead of by luck. null means never polled — which is different from bad.
  • We keep the receipts, not just the verdict. Cameras that fail verification come back as verificationStatus: "contradicted" with a note saying what we caught, rather than quietly disappearing from the response.
  • And we're not immune to it either. Politeness costs us reach: our poller deliberately backs off rather than hammer a source, so a registry with an aggressive CDN costs us coverage on any single pass. That's exactly why trustScore is a rolling measurement over many samples, not one check — and why the number above is Ottawa's, not a source that throttles us.

The same pattern shows up on the other two registries, at different rates — we're not singling one city out. Austin's count moves with how much of its CDN we can reach in a given cycle, since we poll it politely rather than push through the throttling: 273 of 817 on this run, 338 on an earlier one — real, measured, just not a fixed number. Ontario 511, the newer of the two, comes in lower: 61 of 1,663. Across all three registries that's 541 cameras, right now, marked active with no picture behind them. TfL London's 788 active cameras: zero.

Five things the raw city feeds don't give you.

All of it travels inside the same camera object — nothing to join, nothing to look up in a second place.

01

Coordinates that survive a fetch

Each camera's coordinates are checked against the city's bounding box and de-duplicated — registries hand out one placeholder coordinate for whole batches — and its feed URL is fetched to confirm it returns an actual image.

That's auto_checked. Landmark-level confirmation (verified) is a later pass we don't run yet, and we don't pretend otherwise.

02

Uptime we measured, not uptime we were told

Every active camera is polled on a loop and every result is kept. uptime24h, uptime7d, lastOkAt, fresh and a 0–100 trustScore ride along with every camera in the response.

A camera nobody has polled yet reports null, not zero. We don't invent a number to fill the field.

03

Licensing attached to every row

registry.licenseName, licenseUrl, attribution and commercialOk travel with each camera. Austin, Ontario 511 and TfL London publish clear terms — commercialOk: true. Ottawa doesn't, so it's null.

null means unknown, not cleared. We won't guess on your behalf.

04

Docs written for the thing reading them

/llms.txt is the entire API as one plain-text file — endpoints, field semantics, rate limits, and the gotchas that would otherwise cost you an afternoon. /openapi.json for the machine-readable spec. Neither needs a key.

Those gotchas aren't hypothetical — each one is written down because somebody hit it first.

05

Coverage before you fetch a single camera

GET /api/registries lists every source we ingest — activeCameras/totalCameras, licensing, tosNotes, and both proxyOk (legal) and pixelReadable (technical, browser-verified) — with zero camera rows downloaded.

Then narrow for real: minTrust or verificationStatus on GET /api/cameras keep only what's worth rendering — not everything a registry lists.

Cameras are deep. The rest is roadmap.

This is Phase 1. Everything under NOT BUILT YET is exactly that — not behind a flag, not in private beta, not "contact us". It does not exist yet, and nothing on this page pretends it does.

LIVE TODAY

  • GET /api/camerasFilter by city, country, bbox, minTrust or verificationStatus. Paginated: limit (max 500), offset, total, hasMore.
  • GET /api/cameras/{id}One camera, full record.
  • GET /api/registriesCoverage, licensing and pixelReadable per source — no camera download required.
  • POST /api/keysFree key, issued instantly to any email. Shown once.
  • GET /llms.txt · GET /openapi.jsonAgent docs and the OpenAPI spec. No key required.
  • Rate limits, in the open60 req/h without a key, 3,600 with one. X-RateLimit-* on every response, Retry-After on a 429.
  • Six registriesCaltrans California, Washington State, Austin, Ottawa, Ontario 511, TfL London — ~9,000 cameras listed, ~8,500 active, health-polled continuously.

NOT BUILT YET

  • /frame · /streamA same-origin image proxy, CORS solved. Today: <img src="{feedUrl}"> renders fine everywhere, but fetch(), <canvas> and deck.gl's BitmapLayer only work where registry.pixelReadable is true — that's TfL London today, not Austin, Ottawa or Ontario 511. Fetch feedUrl server-side for the rest.
  • Historical framesNothing is archived yet. There is no back-in-time endpoint to call.
  • Aircraft (ADS-B) · satellites (TLE)Planned as passthrough behind the same key. Not in the API today.
  • More registriesThe adapter shape is proven on four. Which comes fifth is the question below.

Tell us what to build next

The cameras are live — you don't need this form to use them. This is for the things that aren't built yet: leave an email and we'll tell you when the ones you pick ship. What gets picked most gets built first.

What should ship next?

Cameras are already live — pick that one if what you want is more cities, or a CORS-solved frame proxy on top of them.

Public data only. Free tier stays free.

NOTED

We'll email when the things you picked ship.

Your picks go straight into the build order. Meanwhile the cameras are live — grab a key and make the first call.

Public data only. Free tier stays free.