# Renderly API: complete guide Renderly gives one API key and one prepaid credit wallet for image, video, audio, 3D and language AI models. 1 credit = US$0.01. Every creation is priced first (free), then created as an asynchronous job that reserves credits; a failed job is refunded automatically. This file is a self-contained reference. The same information is in the API guide (https://renderly-web-production.up.railway.app/docs.html) and the OpenAPI 3.1 document (https://renderly-web-production.up.railway.app/openapi.json). ## 1. Base URL and authentication Base URL: https://woovhhwoevjoipwltiwy.supabase.co/functions/v1/core-api (The SDKs and CLI read the RENDERLY_BASE_URL environment variable to override it.) - Create a key in the workspace: Settings → API access. The key (rly_…) is shown once; store it in a server-side secret store or the RENDERLY_API_KEY variable. - Send `Authorization: Bearer ` on every protected request. - Public routes (no key): GET /catalog, /catalog/details, /catalog/prices, /models, /models/{id}, /status. - Scopes: "Read only" keys can read the account, wallet, ledger, usage, jobs and download results. "Read & create" keys can also quote, create and cancel jobs and upload inputs. - Keys expire after 30, 90 or 365 days or never; up to 25 active keys; optional monthly spend limit per key (credits, calendar month UTC). - API keys cannot manage keys, webhooks, alerts, account export/deletion, account settings, payments or admin controls. Those need a signed-in session. - Never embed a key in browser or mobile code. Browser origins are restricted. ## 2. Request flow 1. Find a model: GET /models?q=portrait&kind=image&available=true Model IDs are opaque catalogue IDs (for example model-3f1c…), not names. 2. Read its contract: GET /models/{id} → model.creation and model.inputSchema (JSON Schema 2020-12 of the `input` object). 3. Price it: POST /quotes {"modelId","operation","sku","input"} → quoteId, credits, maxCredits, expiresAt. Quoting never charges. Show maxCredits to the person paying before continuing. 4. Create it: POST /jobs {"quoteId","requestId","input"} with exactly the quoted input and your own requestId (8–100 letters, digits, "_" or "-"). Persist the requestId first. If the response is uncertain (timeout, network error, 5xx), retry with the same requestId and body: the original job is returned (`reused: true`). Never invent a new requestId for a retry. 5. Wait: GET /jobs/{id}?wait=60 returns as soon as the job is completed or refunded (long poll, up to 60 s). Repeat until status is final. Or pass ?wait=N (or `Prefer: wait=N`) on POST /jobs, or use webhooks. 6. Download: for each job.artifacts[i], GET /artifacts/{artifactId} → {url, expiresIn: 900}. Fetch the signed url without the API key. Request a new link after 15 minutes. Job status: queued → processing → (reconciling) → completed | refunded. completed and refunded are final; reconciling still needs a later check. ## 3. Model inputs - Dynamic models (creation.dynamic = true): send input = {"parameters": {...}} keyed by field IDs from creation.fields, e.g. {"parameters": {"p0": "A glass fox"}}. Field format: {"id":"p0","label":"Prompt","type":"string","required":true}. Optional keys: role (image|video|audio file input), choices, default, minimum, maximum, maxLength, minItems, maxItems, fields (object), conditional. - Fixed models: send the prompt in creation.promptField (prompt, text or input), at most creation.maxPromptLength characters, plus creation.fixedInput unchanged. - Selected-input models use sku "selected-settings-v1". - File inputs (fields with a role): upload first, then put the returned upload ID in that field (an array of IDs for array fields). - inputSchema in GET /models/{id} is authoritative for validation. ## 4. Endpoints All responses are JSON. Every response has X-Request-Id: req_<32 hex>. GET /catalog (public) Full catalogue: {models:[…], updatedAt, pricingStatus}. Prefer /models. GET /catalog/details?ids=a,b (public; 1–100 IDs) {models:[…]} with full creation settings. GET /catalog/prices?ids=a,b (public; 1–12 IDs) {prices:[{modelId, operation, sku, credits, billingUnit, observedAt, validUntil}]} GET /models (public) Query: q, kind, operation, maker, available=true|false, limit 1–200 (50), cursor. → {models:[{id,name,maker,kind,operation,capabilities,available,sku,pricingMode, billingUnit,price:{credits,usd,billingUnit}|null}], nextCursor, total}. price.credits and price.usd are decimal strings. Follow nextCursor to page. GET /models/{id} (public) → {model:{…compact fields, creation, inputSchema}}. 404 not_found. GET /status (public) → {generatedAt, components:[{id,name,state: operational|degraded|outage|unknown, checkedAt}], daily:[{date,jobs,completed,refunded,deliveryFailures, completionRate}], incidents:[{id,title,state,impact,startedAt,resolvedAt, updates:[{at,state,message}]}]}. daily covers 90 days. GET /account (read) → {account:{id,email,tier,limits:{requestsPerMinute,quotesPerMinute, jobsPerMinute,downloadsPerMinute}}, key:{id,name,prefix,scopes,expiresAt, monthlySpendLimit,monthSpent}|null}. GET /wallet (read) → {balance, reserved, lowBalanceCredits, ledger:[recent entries]}. reserved is held by unfinished jobs and already deducted from balance. GET /ledger (read) Query: limit 1–200 (50), before (entry id), kind, from, to (ISO 8601). → {entries:[{id,jobId,kind,amount,balanceAfter,createdAt}], nextBefore}. GET /usage (read) Query: from, to (ISO; default last 30 days; at most 366 days), groupBy = day (default) | model | key. → {from,to,groupBy,totals,rows:[{key,jobs, completed,refunded,inProgress,chargedCredits,refundedCredits,reservedCredits}]}. For groupBy=key, jobs created in the browser use key "workspace". POST /quotes (create) Body {modelId, operation, sku, input}. → {quoteId, credits, maxCredits, expiresAt, maximumReserve?, money:{currency,usdPerCredit,totalUsd,maximumUsd}}. maximumReserve=true (metered models): maxCredits is reserved, unused part refunded. POST /jobs (create) Body {quoteId, requestId, input, webhookUrl?}. Optional ?wait=0–60 or Prefer: wait=N. → {job, balance, reused}. webhookUrl: public HTTPS URL notified for this job only. GET /jobs (read) Query: limit 1–100 (50), before (ISO timestamp = nextBefore), status (comma separated: active, queued, processing, completed, refunded), modelId, from, to, keyId. → {jobs:[…], nextBefore}. GET /jobs/{id} (read) Optional ?wait=0–60 long poll. → {job}. Job: {id, modelId, modelName, kind, operation, requestId, createdAt, updatedAt, completedAt, refundedAt, status, credits, chargedCredits, refundCredits, errorMessage, cancelled?, artifacts:[{id,kind,mimeType,bytes,expiresAt, createdAt,available}]}. POST /jobs/{id}/cancel (create) Body {}. Cancels a job not yet sent for generation and returns the full reservation → {job, balance, cancelled:true}. 409 job_not_cancellable after. GET /artifacts/{id} (read) → {url, expiresIn: 900}. 410 artifact_expired after retention ends. POST /uploads?kind=image|video|audio (create) Either raw bytes with Content-Type: application/octet-stream, or JSON {"url":"https://…"} to import a public HTTPS file. Max 24 MiB; same validation as the workspace. Private, local or redirect-to-private addresses → 400 invalid_upload_url; unreachable file → 502 upload_fetch_failed. → {id, kind, mimeType, bytes, width, height, durationSeconds}. ## 5. Errors Body: {"error": {"code": "…", "message": "…"}}. Check the HTTP status first. - 400 invalid_request, invalid_input, settings_not_quoted, invalid_upload_url: fix the request or input (see inputSchema). - 401 authentication_required: key missing, invalid, expired or revoked. - 403 api_scope_forbidden (key scope or session-only route), verified_account_required, account_temporarily_restricted, payment_account_review_required, origin_not_allowed. - 404 not_found, quote_not_found. - 409 insufficient_balance (add credits), quote_expired / quote_price_changed / quote_input_mismatch / quote_already_used (get and approve a new quote), request_id_conflict (same requestId with a different body), job_not_cancellable, key_spend_limit_reached (key's monthly limit). - 410 artifact_expired. - 429 rate_limited: wait Retry-After seconds. - 502 upload_fetch_failed. - 503 model_unavailable, input_media_not_available, service_unavailable: retry later; a job submission keeps its original requestId. Quote the X-Request-Id header when contacting support. Never send your key. Safe retries: GET requests and POST /jobs (same requestId) may be retried on network errors, 429 and 5xx. Retry POST /quotes at most once, then request a new quote. Uploads and cancellation: retry only after 429. ## 6. Limits and headers - 600 protected requests per minute per API key. - Account limits per minute by tier (lifetime purchased credits): standard (below 10,000): 120 quotes, 60 jobs, 300 downloads growth (10,000 or more): 240 quotes, 120 jobs, 600 downloads scale (100,000 or more): 600 quotes, 300 jobs, 1,200 downloads - Rate-limited routes send X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds when the minute window ends). 429 adds Retry-After (seconds). Browsers can read them (Access-Control-Expose-Headers). - GET /account shows your tier and limits. ## 7. Retention - Results: 7 days by default; 1 hour to 30 days selectable in the workspace. - Uploaded inputs: 24 hours. - Signed download links: 15 minutes. ## 8. Webhooks Account webhooks: register up to five HTTPS endpoints under Settings → API access → Webhooks (signed-in session). Per-request: pass webhookUrl on POST /jobs. Events: - job.completed: data.job with artifacts ready to download. - job.refunded: data.job; the job failed and its credits were returned. - balance.low: data {balance, lowBalanceCredits}; the balance crossed below the account's low-balance threshold (set under Settings, GET/POST /account/alerts). - webhook.test: data {test: true}; sent by "Send test". Each delivery is POST with JSON {id: "evt_…", type, createdAt, attempt, data} and headers Renderly-Event, Renderly-Event-Id and Renderly-Signature: t=,v1=. v1 = HMAC-SHA256(secret, "."). Account endpoints use the endpoint's signing secret (whsec_…, shown once at creation); per-request webhookUrl callbacks use the account callback secret (Settings → API access → Webhooks). Verify against the raw body, compare in constant time, reject timestamps older than five minutes, and ignore repeated event ids. Answer 2xx within 8 seconds. Failures, timeouts and redirects (never followed) are retried after 1 m, 5 m, 30 m, 2 h, 6 h, 12 h and 24 h; eight failed attempts end a delivery and twenty consecutive failures disable the endpoint. Node.js verification without the SDK: import {createHmac, timingSafeEqual} from 'node:crypto'; function verify(rawBody, header, secret) { const {t, v1} = Object.fromEntries(header.split(',').map(p => p.split('='))); if (Math.abs(Date.now() / 1000 - Number(t)) > 300) throw new Error('stale'); const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'); if (!timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) throw new Error('bad signature'); return JSON.parse(rawBody); } ## 9. JavaScript SDK (npm install renderly-ai) Zero dependencies; Node 18+, Deno, Bun, edge runtimes. ESM. import {Renderly, verifyWebhook} from 'renderly-ai'; const renderly = new Renderly(); // RENDERLY_API_KEY, RENDERLY_BASE_URL const {models} = await renderly.searchModels('portrait', {kind: 'image', available: true}); const model = await renderly.model(models[0].id); // creation + inputSchema const {quote, job} = await renderly.run({ modelId: model.id, operation: model.operation, sku: model.sku, input: {parameters: {p0: 'A glass fox in morning light'}}, maxCredits: 50 // refused before any charge if higher }); const bytes = await renderly.downloadArtifact(job.artifacts[0].id); Methods: models, searchModels, iterateModels, model, catalog, catalogDetails, catalogPrices, status, account, wallet, ledger, iterateLedger, usage, quote, createJob(body, {wait}), getJob(id, {wait}), listJobs, iterateJobs, waitForJob, cancelJob, artifact, artifactUrl, downloadArtifact, upload(bytes, {kind}), uploadUrl(url, {kind}), run. Errors are RenderlyError {code, status, requestId, retryAfter}. renderly.lastRateLimit and renderly.lastRequestId reflect the latest response. verifyWebhook({payload, header, secret}) returns the event. ## 10. Python SDK (pip install renderly-ai) Standard library only; Python 3.8+. Import name: renderly_ai. from renderly_ai import Renderly, verify_webhook renderly = Renderly() # RENDERLY_API_KEY, RENDERLY_BASE_URL models = renderly.search_models("portrait", kind="image", available=True)["models"] model = renderly.model(models[0]["id"]) result = renderly.run(model_id=model["id"], operation=model["operation"], sku=model["sku"], input={"parameters": {"p0": "A glass fox"}}, max_credits=50) data = renderly.download_artifact(result["job"]["artifacts"][0]["id"]) Methods mirror JavaScript in snake_case: upload_from_url, cancel_job, iter_jobs, iter_ledger, iter_models, wait_for_job, list_jobs(status=, model_id=, from_=, to=, key_id=), usage(from_=, to=, group_by=). RenderlyError has code, status, request_id, retry_after. verify_webhook(raw_body, header, secret). ## 11. CLI (included with renderly-ai on npm) npx renderly-ai --help # or npm i -g renderly-ai, then renderly renderly balance renderly models "portrait" --kind image --available renderly model # fields and input schema renderly quote --prompt "A glass fox" # free renderly run --prompt "A glass fox" --max-credits 40 [--yes] [--out dir] renderly jobs --status active renderly job --wait renderly cancel renderly download --out result.png renderly upload ./reference.png --kind image (or an https:// URL) renderly usage --group model --from 2026-10-01T00:00:00Z renderly status --input '{json}' or --input @file.json passes a full input. --json prints raw JSON. run shows the quote and asks for confirmation unless --yes, and refuses quotes above --max-credits. Exit codes: 0 ok, 1 API error, 2 usage error. ## 12. MCP server (npx -y renderly-mcp) Stdio MCP server (protocol 2025-06-18) for Claude Desktop, Claude Code, Cursor and other MCP clients. Configure with RENDERLY_API_KEY in its environment: {"mcpServers": {"renderly": {"command": "npx", "args": ["-y", "renderly-mcp"], "env": {"RENDERLY_API_KEY": "rly_…"}}}} claude mcp add renderly --env RENDERLY_API_KEY=rly_… -- npx -y renderly-mcp Tools: get_balance, get_usage, search_models, get_model, get_quote, create_job (spends credits; requires maxCredits and refuses a higher quote), get_job (with waitSeconds), cancel_job, list_jobs, get_download_url, upload_from_url, get_status. A Read-only key limits an assistant to browsing and history; a key spend limit caps what it can spend. ## 13. Model lifecycle Models with API usage get at least 14 days notice before removal, except removals forced by the model supplier or required for safety. Deprecated models are marked in the catalogue. Availability can change at any time; check `available` and handle 503 model_unavailable. ## 14. cURL example export RENDERLY_API_KEY="rly_…" export RENDERLY_BASE_URL="https://woovhhwoevjoipwltiwy.supabase.co/functions/v1/core-api" curl "$RENDERLY_BASE_URL/models?q=portrait&available=true" curl "$RENDERLY_BASE_URL/quotes" -H "Authorization: Bearer $RENDERLY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"modelId":"MODEL_ID","operation":"text-to-image","sku":"selected-settings-v1","input":{"parameters":{"p0":"A glass fox"}}}' curl "$RENDERLY_BASE_URL/jobs?wait=30" -H "Authorization: Bearer $RENDERLY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"quoteId":"QUOTE_ID","requestId":"my-order-0001","input":{"parameters":{"p0":"A glass fox"}}}' curl "$RENDERLY_BASE_URL/jobs/JOB_ID?wait=60" -H "Authorization: Bearer $RENDERLY_API_KEY" curl "$RENDERLY_BASE_URL/artifacts/ARTIFACT_ID" -H "Authorization: Bearer $RENDERLY_API_KEY"