API reference
Generated from the API's own OpenAPI document (GET /v1/openapi.json) — every route and schema here is proven against real responses by
apps/api/src/openapi.contract.test.ts.
Background images (bg)
Pass bg as a top-level POST /v1/images field
(or query param on the signed/public GET routes) to render one of the 40 curated free-pack
background photos full-bleed behind the card, e.g. bg=bg-01. The
template's title/description text gets its own contained backdrop panel so it stays legible
over the photo — the photo itself is never dimmed or scrimmed.
Only templates whose design isn't a deliberate physical/mechanical artifact (a receipt, a
punchcard, a ballot, ...) accept it: basic, blog,
docs, changelog,
product, event,
podcast, job,
release, case-study,
testimonial, lesson,
newsletter, spotlight,
aurora, signal and
verbatim. An unrecognized bg id is
rejected with invalid_input; a valid id passed to a template
that doesn't support backgrounds is silently ignored, same as any other param a template's
schema doesn't declare.
Array fields on GET routes
POST /v1/images takes an array field as a real JSON array in the
request body. The two GET routes — signed GET /i/{keyId}/{sig}
(Signing) and unsigned GET /p/{keyId}/{template}
(Public URLs) — have no JSON body, so an array field is
carried as bracket-indexed query keys instead:
field[0]=first&field[1]=second, up to that field's own max
length, indexes contiguous starting at 0. A plain repeated key
(field=a&field=b) is rejected as a duplicate, not merged into
an array.
11 templates require at least one array field, so a request for them cannot be built without this syntax:
| Template | Array field(s) |
|---|---|
| changelog | changes |
| receipt | items |
| patch | added |
| ledger | rows |
| snellen | lines |
| redacted | lines |
| ballot | options |
| metro | stops |
| facts | rows |
| bracket | contenders, finalists |
| app | platforms |
POST /v1/images
Requires Authorization: Bearer og_live_....
Request body — PostImagesBody | PostImagesHtmlBody
PostImagesBody
| Field | Type | Required |
|---|---|---|
| template | string | no |
| saved | string | no |
| url | string | no |
| refresh | boolean | no |
| params | object | no |
| format | "png" | "jpg" | "webp" | "pdf" | no |
| preset | "og" | "x" | "square" | no |
| scale | "1" | "2" | no |
| v | string | no |
| bg | string | no |
| font | string | no |
| webhook_url | string | no |
| auto | boolean | no |
| hint | string | null | no |
PostImagesHtmlBody
| Field | Type | Required |
|---|---|---|
| html | string | yes |
| css | string | no |
| width | integer | yes |
| height | integer | yes |
| format | "png" | "jpg" | "webp" | "pdf" | no |
Responses
| Status | Type | Description | Retry-After |
|---|---|---|---|
| 201 | PostImagesResponse | PostImagesHtmlResponse | Image resolved (rendered or served from cache). A raw-`html` request is always a fresh render (2 credits). Auto mode (`auto: true`) adds `credits`, `sourceUrl` and `refreshAfter`. | no |
| 202 | PostImagesAccepted | Async mode (`webhook_url` was sent): accepted; the render runs in the background and its outcome is POSTed to the webhook as `image.completed` or `image.failed` (validation and quota failures arrive there, not here). **Webhooks.** When `webhook_url` is set, the outcome is POSTed there as JSON `{ id, type, created_at, data }` (`id` = the delivery id, `created_at` = unix seconds). The URL must be `https://`, is checked by an SSRF guard (private, loopback, link-local and metadata addresses are refused; ports 80/443 only; redirects are not followed) and is re-checked on every delivery attempt. **Signature.** Every request carries `X-Ogmake-Signature: t=<unix seconds>,v1=<hex>` where `v1 = HMAC-SHA256(key, t + "." + rawBody)` in lowercase hex, and `key` is the raw bytes of the base64-decoded signing secret of the API key that submitted the job (the same secret that signs `/i` URLs). Recompute it over the exact raw body, compare in constant time, and reject a `t` more than 5 minutes old. Node: `crypto.createHmac('sha256', Buffer.from(signingSecretBase64, 'base64')).update(t + '.' + rawBody).digest('hex')`. Other headers: `X-Ogmake-Event` (the event type), `X-Ogmake-Delivery` (the delivery id, stable across retries — use it to de-duplicate). **Retries.** A non-2xx response, a timeout (8 s) or a network error is retried up to 3 more times, 1, 5 and 30 minutes apart (4 attempts in all); a refused target is not retried. Each delivery and its attempts are listed by `GET /v1/webhooks/deliveries`. Events: `batch.completed` (`data`: `batch_id`, `status`, `total`, `succeeded`, `failed`, `results_url`), `image.completed` (`data`: `request_id` plus the fields of the synchronous 201 response) and `image.failed` (`data`: `request_id`, `error`, `message`). | no |
| 400 | object | Invalid request | no |
| 401 | object | Missing/unknown/revoked API key | no |
| 402 | object | Async mode only (`webhook_url`): no credit available for the job (plan credits up to the hard cap, plus unexpired top-ups on paid plans). Nothing is stored or charged. Auto mode: `paid_only` on the free plan. | no |
| 403 | object | `url` is not on the saved URL template's hosts/path (`source_host_not_allowed`). Nothing is fetched or charged. Auto mode: `forbidden` when auto mode is not enabled for the account. | no |
| 409 | object | Auto mode: this `Idempotency-Key` was used in the last 24 hours with a different request body (`idempotency_conflict`), or the first request with it is still running (`idempotency_in_progress`, with `Retry-After`). Auto mode with a `hint`: `hint_in_progress` while the same hint for the same page is being read by another request. | yes |
| 413 | object | Request body too large: 64 KB by default; up to 300 KB only for raw `html` requests that send the `X-Ogmake-Raw: html` header. Auto mode with a screenshot: the screenshot is over 5 MB, or the multipart body is over its own cap. | no |
| 422 | object | Saved URL template: the page could not be read (`source_unavailable`) or it has no value for a required field (`source_field_missing`, `detail` names the field). Auto mode with a `hint` and no screenshot: `page_unreadable` when the page's text could not be read; attach a screenshot or set the card fields in `params`. Nothing is charged. | no |
| 429 | object | QuotaExceeded | object | Rate limited, quota exhausted, or (async mode) 50 unfinished async jobs already in progress. Auto mode: `quota_exceeded` when the account is past its monthly cap and the page has no paid 90-day window (`Retry-After` until the next billing period); `rate_limited` is the key's own rate limit. Auto mode with a `hint`: `quota_exceeded` also when a new hint's 2 credits (plus the card's 3 on a new page: 5) do not fit; `ai_limit_reached` when the account's AI reads for the month are used up (`Retry-After` until the next month). | yes |
| 500 | object | Unexpected server error (e.g. a database dependency failure) | no |
| 503 | object | Render engine unavailable. Auto mode: `temporarily_unavailable` when the page's product image could not be fetched just now; nothing is charged. With a `hint` or screenshot: `ai_unavailable` when the AI read failed or AI reading is paused; nothing is charged and nothing retries on its own (`Retry-After`). | yes |
POST /v1/url-templates/analyze
Requires Authorization: Bearer og_live_....
Request body — AnalyzeUrlBody
| Field | Type | Required |
|---|---|---|
| url | string | yes |
Responses
| Status | Type | Description | Retry-After |
|---|---|---|---|
| 200 | AnalyzeUrlResponse | What a URL template can pull from the page: the page type, candidate fields (text, prices, images, links) with their source and confidence, and suggested templates. Free; limited to 5 per minute and 30 per hour per account. | no |
| 400 | object | Not a public http(s) URL | no |
| 401 | object | Missing/unknown/revoked API key | no |
| 413 | object | Request body too large | no |
| 422 | object | The page could not be read | no |
| 429 | object | Rate limited (per minute, hour or day) | yes |
POST /v1/batch
Requires Authorization: Bearer og_live_....
Request body — PostBatchBody
| Field | Type | Required |
|---|---|---|
| items | BatchItem[] | yes |
| webhook_url | string | no |
Responses
| Status | Type | Description | Retry-After |
|---|---|---|---|
| 202 | PostBatchResponse | Accepted; rendering has started. | no |
| 400 | object | Invalid request; `detail[].field` names `items[i].field`, a CSV header/row, or `webhook_url`. | no |
| 401 | object | Missing/unknown/revoked API key | no |
| 402 | object | Not enough credits for the whole batch: the item count exceeds the credits available (plan credits left up to the hard cap, plus unexpired render top-ups on paid plans); `detail` states both numbers. Nothing is stored or charged. | no |
| 413 | object | Request body larger than 1 MiB | no |
| 429 | object | Rate limited, or 5 unfinished batches already in progress | yes |
| 500 | object | Unexpected server error (e.g. a database dependency failure) | no |
GET /v1/batch/{id}
Requires Authorization: Bearer og_live_....
Responses
| Status | Type | Description | Retry-After |
|---|---|---|---|
| 200 | BatchStatus | Batch status with per-item stored URLs or errors | no |
| 401 | object | Missing/unknown/revoked API key | no |
| 404 | object | No such batch for this account | no |
| 429 | object | Rate limited | yes |
GET /v1/webhooks/deliveries
Requires Authorization: Bearer og_live_....
Responses
| Status | Type | Description | Retry-After |
|---|---|---|---|
| 200 | WebhookDeliveriesResponse | Deliveries with their attempt counts and last result | no |
| 400 | object | Invalid `limit` | no |
| 401 | object | Missing/unknown/revoked API key | no |
| 429 | object | Rate limited | yes |
GET /v1/usage
Requires Authorization: Bearer og_live_....
Responses
| Status | Type | Description | Retry-After |
|---|---|---|---|
| 200 | UsageResponse | Current-period usage for the authenticated account | no |
| 401 | object | Missing/unknown/revoked API key | no |
| 429 | object | Rate limited | yes |
GET /v1/templates
Responses
| Status | Type | Description | Retry-After |
|---|---|---|---|
| 200 | TemplatesResponse | Every registered template's id, version, and params JSON Schema | no |
Error codes
Every error response is JSON { "error": <code> }.
| Code | Meaning |
|---|---|
| invalid_input | Request body or params failed validation. |
| unknown_template | `template` does not match a registered template id. |
| params_too_large | Encoded params exceeded the size limit. |
| invalid_json | Request body was not valid JSON. |
| unauthorized | Missing or unknown API key. |
| unauthenticated | No session — sign in first. |
| key_revoked | The API key exists but has been revoked. |
| forbidden | Authenticated, but not allowed to perform this action. |
| not_found | Nothing at this path/id. |
| rate_limited | Per-key burst limit hit; see `Retry-After`. |
| quota_exceeded | Past the monthly cap and its grace window. |
| render_unavailable | The render engine is temporarily unavailable. |
| key_limit | This account already has the maximum of 5 active API keys. |
| invalid_signature | The webhook signature did not verify (or no webhook secret is configured). |
| no_subscription | No billing subscription exists yet for this account. |
| billing_unavailable | The billing provider's API failed; retry after the given `Retry-After`. |