{"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 that matches no known registry, 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":"case-insensitive; 400 (naming the valid values) if it matches no known registry city. Values are registry REGIONS — some are whole states/provinces (california, ontario, washington), not literal cities."},{"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"},{"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"}}}}}},"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/registries":{"get":{"description":"Discover coverage: every registry this API ingests, with camera counts, licensing fields, tosNotes, and the proxyOk (legal)/pixelReadable (technical, browser-verified) signals. 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\":\"you@example.com\"}). 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"}]}