Search the
camera index.
Cameras are everywhere, but their feeds are fragmented and hard to use. OpenEye is building an agent-first visual layer for the planet. Find cameras by coordinates, place, bounding box, route, category, or text through REST or the MCP server; use WHEP when you need live video.
Coverage combines deliverable open-data feeds with source-only public camera pages, including validated official directories such as France's ASFA motorway map and official zoo and aquarium wildlife cams from San Diego, Smithsonian, El Paso, Aquarium of the Pacific, Monterey Bay, Georgia Aquarium, Toledo Zoo, and Wonders of Wildlife. Explore.org's complete 238-record active index contributes 236 source-only records with working official pages, stable feed identities, coordinates where fixed, and retrievable public previews. Every pin has a public path to an image, stream, or per-camera viewer; generic promotional video links do not count as camera-health evidence. Directory media is maintained every 72 hours. Loading a pullable OpenEye-hosted proxy preview refreshes a stale snapshot inline, marks that camera active before cached bytes return, and keeps subsequent pulls on the camera's configured cadence. Opening any eligible directory preview also queues a throttled refresh when its verified image is over 24 hours old. Eligible still responses are reused byte-for-byte as the preview with their upstream Content-Type; there is no resize, watermark, recompression, Images transformation, or second fetch. Every source, including ODOT TripCheck, receives the same seven-day unchanged-image window before being hidden as frozen. Video preview extraction remains a separate pipeline.
Check the Portland I‑5 commute monitorClosure countdown followed by a Powell's Index section with no eyebrow label, its route map, ten full-width route graphs stacked one per line, ODOT corridor times, and then curated camera pressure zones with guaranteed I‑5 freeway views. Includes 24h, 7d, and All time views, green faster-than-usual sections, red slower-than-usual sections, readable dark map popups, labeled axes, exact hover or tap values, and a clean I‑5 shutdown-sign social preview.Open →The database normalizes cameras from otherwise incompatible networks into one searchable record shape. Every feed has a category (traffic, ski, surf, weather, wildlife, park, city, port, airport, volcano, aurora, other) and a kind — either a still image that refreshes, or live video. Each feed also has a human-readable handle (e.g. vail-windy-7k2) that people can open on the site and agents can resolve through MCP or /v1/feeds/:handle.
Public and community-submitted feeds are free to access. Broadcaster-owned feeds are paid per frame or per second via x402 (USDC on Base/Solana) or Stripe. Payment happens at read time — the 402 handshake is documented below.
All endpoints return JSON unless noted. Public read endpoints are anonymous.
/v1/catalog/mapLean projection for rendering many pins. Filter by bbox or category; add zoom + cluster to scale the result to a viewport.
# ski cams in Colorado curl "https://api.openeye.cam/v1/catalog/map?category=ski&bbox=-109,37,-102,41&limit=500" # { # "mode": "items", # "items": [ # { "id": "stream_01...", "handle": "breck-ski-7k2", # "title": "Breck — Peak 8 base", # "lat": 39.4755, "lon": -106.0679, "category": "ski", # "is_free": true } # ], # "total": 96, "free": 96 # } # Dots only — price, attribution and preview come from # GET /v1/catalog/{id} when you open one. # The directory contains hundreds of thousands of cameras, too many to draw at once. # Pass the map's zoom and let the server pick the level of detail: # count bubbles below zoom 10, then one camera per grid cell. curl "https://api.openeye.cam/v1/catalog/map?bbox=-125,24,-66,50&zoom=4&cluster=auto" # { # "mode": "clusters", # "clusters": [ # { "id": "stream_01...", "lat": 40.71, "lon": -74.01, # "count": 1832, "free": 1830, # "categories": { "traffic": 1510, "city": 212, "weather": 110 } } # ], # "total": 33336, "free": 33200 # } # total/free are the real counts for the query either way — # never the size of the sample you were handed.
/v1/catalog/randomOne uniformly random public, live, non-dead camera. The homepage featured set is excluded whenever another public camera is available; it is only a last-resort fallback.
curl https://api.openeye.cam/v1/catalog/random # { "id": "stream_01...", "title": "...", "view": { ... }, ... }
/v1/catalog/featuredCurated visual showcase. Every returned row is render:image. OpenEye-hosted rows normally use an authoritative verified preview object; fresh legacy rows awaiting pointer backfill can use the preview route's ring-frame fallback. candidate_count and omitted_count keep filtered failures visible to monitoring.
curl https://api.openeye.cam/v1/catalog/featured # { "items": [...], "candidate_count": 24, "omitted_count": 1 }
/v1/catalog/categoriesCounts per category. Use for filter sidebars.
curl https://api.openeye.cam/v1/catalog/categories # { "items": [{ "category": "traffic", "count": 789 }, ...], # "total": 45409, "countries": 71 }
/v1/catalog?near=lat,lon&radius_km=NSpatial search via H3. near= rows return nearest-first, filtered to the exact radius, each with distance_km (single page; sort=popularity re-enables cursor paging). Also ?bbox=w,s,e,n, ?q= for FTS, ?embeddable=1 for showable-only. Permissive CORS. THE SAME enriched row shape is returned by every camera endpoint — list, featured (homepage), search, corridor, nearest, and /catalog/:id — so you never make a second call to render one. Featured is stricter: it only returns render:image rows, verifies storage for pointer-backed OpenEye images, and temporarily admits fresh legacy ring frames awaiting preview backfill.
curl "https://api.openeye.cam/v1/catalog?near=39.6,-106.3&radius_km=50" # Every row (here and from search, /catalog/:id, featured, nearest, …): # { # "id": "stream_01…", "title": "Loveland Pass — summit", # "lat": 39.66, "lon": -105.88, "category": "traffic", "is_free": true, # "timezone": "America/Denver", // civil IANA zone derived from coordinates # "live": true, // has a recent frame → render it # "last_frame_age_s": 12, "frame_interval_s": 30, # "frame_ts": 1710000000000, // shown bytes; format in timezone above # "preview_url": "…/preview.webp?v=1&exp=…&sig=…", # "preview_refresh": { // /catalog/:id only; failed pulls retry after 60s # "status": "queued", // queued|retrying|current|unsupported # "stale_after_s": 86400 # }, # "view": { // ← switch on this to render anything # "render": "image", // image=<img> | link=<a> | none=skip # "url": "…/preview.webp?v=1&exp=…&sig=…", # "url_type": "image", // (for link: image|html) # "hosted": "openeye" // who serves the bytes (openeye|source) # }, # "redistribution": { "preview_embed": true, "frame_reuse": "personal-cache", # "frame_access": "proxy", # "attribution": { "name": "CDOT", "url": "https://…", "required": true } } # } # # Directory-only/source rows still appear on the map. If no sanctioned poster # exists, preview_url is null and view.render is "link" or "none"; show a # neutral unavailable card instead of hiding the camera. # # Open direct-image networks (NYC DOT, 511NY/ON/AB) render as an image the # source itself serves — fetch it straight from them, keep the attribution: # "view": { "render": "image", "url": "https://webcams.nyctmc.org/api/cameras/…/image", # "url_type": "image", "hosted": "source" } # # A restricted cam (hotlink-hostile source) instead returns: # "view": { "render": "link", "url": "https://…", "url_type": "html", "hosted": "source" }
/v1/catalog/nearest?near=lat,lonThe single closest live camera to a point, with an auto-expanding radius — no radius_km tuning. Also ?place=<name>. Defaults to free cams; is_free=0 includes paid. Returns { item, radius_km } with distance_km.
curl "https://api.openeye.cam/v1/catalog/nearest?near=40.7561,-73.9857" curl "https://api.openeye.cam/v1/catalog/nearest?place=Times%20Square"
/v1/streams/:id/preview.webpHeader-free sample backed by a verified last-known-good asset. Loading a pullable OpenEye-hosted proxy preview first refreshes it inline when the stored frame is older than twice its configured cadence; refresh failure still serves the last-known-good image. The request also marks it active for subsequent cadence-based pulls. Every free preview preserves source bytes and Content-Type with no Images call, resize, recompression, or watermark; .webp/.jpg are route aliases. Only paid samples are delayed, downscaled, and watermarked. Placeholder cards are locally encoded PNGs and also bypass Images.
# preview_url is ready to paste anywhere — no keyed proxy needed: # <img src="https://api.openeye.cam/v1/streams/STREAM_ID/preview.webp?v=1&exp=...&sig=..." /> # Swap .webp → .jpg on the same link, or revalidate with the ETag: curl -H 'If-None-Match: "<etag>"' "<preview_url>" # → 304 Not Modified when unchanged # No frame right now? You get the "camera unavailable" card, not a broken image: # x-openeye-placeholder: 1 # x-openeye-reason: camera_down | no_frame_yet | no_preview | sample_disabled | link_expired # Real last-known-good bytes report: # x-openeye-preview-state: ready | stale # x-openeye-captured-at: 1787290313932 # Want the error instead? ask for JSON: curl -H 'accept: application/json' "<preview_url>" # → 404/410 problem+json # Paid previews are downscaled, watermarked, and delayed. Free previews preserve source bytes and Content-Type; trust Content-Type rather than the route suffix. # Pace refreshes off frame_interval_s; display x-openeye-captured-at honestly.
/v1/offline.webpThe “camera unavailable” card on its own — public, unsigned, cacheable, always an image. Point an <img> onerror at it when the picture you are rendering is not ours to repair: a view.hosted:'source' cam whose upstream host is down.
<img src="{cam.view.url}" onerror="this.src='https://api.openeye.cam/v1/offline.webp'"> # ?reason= picks the caption (default camera_down): # camera_down | no_frame_yet | no_preview | sample_disabled | link_expired
/v1/streams/:id/frameSingle frame as image bytes. API key required; free for public streams, 402 for paid.
# Get a free API key once (open signup), then set $OPENEYE_KEY curl -X POST "https://api.openeye.cam/v1/agents/register" \ -H "content-type: application/json" -d '{"email":"you@example.com"}' # Public stream — free, but the key is still required curl "https://api.openeye.cam/v1/streams/STREAM_ID/frame?format=webp&max_w=1024" \ -H "Authorization: Bearer $OPENEYE_KEY" -o frame.webp # Paid stream — same key, plus an x402 client that settles payment const fetch402 = withX402(fetch, { wallet: '0x...' }); await fetch402("https://api.openeye.cam/v1/streams/STREAM_ID/frame", { headers: { authorization: 'Bearer ' + process.env.OPENEYE_KEY }, });
/v1/streams/:id/framesSession-mode NDJSON stream. Metered per-frame against a reservation.
curl "https://api.openeye.cam/v1/streams/STREAM_ID/frames?fps=2&duration_s=60&format=webp_b64" \ -H "Authorization: Bearer $OPENEYE_KEY" -H "Accept: application/x-ndjson"
/v1/streams/:id/frame/analyzeFrame + vision model. Frontier: Gemini 3.8 Flash; fast: Gemini 3.5 Flash-Lite; edge: Moondream 3.1. Reserve-then-refund pricing.
curl -X POST "https://api.openeye.cam/v1/streams/STREAM_ID/frame/analyze" \ -H "Authorization: Bearer $OPENEYE_KEY" \ -H "content-type: application/json" \ -d '{ "prompt": "Is the road icy? Count cars.", "model": "fast", "max_tokens": 256 }'
/v1/streams/:id/whep/offerWHEP signaling for live video (live_video feed_kind only). Per-second billing.
# SDP offer in request body; SDP answer in response body. # Requires a live_video stream. See docs/spec/whep.md.
/v1/subscriptionsCondition subscription — on your cadence, POST a signed webhook. trigger_mode=vlm runs analyze_frame (billed, always/on-match); trigger_mode=change is FREE and fires only when the frame changes (no VLM, no prompt). Push instead of poll. min cadence 60s. GET/PATCH/DELETE /v1/subscriptions[/:id] to manage.
curl -X POST "https://api.openeye.cam/v1/subscriptions" \ -H "Authorization: Bearer $OPENEYE_KEY" -H "content-type: application/json" \ -d '{ "stream_id": "STREAM_ID", "prompt": "is the parking lot more than half full?", "cadence_s": 300, "match_only": true, "webhook_url": "https://your.app/openeye-hook" }' # Response includes webhook_secret (shown once). Each delivery carries # X-OpenEye-Signature: sha256=<hmac over the raw body> — recompute to verify.
/v1/traffic/portland-i5/powells-indexPublic Powell's Index behind the I-5 monitor: a stripped-down incident-clock hero followed immediately by a Powell's Index section that starts directly with its heading, without an eyebrow label. Ten named server-sampled routes—including Alberta Co-op, Kenton's Paul Bunyan Statue, Tienda Santa Cruz, Mall 205, Division, the Hollywood Theatre, and Beaverton City Library—appear together on one Google map with current minutes and high-contrast dark marker popups. Full-width 24-hour, seven-day, or all-time graphs and a glanceable four-route ODOT scoreboard follow. Non-overlapping camera zones sit below the travel data, prioritize the views useful to the closure, and use compact 2×2 grids on phones, with badges reserved for refreshing, out-of-date, or unknown-time images. The page rechecks camera metadata five seconds after first load so inline image refreshes are reflected immediately. A dedicated Rose Quarter query guarantees visible slots for I-5 at Rose Quarter—Weidler, Winning Way, and the southbound ramp—plus I-5 at Broadway. Every route card uses a prominent origin heading, a separated Now/Usual/Difference data row, green faster-than-usual line sections, red slower-than-usual sections, a gray usual baseline, labeled axes, exact date spans, and hover, tap, or keyboard sample inspection. The monitor has a dedicated 1200×630 Open Graph preview with an I-5 shield above SHUTDOWN in jagged metal lettering over a dark grid of Portland traffic-camera views. No API key.
curl "https://api.openeye.cam/v1/traffic/portland-i5/powells-index?hours=all&route_id=alberta&step_minutes=60" # hours: 0–168 or all; step_minutes: 5–1440 in five-minute increments # → { cadence_s: 300, sampled_at, history_hours: "all", history_step_s: 3600, routes: [ # { origin_label, current_minutes, usual_minutes, # difference_minutes, maps_url, route_polyline, history: [...] } # ] }
Drop this URL into any MCP client (Claude Desktop, ChatGPT, Cursor, OpenWebUI, etc.). No SDK needed.
https://api.openeye.cam/mcpExample: Claude Desktop config at ~/Library/Application Support/Claude/claude_desktop_config.json:
{ "mcpServers": { "openeye": { "url": "https://api.openeye.cam/mcp" } } }
Free tools return data synchronously. Paid tools enter the 402 bridge.
list_streamssearch_streamsfind_streams_nearfind_streams_in_bboxfind_streams_by_categorylist_categoriesget_streamget_camera_positionget_powells_indexget_frameopen_frame_streamopen_liveanalyze_framewatch_streamlist_subscriptions / unwatch_streamPublic and user-submitted streams are free. For paid streams, two protocols are accepted side-by-side:
- x402 (USDC on Base or Solana) — cryptographic, zero human in the loop, single-round-trip with dual-auth legs.
- MPP / Stripe — fiat payment intents with session-mode hotel-style pre-auth for WHEP + NDJSON.
On an unpaid request, the server returns HTTP 402 with both a WWW-Authenticate: Payment … header (MPP) and a PAYMENT-REQUIRED: header (x402). Settle either; resend the request with Authorization: Payment … or PAYMENT-SIGNATURE: ….
import { withX402Client } from '@openeye/sdk'; const client = withX402Client({ wallet: { privateKey: process.env.WALLET_KEY }, network: 'base', }); const frame = await client.getFrame({ streamId: 'stream_...' });
No login required. Anyone can submit — admins can take down abuse.
/v1/submit-cameraAnonymous. IP-rate-limited to 20/hour.
curl -X POST https://api.openeye.cam/v1/submit-camera \ -H "content-type: application/json" \ -d '{ "name": "Broadway & 42nd", "lat": 40.7561, "lon": -73.9857, "url": "https://example.com/cam.jpg", "feed_kind": "snapshot_pull", "category": "city", "snapshot_interval_s": 60 }'
Or use the web form with a click-to-pin map.