{"openapi":"3.0.3","info":{"title":"Datumfeed","version":"0.1.0"},"components":{"schemas":{"Camera":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"lat":{"type":"number"},"lon":{"type":"number"},"bearingDeg":{"type":["number","null"]},"feedUrl":{"type":"string"},"feedType":{"type":"string"},"verificationStatus":{"type":"string","enum":["unverified","auto_checked","verified","contradicted"]},"verificationNotes":{"type":["string","null"]},"active":{"type":"boolean"},"registry":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"licenseName":{"type":["string","null"]},"licenseUrl":{"type":["string","null"]},"attribution":{"type":["string","null"]},"commercialOk":{"type":["boolean","null"]},"proxyOk":{"type":["boolean","null"]},"pixelReadable":{"type":["boolean","null"]}}},"stats":{"type":"object","properties":{"uptime24h":{"type":["number","null"]},"uptime7d":{"type":["number","null"]},"lastOkAt":{"type":["string","null"]},"lastCheckedAt":{"type":["string","null"]},"fresh":{"type":["boolean","null"]},"trustScore":{"type":["number","null"]}}}}}}},"paths":{"/api/cameras":{"get":{"description":"List cameras, optionally filtered by city, country, bounding box, minimum trust score, or verification status. Paginated: check hasMore/total, not just whether cameras came back non-empty, to know if you have the complete filtered set. Any query param not listed here, and any city/country value we have no coverage for, returns 400 rather than a silently-unfiltered or silently-empty 200 -- see GET /api/registries to discover valid city/country values up front.","parameters":[{"schema":{"type":"string"},"in":"query","name":"city","required":false,"description":"A city name (toronto, seattle, san francisco, los angeles, san diego, austin, ottawa, london, ...) OR a whole registry region (austin, california, london, ontario, ottawa, washington). Case-, space- and punctuation-insensitive: \"San Francisco\", \"san-francisco\" and \"sanfrancisco\" are the same value. A city inside a larger registry is resolved to that registry plus a bounding box; the resolved block in the response says which. A city we have no cameras for returns 400 naming the cities we do cover -- never an empty 200. GET /api/registries lists the city names under each registry."},{"schema":{"type":"string"},"in":"query","name":"region","required":false,"description":"alias for city= (same values, same behavior); pass one or the other, not both"},{"schema":{"type":"string"},"in":"query","name":"country","required":false,"description":"case-insensitive; 400 (naming the valid values) if it matches no known registry country"},{"schema":{"type":"string"},"in":"query","name":"bbox","required":false,"description":"minLon,minLat,maxLon,maxLat. Combines with (does not replace) the box a city= name resolves to -- both are applied."},{"schema":{"type":"string"},"in":"query","name":"limit","required":false,"description":"positive integer, max 500 (default 100)"},{"schema":{"type":"string"},"in":"query","name":"offset","required":false,"description":"non-negative integer (default 0)"},{"schema":{"type":"string"},"in":"query","name":"minTrust","required":false,"description":"integer 0-100; keeps cameras with stats.trustScore >= this value; cameras with a null trustScore (never polled) are EXCLUDED"},{"schema":{"type":"string"},"in":"query","name":"verificationStatus","required":false,"description":"one of unverified | auto_checked | verified | contradicted; 400 on any other value"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"number","description":"this page's length (cameras.length)"},"cameras":{"type":"array"},"total":{"type":"number","description":"full count matching your filters, ignoring limit/offset"},"limit":{"type":"number"},"offset":{"type":"number"},"hasMore":{"type":"boolean","description":"true if offset + count < total — keep paging"},"resolved":{"type":"object","description":"only present when city=/region= was passed: {input, matchedAs: \"city\"|\"registry-region\", city?, region, bbox} — how your value was understood and which bounding box (if any) was applied","additionalProperties":true}}}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/cameras/{id}":{"get":{"description":"Get a single camera by id. Takes no query params — any query string 400s (see GET /api/cameras for filters).","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/cameras/{id}/frame":{"get":{"description":"The camera's current frame (image bytes, passed through from the source unmodified), served with Access-Control-Allow-Origin: * -- fetch(), canvas, createImageBitmap and deck.gl BitmapLayer all work cross-origin on every camera, regardless of the source's own CORS posture. Cached per camera for registry.minPollIntervalS seconds (the source's polite cadence); polling faster than that re-serves the same cached bytes. Cache-Control max-age = seconds until this camera's cache can refresh; Last-Modified = when the served frame was actually fetched. If the source is down but an older frame is cached, that frame is served instead (check Last-Modified). Separate rate budget from browsing: 300/h keyless per IP, 36,000/h with a key. Errors: 403 registry.proxyOk=false (directory-only source; fetch feedUrl directly), 410 inactive camera, 501 non-jpeg_poll feed type, 502 source unreachable with nothing cached.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"the current frame; bytes exactly as the source served them","content":{"application/json":{"schema":{"type":"string","format":"binary","description":"the current frame; bytes exactly as the source served them"}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"410":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"501":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/registries":{"get":{"description":"Discover coverage: every registry this API ingests, with camera counts, licensing fields, tosNotes, the proxyOk (legal)/pixelReadable (technical, browser-verified) signals, and `cities` — the city names GET /api/cameras?city= accepts for this registry (a registry covering a whole state/province serves several). No pagination — the registry count is small by design. Takes no query params — e.g. ?city= here 400s; filter GET /api/cameras instead.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true}}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/keys":{"post":{"description":"Issue a free API key for the given email ({\"email\":\"<your-email>\"} -- a real address; reserved example.com/test/invalid addresses are refused). The key is shown once. Capped at 3 NEW keys per hour per IP -- separate from the 60/h keyless and 3600/h keyed browsing budgets. A request that 400s (bad JSON, invalid email) does not spend this budget; only a request that actually issues a key does. Exhausting it returns a 429 with a message distinct from the general browsing-limit 429, naming the 3/hour cap explicitly. Also capped at 20 attempts/10min/IP regardless of validity (a separate, coarser flood guard).","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"key":{"type":"string"},"note":{"type":"string"}}}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/llms.txt":{"get":{"responses":{"200":{"description":"Default Response"}}}},"/openapi.json":{"get":{"responses":{"200":{"description":"Default Response"}}}},"/signup":{"post":{"responses":{"200":{"description":"Default Response"}}}}},"servers":[{"url":"https://datumfeed.com"}]}