Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Rendry User API
version: 1.6.0
description: 'Template-based image generation API. Design a template in the visual editor (or via POST /presets), then render on-brand images with one call. Every capability the MCP server exposes as a tool is reachable over REST as well — same implementation behind both, so the two never drift. For AI agents a remote MCP endpoint is available at /mcp (streamable HTTP) — see https://rendry.io/llms-full.txt. Every refusal, on every door, is one JSON object {error: machine code, message: sentence, …fields} with the HTTP status of its class — see the Refusal schema; an MCP tool error carries the same object as its text.
Authentication: four bearer credentials open the render and template doors — a persisted live or test API key (sk_live_… / sk_test_…), the browser session of the signed-in owner (the JWT POST /auth/login returns) a connector token (the JWT the OAuth flow issues to an MCP client), and an ephemeral editor session (em_…). Browser/OAuth credentials may select a current team membership with X-Rendry-Account; API keys and embed sessions are bound to their persisted account and cannot switch accounts. GET /principal returns the resolved authority. Roles and scopes intersect; a removed member immediately loses access. Test mode is persisted on the credential: renders retain all admission limits, cost zero credits and carry a RENDRY TEST watermark; test credentials cannot publish public links. Operations that list the `session` scheme alone are the account cabinet: an API key is refused there with 401, a connector token with 403 `browser_session_required` (account deletion: `account_deletion_needs_session`). Operations with an empty `security` are public.
Also served, not described operation by operation: GET /templates/{id}/preview.png and GET /template-assets/{name} (public; the gallery''s pictures), GET /assets/library (public; the built-in image stock), GET /plans, GET /billing/config and GET /version (public; prices and the running build), GET /openapi.json (public; this document), POST /feedback (public, rate-limited by IP) and POST /feedback/agent (render credentials), the OAuth 2.1 doors under /oauth (the three /.well-known discovery documents, /register, /authorize, /client-info, /token — public; POST /oauth/decision takes the owner''s session), the guest batch under /batch-tasks/guest (public, rate-limited by IP: POST /preview, POST, GET /{token}, GET /{token}/archive), the sign-in, sign-up, e-mail verification and password doors under /auth (public; POST /auth/verify/resend takes the owner''s session), the cabinet''s POST /user/password, POST /checkout, GET /billing/history, GET /billing/subscription, GET /photos/search and POST /photos/used (owner''s session only), the payment webhooks POST /billing/webhook and POST /billing/webhook/{provider} (signed by the provider), the /internal/* doors of the render nodes (internal token) and the /admin group (administrators, session only).'
contact:
url: https://rendry.io
servers:
- url: https://rendry.io/api/v1
description: Production
security:
- apiKey: []
- session: []
- connector: []
- embed: []
tags:
- name: User
paths:
/user/limits:
get:
summary: Plan and quota usage
description: Current plan, credit balance, template and asset slots used vs limits. Works with an API key — agents check the balance here, because credits are what a render fails on.
responses:
'200':
description: Usage
content:
application/json:
schema:
$ref: '#/components/schemas/UserLimits'
'401':
description: Missing/invalid credentials
operationId: getUserLimits
parameters:
- $ref: '#/components/parameters/AccountSelector'
security:
- apiKey: []
- session: []
- connector: []
tags:
- User
/user/usage:
get:
summary: Plan, quota used and reset date
responses:
'200':
description: Usage
'401':
description: Missing/invalid API key
operationId: getUserUsage
parameters:
- $ref: '#/components/parameters/AccountSelector'
security:
- apiKey: []
- session: []
- connector: []
tags:
- User
/user/api-keys:
get:
summary: List this account's API keys
security:
- session: []
description: 'For the signed-in owner, in a browser session. An API key cannot list, issue or revoke keys: it is refused with 401. A connector (MCP/OAuth) token is refused on every account-management door with 403 `browser_session_required`: a connector does MCP work, it does not manage keys. Each key is listed with its scope; the key itself is shown only as its first 12 characters. The resolved account may be selected with X-Rendry-Account; only an owner or admin browser session may administer its keys. The mode and actor are reported in each reply.'
responses:
'200':
description: Array of APIKey
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/APIKey'
'401':
description: Not signed in, or an API key was used
'403':
description: '`error` is `browser_session_required`: a connector (MCP/OAuth) token was used'
parameters:
- $ref: '#/components/parameters/EndUserAnalytics'
- $ref: '#/components/parameters/AccountSelector'
operationId: listApiKeys
tags:
- User
post:
summary: Issue an API key with a scope
description: 'For the signed-in owner, in a browser session (an API key is refused with 401). A connector (MCP/OAuth) token is refused on every account-management door with 403 `browser_session_required`: a connector does MCP work, it does not manage keys. The response carries the whole key once; it is not stored and cannot be shown again. `scope` decides what the key may do (see the APIKey schema). A request that names no scope gets a `full` key, as every key did before scopes could be chosen; an unknown scope is refused and nothing is issued. The resolved account may be selected with X-Rendry-Account; only an owner or admin browser session may administer its keys. The mode and actor are reported in each reply.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- name
properties:
name:
type: string
description: Seen only by the owner, to tell keys apart when revoking. Stored without leading and trailing whitespace; one of whitespace alone is refused.
maxLength: 255
scope:
type: string
enum:
- read
- render
- full
default: full
mode:
type: string
enum:
- live
- test
default: live
description: Immutable execution mode, persisted on the issued key. Test rendering is watermarked and does not debit credits.
actor_id:
type: string
description: Current member who owns this credential; defaults to the issuing browser actor. Member removal invalidates their keys.
responses:
'201':
description: Issued. An APIKey whose key_value is the whole key.
content:
application/json:
schema:
$ref: '#/components/schemas/APIKey'
'400':
description: 'The body is not JSON, `error` is `api_key_name_required` (the name is empty or only whitespace), `error` is `api_key_name_too_long` (the name is longer than `limit` characters), or `error` is `invalid_api_key_scope`: the scope is not one of read, render, full (they ride in `allowed`); `unknown_field` for a key this door does not read (a near miss is renamed and reported in `autofixes`). Nothing was issued.'
'401':
description: Not signed in, or an API key was used
'403':
description: '`error` is `browser_session_required`: a connector (MCP/OAuth) token was used'
security:
- session: []
parameters:
- $ref: '#/components/parameters/EndUserAnalytics'
- $ref: '#/components/parameters/AccountSelector'
operationId: createApiKey
tags:
- User
/user/api-keys/{id}:
delete:
summary: Revoke an API key
security:
- session: []
description: 'For the signed-in owner, in a browser session. Requests with the key stop working at once. A connector (MCP/OAuth) token is refused on every account-management door with 403 `browser_session_required`: a connector does MCP work, it does not manage keys.'
parameters:
- name: id
in: path
required: true
schema:
type: string
- $ref: '#/components/parameters/EndUserAnalytics'
- $ref: '#/components/parameters/AccountSelector'
responses:
'200':
description: Revoked
'401':
description: Not signed in, or an API key was used
'403':
description: '`error` is `browser_session_required`: a connector (MCP/OAuth) token was used'
'404':
description: '`error` is `api_key_not_found`: this account has no key with that id — it never existed, was revoked already, or belongs to another account. Nothing was changed.'
operationId: deleteApiKey
tags:
- User
/user/account/deletion:
get:
summary: What deleting this account would cost
security:
- session: []
description: 'For the signed-in owner, in a browser. Answers which confirmation this account uses (`confirm_with`: `password` when the owner has ever set one, otherwise `email`), the e-mail on the account, the credit balance that will be forfeited, and the retention window in days. NOT AVAILABLE TO AGENTS: an API key or an MCP/OAuth connector token is refused with `account_deletion_needs_session`, and there is no MCP tool for this.'
responses:
'200':
description: confirm_with, email, credits_forfeited, retention_days
'401':
description: Not signed in; `error` is `unauthorized`
'403':
description: '`error` is `account_deletion_needs_session`: the request came with an API key or a connector token. Only the owner''s browser session may do this.'
parameters:
- $ref: '#/components/parameters/EndUserAnalytics'
- $ref: '#/components/parameters/AccountSelector'
operationId: getAccountDeletion
tags:
- User
post:
summary: Close this account and schedule its deletion
security:
- session: []
description: 'Closes the account at once: sign-in stops, every issued token stops, every API key is revoked, queued render jobs are failed and refunded, and the remaining credits are forfeited. The data itself is erased after the retention window (`retention_days`), and until then support can restore the account. Payment and credit-ledger records are kept as accounting records, anonymised. NOT AVAILABLE TO AGENTS — see the GET above.'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
password:
type: string
description: The current password. Required when GET answered confirm_with=password.
email:
type: string
description: The account's own e-mail, typed back. Required when GET answered confirm_with=email (an account created through SSO has no password of its own).
responses:
'200':
description: Closed. Returns closed_at, deletion_date, retention_days, credits_forfeited, keys_revoked.
'400':
description: The body is not JSON
'401':
description: Not signed in; `error` is `unauthorized`
'403':
description: '`error` is a machine code: `account_deletion_needs_session` (an API key or a connector token was used) or `account_deletion_not_confirmed` (the password or e-mail did not match — nothing was changed).'
'429':
description: More than 5 attempts a minute from one address
parameters:
- $ref: '#/components/parameters/EndUserAnalytics'
- $ref: '#/components/parameters/AccountSelector'
operationId: deleteAccount
tags:
- User
components:
schemas:
APIKey:
type: object
properties:
id:
type: string
name:
type: string
key_value:
type: string
description: The whole key (sk_live_... or sk_test_...) in the create response, and ONLY there — it is not stored. The list shows the first 12 characters followed by ***.
scope:
type: string
enum:
- read
- render
- full
description: 'What the key may do. read: list and read templates, presets, documents, usage, and price a batch (POST /batch-tasks/preview parses the table and quotes the run, rendering and charging nothing) — no rendering, no changes. render: everything read may, plus render_image, render_batch, render_document and preview_preset (they spend credits) — no template edits or deletions. full: everything, including creating, editing, sharing and deleting templates and files.'
created_at:
type: string
mode:
type: string
enum:
- live
- test
actor_id:
type: string
issued_by_actor_id:
type: string
UserLimits:
type: object
properties:
plan:
type: string
email:
type: string
presets:
type: object
properties:
used:
type: integer
limit:
type: integer
assets:
type: object
properties:
used:
type: integer
limit:
type: integer
upgrade_url:
type: string
description: Legacy alias of packages_url.
credits:
type: object
description: Credit balance — the only thing a render actually fails on.
properties:
balance:
type: number
spent_on_renders:
type: number
grant_withheld:
type: boolean
description: Starter credits are held until the email is confirmed.
packages_url:
type: string
description: Where to buy credits.
email_verified:
type: boolean
parameters:
EndUserAnalytics:
name: X-End-User
in: header
required: false
description: Analytics correlation only; never authorizes files or accounts. One safe ASCII value up to 64 bytes. Invalid values return 400. Persisted embedded subject takes precedence. Allowed by CORS.
schema:
type: string
minLength: 1
maxLength: 64
pattern: ^[A-Za-z0-9_.:@-]{1,64}$
AccountSelector:
name: X-Rendry-Account
in: header
schema:
type: string
description: Browser/OAuth current member account selector. API keys/embed credentials are bound to their persisted account and reject another account. Omitting this header selects the browser actor personal account.
securitySchemes:
apiKey:
type: http
scheme: bearer
description: 'API key sk_live_... or sk_test_... — create at Settings → API Keys. Every key has a scope (read, render or full — see the APIKey schema); a call the scope does not cover answers 403 with `error` = `api_key_scope_insufficient`, the refused `tool`, the key''s `scope`, and a `message` naming what the key can call. A key limited to a list of presets answers a preset outside it with 403 `api_key_preset_not_allowed` and the refused `preset_id`; such a key renders and edits only its listed presets, and an inline design (canvas + layers, a document page or master of its own), a draft or a new template is refused with 403 `api_key_limited_to_presets` and a `reason` (inline_design, draft, create_template, inline_page). RESERVED: no door issues a key limited to presets yet — POST /user/api-keys hands out keys for the whole account — so `api_key_preset_not_allowed` and `api_key_limited_to_presets` are documented for clients built ahead of that door and are not answered by any key issued today.'
session:
type: http
scheme: bearer
bearerFormat: JWT
description: 'The browser session of the signed-in owner: the JWT POST /auth/login (or the SSO door) returns, sent as `Authorization: Bearer <jwt>`. Opens every render and template door and, alone, the account cabinet (API keys, account deletion, password, billing).'
connector:
type: http
scheme: bearer
bearerFormat: JWT
description: 'The token the OAuth 2.1 flow under /oauth issues to an MCP client (claude.ai connectors): the same JWT shape as a session, tagged as issued to a connector. Opens the render and template doors and /mcp; every cabinet door refuses it with 403 `browser_session_required`, account deletion with 403 `account_deletion_needs_session`.'
embed:
type: http
scheme: bearer
description: Ephemeral em_... token redeemed by the origin-restricted embedded editor. Its account, trusted subject, role and live/test mode are persisted server-side. It cannot access account administration, billing, account-wide batch services or public links. Never persist or put it in a URL.
x-rendry-service-policy:
rates:
rest_render:
requests: 10
window_seconds: 1
burst: 10
scope: account
consistency: api_process
mcp_request:
requests: 10
window_seconds: 1
burst: 10
scope: account
consistency: api_process
ui_request:
requests: 10
window_seconds: 1
burst: 10
scope: account
consistency: api_process
public_link:
requests: 60
window_seconds: 60
burst: 60
scope: ip
consistency: api_process
embed_frame:
requests: 10
window_seconds: 60
burst: 10
scope: ip
consistency: api_process
embed_redeem:
requests: 10
window_seconds: 60
burst: 10
scope: ip
consistency: api_process
sync_concurrency:
account: 4
subject: 2
consistency: api_process
scope: synchronous_render
document_queue:
pending_per_account: 4
long_pending_per_account: 1
long_above_pages: 20
consistency: postgres_transaction
scope: account; queued and processing documents
retention:
authenticated_days: 365
guest_hours: 72
url_cache_days: 30
starts_at: 'authenticated: completion; guest: batch creation'
end_user:
header: X-End-User
max_bytes: 64
pattern: ^[A-Za-z0-9_.:@-]{1,64}$
authority: analytics only; trusted embedded subject takes precedence
notes:
- REST, MCP and UI request buckets are separate; each account bucket is shared by members, keys and embedded subjects
- API keys bypass only the UI bucket; authenticated embedded sessions use it
- Sync concurrency is per API process; queued document admission is separately reserved in PostgreSQL
- Retention is the source policy; deployment of storage lifecycle rules requires a separate operator action