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.

Live frames from ~8,500 public traffic cameras your page can actually render — CORS-clean, source bytes untouched, every camera health-polled and trust-scored so the dead ones are flagged before you draw them. Free key in one request.

~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?city=seattle
GET /api/cameras?city=san-francisco
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":"YOUR@EMAIL.HERE"}' | 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-09-01T01:56:18.658Z", "fresh": true, "trustScore": 90 } }, // …2 more, same shape ], "total": 584, "limit": 3, "offset": 0, "hasMore": true }
 
# real response, 2026-09-01 UTC. Trimmed for width: bearingDeg, active,
# registry.slug/name/city/country/licenseUrl/proxyOk and
# stats.uptime7d/lastCheckedAt are in the real payload too.

total: 584 — what's left of Austin's 817 active cameras once the filter keeps only the ones that pass every check; the other 233 on this run (2026-09-01 UTC — and 584 + 233 = 817, the arithmetic has to close or one of the numbers is lying) 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.

Swap in a city name. ?city=austin, ?city=seattle, ?city=san-francisco, ?city=los-angeles, ?city=san-diego, ?city=sacramento, ?city=toronto, ?city=ottawa, ?city=london — case, spaces and punctuation don't matter, and a city inside a state-wide registry (Seattle inside Washington, SF inside California) is narrowed to that city for you. A city we don't cover gets a 400 that says so and names the ones we do — never an empty list dressed up as an answer. GET /api/registries lists every accepted name.

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,
# cities (the city names ?city= accepts for that registry) and
# 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 — works cross-origin natively where pixelReadable is true (~4,000 cameras across those two registries) — and through GET /api/cameras/{id}/frame on every camera, no server of your own required.

“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 across the other 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: 233 of 817 on the 2026-08-31 run, 273 and 338 on earlier ones — real, measured, just not a fixed number. The same day: Ontario 511 75 of 1,663, Washington State 166 of 1,533, Caltrans — the tightest catalog we ingest — just 9 of 3,304, and TfL London clean on every run we've measured. Fleet-wide that's 586 cameras marked active with no picture behind them — we re-fetch that count when this page loads; if the fetch fails, you're seeing the 2026-08-31 measurement.

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.

One caveat we'd rather state than have you discover: uptime measures whether the source answered our fetch. A camera stuck on a placeholder card can hold a perfect uptime24h: 1.0 — catching that is verificationStatus and fresh's job, so read them together, never uptime alone.

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. Caltrans, WSDOT and Ottawa publish none, so all three are commercialOk: null — and together they are 5,248 of 8,507 active cameras (62%) as of 2026-08-31. The majority of the fleet, not an edge case.

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 — a real city name (austin, seattle, san-francisco, toronto) or a whole registry region (california, washington, ontario) — plus 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.
  • GET /api/cameras/{id}/frameThe camera's current frame with CORS solved — fetch(), <canvas> and deck.gl's BitmapLayer work on every camera, not just the pixelReadable ones. Bytes pass through from the source unmodified, cached per camera at the source's own polite cadence.
  • 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; frames have their own budget — 300 and 36,000. 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

  • /streamA live MJPEG stream per camera. Until it exists, poll /frame at the camera's minPollIntervalS — polling faster just re-reads the same cached frame.
  • 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 six. Which comes next 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 — frame proxy included; pick that one if what you want is more cities.

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.