{"openapi":"3.1.0","info":{"title":"Fovea","version":"1.0.0","summary":"Video in, your JSON out.","description":"Fovea turns a video into JSON matching a schema you supply. Send a video file or a URL together with a JSON Schema — or a loose sketch of the shape you want — and receive JSON validated against it before it is returned; a malformed shape is retried internally rather than handed back half-built. Any field asking for a time comes back as MM:SS. Billed per second of video with a one-minute minimum, measured server-side from the received file. Videos are deleted once the job finishes and nothing sent here is used to train a model. The same capability is exposed to agents over MCP at POST /v1/mcp, with the same key and the same metering.","contact":{"name":"Fovea","email":"hello@fovea.run"}},"servers":[{"url":"https://api.fovea.run"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Your Fovea API key."}},"schemas":{"ExtractResponse":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The job id. Retrievable later at /v1/jobs/{id}."},"data":{"description":"Your data, validated against the schema you sent."},"video":{"type":"object","properties":{"duration_seconds":{"type":"number","description":"Measured from the file itself, never taken from the request."}},"required":["duration_seconds"],"additionalProperties":false},"billed_seconds":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"What this job cost. Per second, with a one-minute minimum."}},"required":["id","data","video","billed_seconds"],"additionalProperties":false},"Error":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthorised","schema_missing","schema_invalid","media_unreadable","video_missing","video_unreachable","video_too_long","file_too_large","out_of_minutes","too_many_jobs","rate_limited","extraction_failed","provider_unavailable"]},"message":{"type":"string"},"details":{"description":"Present when we can be specific.","type":"array","items":{"type":"string"}}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"Account":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"email":{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"minutes_remaining":{"type":"number","description":"Fractional. The balance divided by sixty."},"seconds_remaining":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["email","minutes_remaining","seconds_remaining"],"additionalProperties":false}}},"security":[{"bearerAuth":[]}],"paths":{"/v1/extract":{"post":{"operationId":"extractFromVideo","summary":"Extract structured data from a video","description":"Analyse a video and return JSON matching a schema you supply.\n\nThe `schema` field accepts either a full JSON Schema or a loose sketch of the shape you want — `{ \"decisions\": [{ \"at\": \"timestamp\", \"text\": \"string\" }] }` is valid. Any field that asks for a time comes back as `MM:SS`, so you can jump to that point in the video.\n\nEvery response is validated against your schema before it is returned. A malformed shape is retried internally and escalated once; you never receive a partial or best-effort object. If no conforming result can be produced you get a 422 rather than something your code would happily consume.\n\nSend the video as multipart `video` or as a `video_url` we can fetch. Videos up to 10 minutes, 2 GB per file (512 MB as multipart). Billed per second of video with a one-minute minimum, measured server-side with ffprobe from the bytes received — a duration you supply is never used. Runs synchronously; a short clip takes roughly half a minute.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"video":{"type":"string","format":"binary"},"video_url":{"type":"string","format":"uri"},"schema":{"type":"string","description":"JSON Schema or sketch, as JSON."},"instructions":{"type":"string"}},"required":["schema"]}},"application/json":{"schema":{"type":"object","properties":{"video_url":{"type":"string","format":"uri"},"schema":{},"instructions":{"type":"string"}},"required":["schema","video_url"]}}}},"responses":{"200":{"description":"Data conforming to your schema.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtractResponse"}}}},"402":{"description":"Out of minutes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"No conforming result could be produced.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/jobs/{id}":{"get":{"operationId":"getJob","summary":"Retrieve a past extraction","description":"Read back a job by id, including the extracted JSON, what it cost and which model ran. Scoped to the account that owns the key. Useful when a synchronous /v1/extract call was cut off by a client timeout after the work had already been done and paid for — the result is waiting rather than lost, and re-running the extraction would charge for it twice.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The job."}}}},"/v1/me":{"get":{"operationId":"getAccount","summary":"Account and remaining balance","description":"The account behind this key and how many minutes it has left. The balance is the sum of an append-only ledger, not a stored counter. Jobs stop when it reaches zero rather than overdrawing.","responses":{"200":{"description":"Your account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Account"}}}}}}},"/v1/mcp":{"post":{"operationId":"mcpEndpoint","summary":"Model Context Protocol endpoint (Streamable HTTP)","description":"The same capability as /v1/extract, exposed to agents as MCP tools: `extract`, and `recent_extractions` for reading back a job whose synchronous response was lost to a client timeout. Same key, same metering, same guarantees — a surface, not a separate product. Speaks JSON-RPC rather than REST, so point an MCP client at it rather than calling it by hand:\n\n    claude mcp add --transport http fovea <server>/v1/mcp --header \"Authorization: Bearer $FOVEA_KEY\"\n\n    codex mcp add fovea --url <server>/v1/mcp --bearer-token-env-var FOVEA_KEY","responses":{"200":{"description":"A JSON-RPC response, or an SSE stream."}}}}},"x-mcp":{"transport":"streamable-http","url":"https://api.fovea.run/v1/mcp","tools":["extract","recent_extractions"]}}