API
The Foveate REST API lets you create and manage decks from your own tools — a CRM, an automation platform like Zapier, an internal script, or an AI agent. Every endpoint lives under:
https://app.foveate.com/api/v1/
The API is versioned. Anything documented here under v1 is a compatibility promise; the web app's own internal routes are not.
Authentication
Requests are authenticated with an API key sent as a Bearer token:
Authorization: Bearer fv_live_<keyId>_<secret>
A key acts as one user — the person it was issued to. Requests run under their identity: their decks, their workspace, their credits, and the same access rules the app enforces. A key never bypasses permissions.
Creating a key
- Open Settings → Developer in the app.
- Choose New key, give it a name (for example, "Zapier"), and pick its permissions.
- Copy the token. You can copy it again later from the same page, and revoke it there when you're done with it.
Scopes
Each key carries one or more scopes. A request that needs a scope the key doesn't have returns 403 insufficient_scope.
| Scope | Allows |
|---|---|
decks:read | List decks and read a deck. |
decks:write | Create decks from an outline, rename, file, share, delete. |
decks:generate | Generate a deck from a prompt with AI (uses credits). |
A Firebase ID token is also accepted as the Bearer value, for scripts a user runs as themselves.
Conventions
- Responses. Success returns
{ "data": … }. Failure returns{ "error": { "code", "message", "requestId", "details"? } }. - Request IDs. Every response carries an
x-request-idheader. Quote it when reporting a problem. - Strict bodies. Bodies are JSON. Objects are strict — an unknown field is a
400 validation_error, withdetails: [{ path, message }]pointing at the offending field. - Idempotency. Send an
Idempotency-Keyheader (any string up to 200 characters) onPOST /decksandPOST /decks/generate. A repeat within 24 hours returns the original response withIdempotent-Replayed: true. A repeat while the first request is still running returns409 request_in_flight. - Rate limits. 60 requests per minute per key.
POST /decks/generateis limited to 5 per minute. Exceeding either returns429 rate_limitedwith aRetry-Afterheader.
Endpoints
GET /api/v1/me
Who the credential acts as. No scope required.
{
"data": {
"user": { "uid": "…", "email": "…", "displayName": "…" },
"auth": { "kind": "api_key", "keyId": "…", "name": "Zapier", "scopes": ["decks:read", "decks:write"] }
}
}
GET /api/v1/decks — decks:read
Your decks, newest-updated first.
Query parameters: limit (1–100, default 25) and cursor (the nextCursor from the previous page).
{ "data": { "decks": [ /* Deck */ ], "nextCursor": "deckId or null" } }
POST /api/v1/decks — decks:write
Create a deck from an outline (see The outline contract below). Returns 201 with the Deck.
curl -X POST https://app.foveate.com/api/v1/decks \
-H "Authorization: Bearer fv_live_…" \
-H "Idempotency-Key: order-8812" \
-H "Content-Type: application/json" \
-d '{
"name": "Riverside Library",
"pageSize": { "preset": "widescreen" },
"slides": [
{ "role": "cover", "content": { "title": "Riverside Library", "eyebrow": "Portland · 2027" } },
{ "role": "content", "content": { "eyebrow": "The Site", "title": "A working waterfront",
"body": ["The parcel sits between the rail line and the river."] } },
{ "role": "metrics", "content": { "title": "By the numbers",
"metrics": [{ "value": "42k", "label": "sq ft" }, { "value": "18", "label": "months" }] } },
{ "role": "closing", "content": { "title": "Next step", "subtitle": "Schedule the site walk" } }
]
}'
POST /api/v1/decks/generate — decks:generate
Generate a deck from a prompt with AI. The call is synchronous and can take up to about 120 seconds, so always send an Idempotency-Key.
Generation bills the key owner's credits at the deck generation rate. The response includes creditsCharged and the outline the model wrote. If the balance can't cover the deck, the API returns 402 insufficient_credits.
Body fields:
| Field | Notes |
|---|---|
prompt | Required. The brief. |
name | Optional deck title. |
type | proposal, sales, casestudy, meeting, pitch, competition, update, or partnership. |
industry | Steers templates and stock imagery. |
slideCount | 1–60. |
projectId | File the deck in a project. |
pageSize, theme, placeholderImages | Same as the deck-level options below. |
GET /api/v1/decks/{id} — decks:read
A deck you can see in the app — yours, shared with you, or in a workspace you own. Add ?include=slides to get the internal slide model (large; its shape is not part of the v1 contract). Unknown or invisible decks return 404.
PATCH /api/v1/decks/{id} — decks:write
Owner only. Fields: name, description, projectId (nullable), isPublic.
DELETE /api/v1/decks/{id} — decks:write
Owner only. Deleting a deck that's already gone succeeds.
GET /api/v1/schemas — no auth
The request contracts as JSON Schema (draft 2020-12), generated from the server's own validator, with a description on every field. Returns { version, roles, deckTypes, pageSizePresets, schemas: { createDeck, generateDeck, patchDeck, outlineSlide } }. Use it for structured-output prompting or client-side validation (see Using the JSON Schema below).
The Deck object
{
"id": "…",
"name": "…",
"description": "…",
"projectId": null,
"workspaceId": "…",
"isPublic": false,
"aspectRatio": "16:9",
"pageSize": { "preset": "widescreen", "w": 1920, "h": 1080, "phys": null },
"theme": { },
"slideCount": 4,
"createdAt": "ISO 8601",
"updatedAt": "ISO 8601",
"urls": {
"edit": "https://app.foveate.com/deck/{id}",
"share": "https://app.foveate.com/share/deck/{id}"
}
}
The outline contract
A slide is { "role", "content" }. The role picks the layout; the content is the text. Foveate lays the slide out — you never position elements.
| Role | Content fields |
|---|---|
cover | title, eyebrow (location · year, or the client) |
section | eyebrow, title, subtitle? |
statement | title (one bold sentence), subtitle? |
content | eyebrow, title, body[] (1–3 complete statements) |
metrics | title, metrics[{ value, label }] (2–3) |
timeline | title, phases[{ date, title }] (3–5) |
features | title, features[{ title, description }] (3) |
image | title or body[] as the caption; the image is a placeholder |
closing | title, subtitle (the call to action) |
Every slide may also carry notes — speaker notes shown only to the presenter, never rendered on the slide.
Limits: title up to 200 characters, eyebrow up to 60, subtitle up to 200, body up to 12 items of up to 1,000 characters each, and up to 120 slides per deck.
Deck-level options
| Field | Notes |
|---|---|
description | Shown alongside the deck. |
projectId | File the deck in a project. |
pageSize | { "preset": "widescreen" | "standard-4-3" | "square" | … } or { "w", "h" }. The full preset list is in GET /api/v1/schemas. |
theme | backgroundColor and accentColor as hex, plus headingFont, bodyFont, palette. |
industry | Steers templates and stock imagery. |
placeholderImages | Set false to leave image slots empty instead of filling them with stock photos. |
Writing a good outline
The schema guarantees an outline is valid. These rules make it good — and they are the same rules Foveate's own generator follows. If you're having an AI author the outline, paste this section into its instructions.
- Exactly one
cover, first. Exactly oneclosing, last. - One idea per slide. When there is more to say, add a slide — don't lengthen the body.
- Titles up to 5 words. Eyebrows up to 20 characters, specific and different on every slide — never "Overview" or "Section", never the title repeated.
- Body items are complete sentences in a confident, declarative voice. No fragments, no trailing "…", no bullets-as-sentences. Aim for 25–55 words per
contentslide. - Metric labels up to 24 characters. Phase titles up to 24. Feature titles up to 24, descriptions up to 90.
- Use
metrics,timeline, andfeaturesonly when the brief genuinely contains numbers, a schedule, or discrete capabilities. Don't invent figures or dates to fill a layout. - Names, numbers, places, and dates come from the brief or the source material. Never invent a client, a firm, or a statistic.
- Use only the fields listed for a role. The API rejects unknown fields.
- Aim for 8–12 slides for a proposal or pitch, or follow an explicit slide count when the brief gives one.
A reliable arc when the brief doesn't dictate one: cover → the context or problem → the idea in one statement → how it works (content or features) → proof (metrics, image) → delivery (timeline) → closing with the next step.
A worked example
Brief: "A proposal to convert a closed 40-acre warehouse district on the Oakland waterfront into production studios. 180k sq ft, 24 months to first occupancy, we keep 62% of the existing structure. Three moves: open the long shed into bays, raise a plinth above the flood line, keep the gantry cranes as canopy structure. Entitlements Q1 2027, first studios Q1 2029. Ask them to walk the site Thursday the 14th at 9am."
{
"name": "Harbor Point Studios",
"slides": [
{ "role": "cover", "content": { "title": "Harbor Point Studios", "eyebrow": "Oakland · 2027" } },
{ "role": "section", "content": { "eyebrow": "Context", "title": "A waterfront that stopped working",
"subtitle": "Forty acres of closed warehouses between the port and the neighborhood" } },
{ "role": "content", "content": { "eyebrow": "The Site", "title": "Between the cranes and the street",
"body": [
"The parcel sits where the rail spur meets the estuary, with the working port on one side and a residential grid on the other.",
"Every earlier proposal treated that seam as a problem to hide. We treat it as the reason to build here."
] } },
{ "role": "statement", "content": { "title": "Production space should face the city, not turn its back on it." } },
{ "role": "metrics", "content": { "title": "By the numbers",
"metrics": [
{ "value": "180k", "label": "sq ft of studio space" },
{ "value": "24", "label": "months to first occupancy" },
{ "value": "62%", "label": "of existing structure retained" }
] } },
{ "role": "features", "content": { "title": "Three moves",
"features": [
{ "title": "Open the shed", "description": "Cut the long warehouse into three bays with public passages between them." },
{ "title": "Raise the ground", "description": "A continuous plinth lifts studios above the flood line and becomes the street." },
{ "title": "Keep the cranes", "description": "Two gantry cranes stay as structure for the shade canopy." }
] } },
{ "role": "image", "content": { "title": "View from the estuary",
"body": ["The retained shed roof reads as one long horizon; new work sits beneath it."] } },
{ "role": "timeline", "content": { "title": "Delivery",
"phases": [
{ "date": "Q1 2027", "title": "Entitlements" },
{ "date": "Q3 2027", "title": "Shed stabilization" },
{ "date": "Q2 2028", "title": "Plinth and bays" },
{ "date": "Q1 2029", "title": "First studios open" }
] } },
{ "role": "closing", "content": { "title": "Walk the site with us",
"subtitle": "Thursday the 14th, 9am — we will bring the drawings" } }
]
}
Everything in it traces to the brief. Nothing was invented to fill a slot.
The validation loop
The API is strict, and its errors are addressed to the field, so an AI can self-correct in one round trip:
POST /api/v1/deckswith the outline (plusname, and optionallyindustry,pageSize,theme,placeholderImages).- On
400 validation_error, the body carrieserror.details: [{ path, message }]— for example{ "path": "slides.2.content.metrics.1", "message": "Unrecognized key: \"month\"" }. Feed those back to the model verbatim and ask for the corrected JSON. - On
201,data.urls.editopens the deck.
Always send an Idempotency-Key on the POST so a retried request can't create the deck twice.
Using the JSON Schema
GET /api/v1/schemas returns each contract as draft 2020-12 JSON Schema with additionalProperties: false throughout and a description on every field. Two ways to use it:
- Structured output. Pass
schemas.createDeckas the response schema to a model API that supports JSON-Schema-constrained output (a Claude toolinput_schema, OpenAIresponse_format). The model then can't emit an invalid shape, and the rules above only have to teach it to write well. - Client-side validation. Any JSON Schema validator (Ajv, for example) checks a body before it leaves your system.
If you have a brief rather than an outline, POST /api/v1/decks/generate runs Foveate's own outline model against these same rules and builds the deck in one call.
Error codes
| Status | Code |
|---|---|
| 400 | invalid_json, validation_error, invalid_cursor, invalid_idempotency_key |
| 401 | missing_token, invalid_api_key, invalid_token |
| 402 | insufficient_credits |
| 403 | insufficient_scope, forbidden |
| 404 | not_found |
| 409 | request_in_flight |
| 422 | empty_deck |
| 429 | rate_limited |
| 500 | internal_error |
| 502 / 504 | generation_* (provider error / timeout) |


