{"openapi":"3.1.0","info":{"title":"instadoc API","summary":"Fill .docx / .xlsx / .pdf / .pptx templates from JSON.","description":"Fill **.docx / .xlsx / .pptx / .pdf** templates from a JSON payload, and get\nback the rendered document.\n\n### Authentication\n\nEvery `/v1/*` endpoint requires a Bearer token in the `Authorization`\nheader. Use your API key — issued from the dashboard, the full key body\nis shown **once** at creation and never logged:\n\n```\nAuthorization: Bearer ido_live_<24-base62-chars>\n```\n\nStored only as an argon2id hash plus an 8-char prefix; lose it and you'll\nneed to rotate. Manage keys at\n[/dashboard/api-keys](/dashboard/api-keys) — key management itself is\ndashboard-only and not callable via API key.\n\n#### Try it from this page\n\nPaste your API key into the auth panel (top-right of any endpoint), then\nhit **Test request** to execute against the live backend. Authentication\npersists across visits in your browser's local storage; nothing is sent\nto our servers until you click Test.\n\n### Rate limits\n\nThe per-key ceiling depends on your plan; a flat **300 requests / minute / IP**\napplies on top. Whichever you hit first returns 429.\n\n| Plan    | Requests / min / API key | Renders / month |\n| ------- | -----------------------: | --------------: |\n| Starter |                       60 |             100 |\n| Growth  |                      600 |             500 |\n| Scale   |                    6,000 |           5,000 |\n\nEvery authenticated response includes `X-RateLimit-Limit`,\n`X-RateLimit-Remaining`, and `X-RateLimit-Reset` (Unix epoch) so clients\ncan back off proactively.\n\n### Errors\n\nErrors return a JSON envelope:\n\n```json\n{\n  \"error\": {\n    \"code\": \"plan_limit_exceeded\",\n    \"message\": \"...\",\n    \"request_id\": \"...\",\n    \"details\": null\n  },\n  \"detail\": \"...\"\n}\n```\n\nSwitch on the HTTP status code; the `error.code` is stable across releases\nwhile `detail` is human-readable and may change. The full error-code table\nlives at [/docs#errors](/docs#errors).","contact":{"name":"instadoc support","email":"support@instadoc.dev"},"version":"0.1.0"},"servers":[{"url":"https://api.instadoc.dev","description":"production"}],"paths":{"/v1/keys":{"get":{"tags":["API keys"],"summary":"List your API keys","description":"Return every API key owned by the current user.\n\nThe full key body is **never** returned by this endpoint — only the\n8-char `prefix` (for UI display) and metadata. The `status` field is\none of `\"active\"`, `\"revoked\"`, or `\"expired\"`.","operationId":"list_keys_v1_keys_get","responses":{"200":{"description":"Array of your keys, newest first.","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ApiKeyOut"},"type":"array","title":"Response List Keys V1 Keys Get"}}}},"401":{"description":"Missing or invalid Clerk session."}}},"post":{"tags":["API keys"],"summary":"Create an API key","description":"Issue a new API key for the current user.\n\nThe full key body (`ido_live_<24-chars>`) is returned **once** under\nthe `key` field. After this response we only persist the 8-char\nprefix and the argon2id hash — the full body cannot be recovered.\n\nOptional fields:\n- `name` (required) — a label for your records (e.g. `\"prod\"`,\n  `\"laptop\"`).\n- `scopes` — defaults to `[\"render\"]`. Reserved for future expansion.\n- `expires_at` — RFC 3339 timestamp. Past this point the key returns\n  401 on use; the row can then be hard-deleted via `DELETE /v1/keys/{id}`.\n\nAll creation events are audit-logged.","operationId":"create_key_v1_keys_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreate"}}},"required":true},"responses":{"201":{"description":"Key created. The `key` field is the full token — **this is the only time it is returned**. Save it immediately.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreated"}}}},"401":{"description":"Missing or invalid Clerk session."},"422":{"description":"Validation failed (e.g. name too long, unknown scope)."}}}},"/v1/keys/{key_id}":{"patch":{"tags":["API keys"],"summary":"Rename a key, change its scopes, or change its expiry","description":"Update metadata for an existing key without re-issuing it.\n\nAll three fields are optional — supply only the ones you want to change.\nThe actual key body is **not** rotated; use `POST /v1/keys/{id}/rotate`\nfor that. Audit-logged.","operationId":"update_key_v1_keys__key_id__patch","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Key Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyUpdate"}}}},"responses":{"200":{"description":"Updated key metadata.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyOut"}}}},"401":{"description":"Missing or invalid Clerk session."},"404":{"description":"Key not found or belongs to another user."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["API keys"],"summary":"Hard-delete a revoked or expired key","description":"Hard-delete a revoked or expired key.\n\nSECURITY: refuses to delete an active key — those must be revoked first\n(prevents a confused-deputy where deleting an actively-used key would\nsilently break the caller). render_logs.api_key_id is ON DELETE SET NULL\nso historical render rows survive with `api_key_id = NULL`. The audit\nlog is unaffected — it references the key by its target_id string.","operationId":"delete_key_v1_keys__key_id__delete","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Key Id"}}],"responses":{"204":{"description":"Key permanently deleted. Empty response body."},"401":{"description":"Missing or invalid Clerk session."},"404":{"description":"Key not found or belongs to another user."},"409":{"description":"Key is still active — revoke it first."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/keys/{key_id}/revoke":{"post":{"tags":["API keys"],"summary":"Revoke a key","description":"Permanently invalidate a key.\n\nFrom this point on, any `/v1/*` request bearing this token returns\n401 *\"Key revoked\"*. The row stays in your list (with\n`status: \"revoked\"`) so historical render logs keep their attribution.\nUse `DELETE /v1/keys/{id}` to hard-delete a revoked key. Audit-logged.","operationId":"revoke_key_v1_keys__key_id__revoke_post","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Key Id"}}],"responses":{"200":{"description":"Key marked revoked. Idempotent — re-revoking is a no-op.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyOut"}}}},"401":{"description":"Missing or invalid Clerk session."},"404":{"description":"Key not found or belongs to another user."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/keys/{key_id}/rotate":{"post":{"tags":["API keys"],"summary":"Rotate a key (revoke old, return new)","description":"Atomic rotate: revoke the old key, issue a new one with identical\nname / scopes / expiry. Both operations are committed in the same\ntransaction.\n\nUse this when a key may have leaked. The old key becomes 401 immediately;\nthe new key starts working as soon as you receive the response. Save\nthe returned `key` value — it's only shown here.","operationId":"rotate_key_v1_keys__key_id__rotate_post","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Key Id"}}],"responses":{"200":{"description":"Old key revoked; new key returned with the same name, scopes and expiry. The `key` field is the full body — **shown once, never again**.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreated"}}}},"401":{"description":"Missing or invalid Clerk session."},"404":{"description":"Key not found or belongs to another user."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/templates":{"get":{"tags":["Templates"],"summary":"List your templates","description":"Every template you own, excluding soft-deleted rows.","operationId":"list_templates_v1_templates_get","responses":{"200":{"description":"Array of your non-deleted templates, newest first.","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/TemplateOut"},"type":"array","title":"Response List Templates V1 Templates Get"}}}},"401":{"description":"Missing or invalid Clerk session."}},"security":[{"API Key":[]}]},"post":{"tags":["Templates"],"summary":"Upload a template","description":"Upload a `.docx`, `.xlsx`, `.pdf`, or `.pptx` file. Max 10 MiB.\n\n**Multipart form fields**:\n- `file` (required) — the binary content.\n- `name` (optional) — display name. Defaults to the filename without\n  extension if omitted.\n\n**Validation pipeline** (any failure → 415):\n1. Extension must be `docx` / `xlsx` / `pdf` / `pptx`. Macro variants\n   (`.docm`, `.xlsm`, `.pptm`, `.xlsb`) are rejected.\n2. MIME sniff via `libmagic` on the actual bytes.\n3. PDFs must start with `%PDF-`.\n4. ZIP-based formats are opened and inspected: archive must contain the\n   expected marker (`word/document.xml`, `xl/workbook.xml`,\n   `ppt/presentation.xml`) and **must not** contain `vbaProject.bin`\n   anywhere (catches macros hidden behind an innocent extension).\n5. The user-supplied filename is **never** used on disk — files are\n   renamed to `{user_id}/{template_id}.{ext}`.\n\nSHA-256 is computed on upload and recorded for integrity / dedup.","operationId":"upload_template_v1_templates_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_template_v1_templates_post"}}},"required":true},"responses":{"201":{"description":"Template stored and metadata returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateOut"}}}},"401":{"description":"Missing or invalid Clerk session."},"413":{"description":"File exceeds the 10 MiB cap."},"415":{"description":"File rejected: unsupported extension, MIME mismatch, macro-enabled (vbaProject.bin found), or missing the expected zip structure."},"502":{"description":"Backend storage (DO Spaces / local fs) rejected the put."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"API Key":[]}]}},"/v1/templates/{template_id}":{"get":{"tags":["Templates"],"summary":"Fetch one template","description":"Metadata for a single template — including its detected\n`placeholder_schema` (populated once the render pipeline lands).","operationId":"get_template_v1_templates__template_id__get","security":[{"API Key":[]}],"parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Template Id"}}],"responses":{"200":{"description":"Full template metadata.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateOut"}}}},"401":{"description":"Missing or invalid Clerk session."},"404":{"description":"Template not found, soft-deleted, or belongs to another user. (We return 404 rather than 403 so existence isn't leaked across accounts.)"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Templates"],"summary":"Delete a template","operationId":"delete_template_v1_templates__template_id__delete","security":[{"API Key":[]}],"parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Template Id"}}],"responses":{"204":{"description":"Soft-deleted; storage object removed. Empty body."},"401":{"description":"Missing or invalid Clerk session."},"404":{"description":"Template not found or belongs to another user."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Templates"],"summary":"Rename a template","description":"Change a template's display name. The `template_id`, file content,\nsha256, doc_type and storage_key are all unchanged — any existing\n`/v1/render` calls referencing this template continue to work.","operationId":"rename_template_v1_templates__template_id__patch","security":[{"API Key":[]}],"parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Template Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateRename"}}}},"responses":{"200":{"description":"Updated metadata.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateOut"}}}},"401":{"description":"Missing or invalid Clerk session."},"404":{"description":"Template not found or belongs to another user."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/templates/{template_id}/file":{"put":{"tags":["Templates"],"summary":"Replace a template's file (keeps the same template_id)","description":"Upload a new file under the same `template_id`.\n\nRuns the same security / MIME / macro / zip-structure validation as\n`POST /v1/templates`. The new file must have the **same extension**\nas the existing template — switching doc_type isn't supported (would\ninvalidate any existing render integrations). To change format,\ndelete and re-upload.","operationId":"replace_template_file_v1_templates__template_id__file_put","security":[{"API Key":[]}],"parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Template Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_replace_template_file_v1_templates__template_id__file_put"}}}},"responses":{"200":{"description":"File replaced. Same `template_id`, new sha256 / size / placeholder_schema. Existing render calls continue to work and immediately use the new content.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateOut"}}}},"401":{"description":"Missing or invalid Clerk session."},"404":{"description":"Template not found or belongs to another user."},"413":{"description":"File exceeds the 10 MiB cap."},"415":{"description":"Same validation pipeline as upload."},"502":{"description":"Storage backend rejected the put."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/templates/{template_id}/versions":{"get":{"tags":["Templates"],"summary":"List a template's version history","description":"Return every immutable snapshot for the template, newest first.\n\nA snapshot is written on initial upload, on every successful\n`PUT /v1/templates/{id}/file`, and on every successful restore. The\n`is_current` flag marks the snapshot whose sha256 matches the\ntemplate's current bytes — that's what `/v1/render` will use today.","operationId":"list_template_versions_v1_templates__template_id__versions_get","security":[{"API Key":[]}],"parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Template Id"}}],"responses":{"200":{"description":"All snapshots, newest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TemplateVersionOut"},"title":"Response List Template Versions V1 Templates  Template Id  Versions Get"}}}},"401":{"description":"Missing or invalid Clerk session."},"404":{"description":"Template not found or belongs to another user."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/templates/{template_id}/versions/{version}/restore":{"post":{"tags":["Templates"],"summary":"Restore a prior version as the current template content","description":"Make `version` the current bytes for this template.\n\nReads the snapshot from storage, writes it back to the canonical\nstorage key, mirrors the snapshot's metadata onto the template row,\nand records a *new* version pointing at the restored content. The\nolder version snapshot stays in place — restoring is non-destructive.","operationId":"restore_template_version_v1_templates__template_id__versions__version__restore_post","security":[{"API Key":[]}],"parameters":[{"name":"template_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Template Id"}},{"name":"version","in":"path","required":true,"schema":{"type":"integer","title":"Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateVersionRestore"}}}},"responses":{"200":{"description":"The named version is copied back to the canonical storage key and a new snapshot is recorded so the history stays linear (no destructive moves). Same `template_id` — existing render integrations are unaffected.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateOut"}}}},"401":{"description":"Missing or invalid Clerk session."},"404":{"description":"Template or version not found, or owned by another user."},"502":{"description":"Storage backend rejected the put."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/render":{"post":{"tags":["Render"],"summary":"Fill a template with a JSON payload","description":"Fill a template or composition with a JSON payload.\n\n**Query string** (exactly one required):\n- `template_id` — UUID of a single template you own.\n- `composition_id` — UUID of a composition you own; each block is\n  rendered with the same payload, then merged into one output.\n\n**Body**: JSON object. Top-level keys are addressable inside templates\nvia Jinja2 syntax. For composition renders, the payload may include:\n- `chapters: [{\"type\": \"<slug>\", ...}, ...]` — when present, the\n  array determines block ordering AND inclusion; each entry's\n  `type` (or `name`) is matched to a block's slug. Without it, the\n  composition's `position` order is used.\n- `blocks: { \"<slug>\": { ...overrides } }` — per-block overrides\n  merged on top of each block's resolved context.\n\nSize cap: `MAX_PAYLOAD_BYTES` (100 KiB by default).","operationId":"render_endpoint_v1_render_post","security":[{"API Key":[]}],"parameters":[{"name":"template_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"UUID of an uploaded template you own. Mutually exclusive with composition_id.","title":"Template Id"},"description":"UUID of an uploaded template you own. Mutually exclusive with composition_id."},{"name":"composition_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"UUID of a composition you own. Renders every block in the composition into a single output. Mutually exclusive with template_id.","title":"Composition Id"},"description":"UUID of a composition you own. Renders every block in the composition into a single output. Mutually exclusive with template_id."},{"name":"format","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional output format override. Pass `pdf` to convert a docx/xlsx/pptx render to PDF via the Gotenberg sidecar. Omit (or pass `native`) to return the template's native format. Requires `GOTENBERG_URL` — returns 501 otherwise.","title":"Format"},"description":"Optional output format override. Pass `pdf` to convert a docx/xlsx/pptx render to PDF via the Gotenberg sidecar. Omit (or pass `native`) to return the template's native format. Requires `GOTENBERG_URL` — returns 501 otherwise."},{"name":"orientation","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional PDF orientation override: `portrait` or `landscape`. Only meaningful when `format=pdf`; ignored otherwise. When omitted, LibreOffice uses the source document's declared orientation.","title":"Orientation"},"description":"Optional PDF orientation override: `portrait` or `landscape`. Only meaningful when `format=pdf`; ignored otherwise. When omitted, LibreOffice uses the source document's declared orientation."},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"description":"Opaque client-supplied identifier (up to 128 chars). Replaying the same call with the same key inside the TTL window (default 24h) returns the cached prior response instead of re-rendering. Required when REDIS_URL is unset is a no-op (best-effort).","title":"Idempotency-Key"},"description":"Opaque client-supplied identifier (up to 128 chars). Replaying the same call with the same key inside the TTL window (default 24h) returns the cached prior response instead of re-rendering. Required when REDIS_URL is unset is a no-op (best-effort)."}],"responses":{"200":{"description":"The rendered document as a binary stream. `Content-Type` is the format's canonical MIME. If an `Idempotency-Key` header was supplied and matched a prior call, the response is replayed unchanged with the extra header `Idempotent-Replay: true`.","content":{"application/json":{"schema":{}},"application/vnd.openxmlformats-officedocument.wordprocessingml.document":{},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{},"application/pdf":{},"application/vnd.openxmlformats-officedocument.presentationml.presentation":{}}},"400":{"description":"Invalid JSON body or template_id not a UUID."},"401":{"description":"Missing, malformed, revoked, or expired API key."},"404":{"description":"Template not found, soft-deleted, or owned by another user."},"413":{"description":"Payload exceeds `MAX_PAYLOAD_BYTES`."},"423":{"description":"Account suspended."},"429":{"description":"Rate limit exceeded — see `X-RateLimit-Reset`."},"500":{"description":"Rendering itself failed (bad template, runtime sandbox error)."},"501":{"description":"Rendering this doc_type isn't implemented. Today only `pdf` template source returns 501 — `docx`, `xlsx`, and `pptx` are all supported. Also returned when `?format=pdf` is requested with no `GOTENBERG_URL` configured."},"504":{"description":"Render exceeded the 15s wall-clock timeout."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me":{"get":{"tags":["Usage"],"summary":"Identity + plan summary for the authenticated dashboard user","description":"The dashboard's source of truth for plan + limits.\n\nThe plan slug stored on the user row is normalised through\n`plans.normalise_slug` so empty / \"free_user\" / unknown values all\ncollapse to \"free\". Adding a new plan tier requires editing\n`app/services/plans.py`; routes pick it up automatically.","operationId":"whoami_v1_me_get","responses":{"200":{"description":"Caller profile + the resolved plan limits.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeOut"}}}},"401":{"description":"Missing or invalid Clerk session."}},"security":[{"API Key":[]}]}},"/v1/usage":{"get":{"tags":["Usage"],"summary":"Aggregate render activity for your account","description":"Return an aggregate view of your render activity.\n\nThe breakdown by template returns up to **10** rows ordered by render\ncount; finer-grained data lives in `/v1/render-logs`.\n\n`period` is one of:\n- `current_month` — first day of the current calendar month (UTC) to now.\n- `last_30d` — rolling 30 days.","operationId":"get_usage_v1_usage_get","security":[{"API Key":[]}],"parameters":[{"name":"period","in":"query","required":false,"schema":{"enum":["current_month","last_30d"],"type":"string","default":"current_month","title":"Period"}}],"responses":{"200":{"description":"Aggregate counters for the requested window.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageResponse"}}}},"401":{"description":"Missing or invalid Clerk session."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/render-logs":{"get":{"tags":["Usage"],"summary":"Paginated audit trail of your render calls","description":"List render attempts for the authenticated user.\n\nFilterable by `template_id`, `status` (success/error), and a\n`since`/`until` window. Pagination is a cursor on (created_at, id) so\nrows that share a timestamp don't get skipped.\n\nThe body of the original request is **never** stored, so it isn't\nreturned here. Only metadata: status, error code, duration, output\nsize, request id.","operationId":"list_render_logs_v1_render_logs_get","security":[{"API Key":[]}],"parameters":[{"name":"template_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"Filter to one template.","title":"Template Id"},"description":"Filter to one template."},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"enum":["success","error"],"type":"string"},{"type":"null"}],"description":"Filter by outcome — \"success\" or \"error\".","title":"Status"},"description":"Filter by outcome — \"success\" or \"error\"."},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Inclusive lower bound on created_at.","title":"Since"},"description":"Inclusive lower bound on created_at."},{"name":"until","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Exclusive upper bound on created_at.","title":"Until"},"description":"Exclusive upper bound on created_at."},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque cursor returned by a previous call.","title":"Cursor"},"description":"Opaque cursor returned by a previous call."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"Page size — max 200.","default":50,"title":"Limit"},"description":"Page size — max 200."}],"responses":{"200":{"description":"Up to `limit` log entries newest-first, plus a `next_cursor` to fetch the following page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderLogsResponse"}}}},"400":{"description":"Malformed cursor."},"401":{"description":"Missing or invalid Clerk session."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/stats/public":{"get":{"tags":["Usage"],"summary":"Aggregate counters for the marketing homepage.","description":"Return platform-wide aggregate counts.\n\nUnauthenticated. The numbers are intentionally non-attributable:\nnothing about specific users, templates, or renders is exposed.\nPer-IP rate-limited via the same bucket the rest of /v1 uses to\ndiscourage hammering this from a scraper.","operationId":"public_stats_v1_stats_public_get","responses":{"200":{"description":"Coarse-grained, non-PII counts. Safe to cache for minutes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicStats"}}}}}}},"/v1/themes":{"get":{"tags":["Themes"],"summary":"List your themes","operationId":"list_themes_v1_themes_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ThemeOut"},"type":"array","title":"Response List Themes V1 Themes Get"}}}}},"security":[{"API Key":[]}]},"post":{"tags":["Themes"],"summary":"Upload a theme","description":"Upload a `.pptx` (or `.potx` saved as `.pptx`) to use as a theme.\n\nFuture doc_types are allowed by the schema but only `pptx` exercises\nmaster-signature enforcement today.","operationId":"create_theme_v1_themes_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_create_theme_v1_themes_post"}}},"required":true},"responses":{"201":{"description":"Theme stored. The master_signature is computed for pptx.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThemeOut"}}}},"400":{"description":"Missing or empty name."},"413":{"description":"File exceeds the 10 MiB cap."},"415":{"description":"Same MIME / structure / macro validation as templates."},"502":{"description":"Storage backend rejected the put."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"API Key":[]}]}},"/v1/themes/{theme_id}":{"get":{"tags":["Themes"],"summary":"Fetch one of your themes","operationId":"get_theme_v1_themes__theme_id__get","security":[{"API Key":[]}],"parameters":[{"name":"theme_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Theme Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThemeOut"}}}},"404":{"description":"Theme not found or belongs to another user."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Themes"],"summary":"Update theme metadata","operationId":"update_theme_v1_themes__theme_id__patch","security":[{"API Key":[]}],"parameters":[{"name":"theme_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Theme Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThemeUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThemeOut"}}}},"400":{"description":"Empty name."},"404":{"description":"Theme not found or belongs to another user."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Themes"],"summary":"Delete a theme","operationId":"delete_theme_v1_themes__theme_id__delete","security":[{"API Key":[]}],"parameters":[{"name":"theme_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Theme Id"}}],"responses":{"204":{"description":"Soft-deleted; storage object removed. Empty body."},"404":{"description":"Theme not found or belongs to another user."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/compositions":{"get":{"tags":["Compositions"],"summary":"List your compositions","operationId":"list_compositions_v1_compositions_get","responses":{"200":{"description":"Your non-deleted compositions, newest first.","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/CompositionOut"},"type":"array","title":"Response List Compositions V1 Compositions Get"}}}},"401":{"description":"Missing or invalid Clerk session."}},"security":[{"API Key":[]}]},"post":{"tags":["Compositions"],"summary":"Create a composition","description":"Create an empty composition. Add blocks with `POST /v1/compositions/{id}/blocks`.","operationId":"create_composition_v1_compositions_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompositionCreate"}}},"required":true},"responses":{"201":{"description":"Composition created. Add blocks via POST /v1/compositions/{id}/blocks.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompositionOut"}}}},"401":{"description":"Missing or invalid Clerk session."},"404":{"description":"Referenced theme_id is not yours."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"API Key":[]}]}},"/v1/compositions/{composition_id}":{"get":{"tags":["Compositions"],"summary":"Fetch a composition with its blocks","description":"The composition row plus its blocks in `position` order. The blocks\nare returned inline so the dashboard only needs one round-trip.","operationId":"get_composition_v1_compositions__composition_id__get","security":[{"API Key":[]}],"parameters":[{"name":"composition_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Composition Id"}}],"responses":{"200":{"description":"Full composition + ordered blocks.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompositionWithBlocksOut"}}}},"401":{"description":"Missing or invalid Clerk session."},"404":{"description":"Composition not found or not yours."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Compositions"],"summary":"Update composition metadata","description":"Rename or rebind the theme. `doc_type` is immutable — to change it,\ndelete and recreate.","operationId":"update_composition_v1_compositions__composition_id__patch","security":[{"API Key":[]}],"parameters":[{"name":"composition_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Composition Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompositionUpdate"}}}},"responses":{"200":{"description":"Updated composition.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompositionOut"}}}},"401":{"description":"Missing or invalid Clerk session."},"404":{"description":"Composition or theme not found."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Compositions"],"summary":"Delete a composition (soft)","operationId":"delete_composition_v1_compositions__composition_id__delete","security":[{"API Key":[]}],"parameters":[{"name":"composition_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Composition Id"}}],"responses":{"204":{"description":"Soft-deleted; existing render_logs keep their composition_id pointer."},"401":{"description":"Missing or invalid Clerk session."},"404":{"description":"Composition not found or not yours."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/compositions/{composition_id}/blocks":{"post":{"tags":["Compositions"],"summary":"Add a block to a composition","description":"Append a block. The template must be owned by you and share the\ncomposition's doc_type. `name` must be unique within the composition;\n`position` may not collide with an existing block — re-number the\nothers first if you need to insert.","operationId":"create_block_v1_compositions__composition_id__blocks_post","security":[{"API Key":[]}],"parameters":[{"name":"composition_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Composition Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompositionBlockCreate"}}}},"responses":{"201":{"description":"Block added.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompositionBlockOut"}}}},"400":{"description":"doc_type mismatch, slug taken, or position collision."},"401":{"description":"Missing or invalid Clerk session."},"404":{"description":"Composition or template not found / not yours."},"409":{"description":"Composition has reached the 50-block cap."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Compositions"],"summary":"List blocks in a composition","operationId":"list_blocks_v1_compositions__composition_id__blocks_get","security":[{"API Key":[]}],"parameters":[{"name":"composition_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Composition Id"}}],"responses":{"200":{"description":"Blocks ordered by position ascending.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CompositionBlockOut"},"title":"Response List Blocks V1 Compositions  Composition Id  Blocks Get"}}}},"401":{"description":"Missing or invalid Clerk session."},"404":{"description":"Composition not found or not yours."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/compositions/{composition_id}/blocks/{block_id}":{"patch":{"tags":["Compositions"],"summary":"Update a block","operationId":"update_block_v1_compositions__composition_id__blocks__block_id__patch","security":[{"API Key":[]}],"parameters":[{"name":"composition_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Composition Id"}},{"name":"block_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Block Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompositionBlockUpdate"}}}},"responses":{"200":{"description":"Updated block.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompositionBlockOut"}}}},"400":{"description":"doc_type mismatch, slug taken, or position collision."},"401":{"description":"Missing or invalid Clerk session."},"404":{"description":"Composition, block, or template not found / not yours."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Compositions"],"summary":"Remove a block","operationId":"delete_block_v1_compositions__composition_id__blocks__block_id__delete","security":[{"API Key":[]}],"parameters":[{"name":"composition_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Composition Id"}},{"name":"block_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Block Id"}}],"responses":{"204":{"description":"Block removed. Positions of other blocks are unchanged."},"401":{"description":"Missing or invalid Clerk session."},"404":{"description":"Composition or block not found."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/healthz":{"get":{"tags":["Health"],"summary":"Healthz","operationId":"healthz_healthz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Healthz Healthz Get"}}}}}}}},"components":{"schemas":{"ApiKeyCreate":{"properties":{"name":{"type":"string","maxLength":80,"minLength":1,"title":"Name","description":"A label only you see (e.g. `prod`, `laptop`).","examples":["prod"]},"scopes":{"items":{"type":"string","const":"render"},"type":"array","title":"Scopes","description":"Permissions granted to this key. Default `['render']`. Reserved for future expansion; today there's only one scope."},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At","description":"Optional RFC 3339 timestamp at which the key auto-expires. Past this point the key returns 401 on use; it can then be hard-deleted via `DELETE /v1/keys/{id}`."}},"additionalProperties":false,"type":"object","required":["name"],"title":"ApiKeyCreate"},"ApiKeyCreated":{"properties":{"id":{"type":"string","format":"uuid","title":"Id","description":"Stable identifier for this key row."},"name":{"type":"string","title":"Name","description":"Display label."},"prefix":{"type":"string","title":"Prefix","description":"First 8 chars of the random body (after `ido_live_`). Safe to show in UIs — it's not enough on its own to reconstruct the key.","examples":["aB3cD4eF"]},"display":{"type":"string","title":"Display","description":"Pre-formatted `ido_live_<prefix>…` string for UI display.","examples":["ido_live_aB3cD4eF…"]},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes","description":"Permissions granted to this key."},"created_at":{"type":"string","format":"date-time","title":"Created At"},"last_used_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Used At","description":"Updated on use, debounced to at most once per `API_KEY_LAST_USED_DEBOUNCE_SEC` window."},"revoked_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Revoked At"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"status":{"type":"string","enum":["active","revoked","expired"],"title":"Status","description":"Derived: `active` if not revoked and not expired, otherwise the specific terminal state."},"key":{"type":"string","title":"Key","description":"The full API key body. **Shown once on creation/rotation; never returned again.** Store it in your secrets manager immediately.","examples":["ido_live_aB3cD4eFgH7iJ8kL9mN0pQ1r"]}},"type":"object","required":["id","name","prefix","display","scopes","created_at","last_used_at","revoked_at","expires_at","status","key"],"title":"ApiKeyCreated","description":"Returned ONCE on POST /v1/keys — includes the full key body."},"ApiKeyOut":{"properties":{"id":{"type":"string","format":"uuid","title":"Id","description":"Stable identifier for this key row."},"name":{"type":"string","title":"Name","description":"Display label."},"prefix":{"type":"string","title":"Prefix","description":"First 8 chars of the random body (after `ido_live_`). Safe to show in UIs — it's not enough on its own to reconstruct the key.","examples":["aB3cD4eF"]},"display":{"type":"string","title":"Display","description":"Pre-formatted `ido_live_<prefix>…` string for UI display.","examples":["ido_live_aB3cD4eF…"]},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes","description":"Permissions granted to this key."},"created_at":{"type":"string","format":"date-time","title":"Created At"},"last_used_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Used At","description":"Updated on use, debounced to at most once per `API_KEY_LAST_USED_DEBOUNCE_SEC` window."},"revoked_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Revoked At"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"status":{"type":"string","enum":["active","revoked","expired"],"title":"Status","description":"Derived: `active` if not revoked and not expired, otherwise the specific terminal state."}},"type":"object","required":["id","name","prefix","display","scopes","created_at","last_used_at","revoked_at","expires_at","status"],"title":"ApiKeyOut","description":"Listed/returned view of a key. Never includes the full key body."},"ApiKeyUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":80,"minLength":1},{"type":"null"}],"title":"Name"},"scopes":{"anyOf":[{"items":{"type":"string","const":"render"},"type":"array"},{"type":"null"}],"title":"Scopes"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"}},"additionalProperties":false,"type":"object","title":"ApiKeyUpdate"},"Body_create_theme_v1_themes_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"}},"type":"object","required":["file"],"title":"Body_create_theme_v1_themes_post"},"Body_replace_template_file_v1_templates__template_id__file_put":{"properties":{"file":{"type":"string","format":"binary","title":"File"}},"type":"object","required":["file"],"title":"Body_replace_template_file_v1_templates__template_id__file_put"},"Body_upload_template_v1_templates_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"}},"type":"object","required":["file"],"title":"Body_upload_template_v1_templates_post"},"CompositionBlockCreate":{"properties":{"template_id":{"type":"string","format":"uuid","title":"Template Id"},"position":{"type":"integer","minimum":0.0,"title":"Position"},"name":{"type":"string","maxLength":80,"minLength":1,"pattern":"^[a-z0-9][a-z0-9_-]{0,79}$","title":"Name"},"include_when":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}],"title":"Include When"},"data_path":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Data Path"}},"additionalProperties":false,"type":"object","required":["template_id","position","name"],"title":"CompositionBlockCreate","description":"Block inside a composition.\n\n`position` is the render order (0-indexed). `name` is a slug used by the\nrender payload to address per-block overrides (`blocks: { \"<slug>\": {...} }`)\nand to match payload `chapters[].type` for per-render ordering.\n`include_when` is a Jinja expression evaluated in the sandbox.\n`data_path` is a dotted path into the render payload that becomes the\nblock's context (default: full payload)."},"CompositionBlockOut":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"composition_id":{"type":"string","format":"uuid","title":"Composition Id"},"template_id":{"type":"string","format":"uuid","title":"Template Id"},"position":{"type":"integer","title":"Position"},"name":{"type":"string","title":"Name"},"include_when":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Include When"},"data_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Data Path"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","composition_id","template_id","position","name","include_when","data_path","created_at"],"title":"CompositionBlockOut"},"CompositionBlockUpdate":{"properties":{"template_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Template Id"},"position":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Position"},"name":{"anyOf":[{"type":"string","pattern":"^[a-z0-9][a-z0-9_-]{0,79}$"},{"type":"null"}],"title":"Name"},"include_when":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}],"title":"Include When"},"data_path":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Data Path"}},"additionalProperties":false,"type":"object","title":"CompositionBlockUpdate"},"CompositionCreate":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name"},"doc_type":{"type":"string","enum":["docx","xlsx","pdf","pptx"],"title":"Doc Type"},"theme_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Theme Id"}},"additionalProperties":false,"type":"object","required":["name","doc_type"],"title":"CompositionCreate"},"CompositionOut":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"name":{"type":"string","title":"Name"},"doc_type":{"type":"string","enum":["docx","xlsx","pdf","pptx"],"title":"Doc Type"},"theme_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Theme Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","name","doc_type","theme_id","created_at"],"title":"CompositionOut"},"CompositionUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name"},"theme_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Theme Id"}},"additionalProperties":false,"type":"object","title":"CompositionUpdate"},"CompositionWithBlocksOut":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"name":{"type":"string","title":"Name"},"doc_type":{"type":"string","enum":["docx","xlsx","pdf","pptx"],"title":"Doc Type"},"theme_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Theme Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"blocks":{"items":{"$ref":"#/components/schemas/CompositionBlockOut"},"type":"array","title":"Blocks"}},"type":"object","required":["id","name","doc_type","theme_id","created_at","blocks"],"title":"CompositionWithBlocksOut","description":"Composition + its ordered blocks. Returned by GET /v1/compositions/{id}."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"MeOut":{"properties":{"id":{"type":"string","title":"Id"},"email":{"type":"string","title":"Email"},"role":{"type":"string","enum":["user","admin"],"title":"Role"},"is_suspended":{"type":"boolean","title":"Is Suspended"},"plan":{"$ref":"#/components/schemas/PlanInfo"},"storage_used_bytes":{"type":"integer","minimum":0.0,"title":"Storage Used Bytes","description":"Sum of size_bytes across the user's active templates."},"template_count":{"type":"integer","minimum":0.0,"title":"Template Count","description":"Number of active (non soft-deleted) templates the user owns."},"plan_synced_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Plan Synced At","description":"When the plan was last updated by a Clerk Billing webhook."}},"type":"object","required":["id","email","role","is_suspended","plan","storage_used_bytes","template_count","plan_synced_at"],"title":"MeOut","description":"Identity + plan summary for the authenticated dashboard user."},"PlanInfo":{"properties":{"slug":{"type":"string","title":"Slug","description":"Normalised plan slug — one of starter / growth / scale today."},"display_name":{"type":"string","title":"Display Name"},"max_templates":{"type":"integer","minimum":0.0,"title":"Max Templates","description":"Cap on active templates. 0 means unlimited."},"monthly_render_quota":{"type":"integer","minimum":0.0,"title":"Monthly Render Quota","description":"Successful renders per calendar month. 0 means unlimited."},"rate_limit_per_min":{"type":"integer","minimum":0.0,"title":"Rate Limit Per Min"},"storage_quota_bytes":{"type":"integer","minimum":0.0,"title":"Storage Quota Bytes","description":"Total bytes across the user's active templates allowed by the plan. 0 means unlimited."}},"type":"object","required":["slug","display_name","max_templates","monthly_render_quota","rate_limit_per_min","storage_quota_bytes"],"title":"PlanInfo"},"PublicStats":{"properties":{"templates_total":{"type":"integer","minimum":0.0,"title":"Templates Total","description":"Templates ever uploaded across all users (incl. soft-deleted)."},"renders_success_total":{"type":"integer","minimum":0.0,"title":"Renders Success Total","description":"Successful renders across all users."},"users_active":{"type":"integer","minimum":0.0,"title":"Users Active","description":"Currently active (not suspended, not deleted) users."}},"type":"object","required":["templates_total","renders_success_total","users_active"],"title":"PublicStats","description":"Coarse aggregate counters for the marketing homepage.\n\nUnauthenticated; nothing user-attributable is exposed."},"RenderLogEntry":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"template_id":{"type":"string","format":"uuid","title":"Template Id"},"template_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Template Name"},"api_key_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Api Key Id"},"api_key_prefix":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key Prefix"},"status":{"type":"string","enum":["success","error"],"title":"Status"},"error_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Code"},"duration_ms":{"type":"integer","title":"Duration Ms"},"bytes_out":{"type":"integer","title":"Bytes Out"},"request_id":{"type":"string","title":"Request Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","template_id","status","duration_ms","bytes_out","request_id","created_at"],"title":"RenderLogEntry"},"RenderLogsResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/RenderLogEntry"},"type":"array","title":"Items"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Opaque cursor for the next page. Pass it back as the `cursor` query parameter. `null` when there is no further page."}},"type":"object","required":["items"],"title":"RenderLogsResponse"},"TemplateOut":{"properties":{"id":{"type":"string","format":"uuid","title":"Id","description":"Stable template identifier; use this in render calls."},"name":{"type":"string","title":"Name","description":"Display label (defaulted to the filename stem if not supplied at upload)."},"doc_type":{"type":"string","enum":["docx","xlsx","pdf","pptx"],"title":"Doc Type","description":"Inferred from the file extension and verified against the MIME sniff."},"size_bytes":{"type":"integer","minimum":0.0,"title":"Size Bytes","description":"Size on storage. Capped at `MAX_TEMPLATE_BYTES` (10 MiB by default)."},"sha256":{"type":"string","maxLength":64,"minLength":64,"title":"Sha256","description":"SHA-256 of the uploaded bytes — useful for dedup and integrity checks.","examples":["e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"]},"placeholder_schema":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Placeholder Schema","description":"Auto-detected skeleton of the JSON payload the template expects. Walked from the template's Jinja AST at upload time. Useful as the default editor content in dashboard preview UIs. Always `null` for non-docx templates today."},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","name","doc_type","size_bytes","sha256","placeholder_schema","created_at"],"title":"TemplateOut"},"TemplateRename":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name"}},"additionalProperties":false,"type":"object","required":["name"],"title":"TemplateRename","description":"Rename a template via PATCH /v1/templates/{id}."},"TemplateVersionOut":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"version":{"type":"integer","minimum":1.0,"title":"Version","description":"1-based, monotonically increasing per template."},"size_bytes":{"type":"integer","minimum":0.0,"title":"Size Bytes"},"sha256":{"type":"string","maxLength":64,"minLength":64,"title":"Sha256"},"placeholder_schema":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Placeholder Schema"},"change_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Change Note","description":"Optional human-readable note attached when the version was written."},"created_at":{"type":"string","format":"date-time","title":"Created At"},"is_current":{"type":"boolean","title":"Is Current","description":"True for the version that matches the template's current sha256 — i.e. the bytes /v1/render would return today."}},"type":"object","required":["id","version","size_bytes","sha256","created_at","is_current"],"title":"TemplateVersionOut","description":"A single immutable snapshot in a template's history."},"TemplateVersionRestore":{"properties":{"change_note":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Change Note"}},"additionalProperties":false,"type":"object","title":"TemplateVersionRestore","description":"POST /v1/templates/{id}/versions/{version}/restore body."},"ThemeOut":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"name":{"type":"string","title":"Name"},"doc_type":{"type":"string","enum":["docx","xlsx","pdf","pptx"],"title":"Doc Type"},"storage_key":{"type":"string","title":"Storage Key"},"master_signature":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Master Signature"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","name","doc_type","storage_key","master_signature","created_at"],"title":"ThemeOut"},"ThemeUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Name"}},"additionalProperties":false,"type":"object","title":"ThemeUpdate"},"UsageByDocType":{"properties":{"doc_type":{"type":"string","title":"Doc Type"},"renders":{"type":"integer","title":"Renders"}},"type":"object","required":["doc_type","renders"],"title":"UsageByDocType"},"UsageByTemplate":{"properties":{"template_id":{"type":"string","format":"uuid","title":"Template Id"},"template_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Template Name"},"renders":{"type":"integer","title":"Renders"}},"type":"object","required":["template_id","renders"],"title":"UsageByTemplate"},"UsageCounters":{"properties":{"renders_total":{"type":"integer","title":"Renders Total","description":"All render attempts in the window."},"renders_success":{"type":"integer","title":"Renders Success","description":"2xx renders."},"renders_error":{"type":"integer","title":"Renders Error","description":"Non-2xx renders."},"bytes_out_total":{"type":"integer","title":"Bytes Out Total","description":"Sum of output bytes for successful renders."},"avg_duration_ms":{"type":"integer","title":"Avg Duration Ms","description":"Average wall-clock duration over the window."}},"type":"object","required":["renders_total","renders_success","renders_error","bytes_out_total","avg_duration_ms"],"title":"UsageCounters"},"UsageResponse":{"properties":{"period_start":{"type":"string","format":"date-time","title":"Period Start","description":"Inclusive lower bound of the window."},"period_end":{"type":"string","format":"date-time","title":"Period End","description":"Exclusive upper bound (== now)."},"period":{"type":"string","enum":["current_month","last_30d"],"title":"Period","default":"current_month"},"counters":{"$ref":"#/components/schemas/UsageCounters"},"by_doc_type":{"items":{"$ref":"#/components/schemas/UsageByDocType"},"type":"array","title":"By Doc Type"},"top_templates":{"items":{"$ref":"#/components/schemas/UsageByTemplate"},"type":"array","title":"Top Templates","description":"Top 10 templates by render count in the window."},"rate_limit_per_key_per_min":{"type":"integer","title":"Rate Limit Per Key Per Min"},"rate_limit_per_ip_per_min":{"type":"integer","title":"Rate Limit Per Ip Per Min"},"storage_used_bytes":{"type":"integer","minimum":0.0,"title":"Storage Used Bytes","description":"Sum of size_bytes across the caller's active templates. Not period-scoped — this is current state, not a window count."},"storage_quota_bytes":{"type":"integer","minimum":0.0,"title":"Storage Quota Bytes","description":"Plan storage cap. 0 means unlimited."},"storage_template_count":{"type":"integer","minimum":0.0,"title":"Storage Template Count","description":"Number of active templates contributing to the storage figure."}},"type":"object","required":["period_start","period_end","counters","by_doc_type","top_templates","rate_limit_per_key_per_min","rate_limit_per_ip_per_min","storage_used_bytes","storage_quota_bytes","storage_template_count"],"title":"UsageResponse"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}},"securitySchemes":{"API Key":{"type":"http","description":"Your `ido_live_…` API key from the dashboard, sent as `Authorization: Bearer <key>`.","scheme":"bearer","bearerFormat":"ido_live_<24 base62 chars>"}}},"tags":[{"name":"API keys","description":"Create, list, rotate, revoke and delete API keys. The full key body is returned **once** on create and rotate — store it immediately. List operations only return the 8-char prefix."},{"name":"Templates","description":"Upload, list, fetch and delete the source documents you want to fill. Max 32 MB per file. Accepted extensions: `.docx`, `.xlsx`, `.pptx`, `.pdf`. Macro-enabled files (`.docm`, `.xlsm`, `.pptm`) and archives containing `vbaProject.bin` are rejected outright."},{"name":"Render","description":"Fill a template (or composition) with a JSON payload and stream the rendered document back. Add `?format=pdf` to convert a docx/xlsx/pptx render to PDF via the Gotenberg sidecar; `&orientation=portrait|landscape` overrides the page layout."},{"name":"Themes","description":"Upload, list, fetch, rename, or delete a shared `.potx`-style master deck. PPTX uploads compute a `master_signature` hash so future template uploads can be checked against the theme's master tree."},{"name":"Compositions","description":"An ordered, optionally-conditional sequence of templates rendered into a single output document. All four formats (docx, xlsx, pptx, pdf) support composition merging. Up to 50 blocks per composition, 200 slides / pages per render."},{"name":"Usage","description":"Read-only observability for the authenticated user: aggregate counters (`/v1/usage`), a paginated audit of every render attempt (`/v1/render-logs`), and a public aggregate counters endpoint for the marketing site (`/v1/stats/public`)."},{"name":"Health","description":"Liveness probe. No auth required."}]}