Everything Render can do in the editor, it can do from an AI assistant or from your own code: turn a brief into a finished ad video, keep it on brand, use your product photos, and tell you when it is ready. This page is the complete description of both.
Add Render to Claude, ChatGPT, Cursor or any MCP client. Then ask for ads in plain English; the assistant plans, shows you the price, and generates when you say yes.
Code · REST + SDKA REST API with a TypeScript SDK. Create ads in bulk or on a trigger, attach brands and product photos, get a webhook when each one is ready.
Both ride the same pipeline as the editor. An ad is planned first — the angle, every shot, every spoken line, the exact cost — and only generated once the plan is approved. Video is billed per generated second: 25 credits a second from your plan, then $0.25 a second on your card once the credits run out. Nothing is charged for a plan.
The base URL is https://tryrender.ai/api/v1. Every response is JSON, wrapped as { success: true, data } or { success: false, error }. The machine-readable description of every endpoint is /api/v1/openapi.json, generated from the same schemas the server validates with.
Render is a Model Context Protocol server. Any assistant that speaks MCP can be given its address and will then know how to list your brands, make ads, check on them, revise them and approve them — as tools it calls on your behalf. You never write code and never copy a key: the assistant sends you to Render to sign in.
Server URL: https://tryrender.ai/api/mcp
Transport: Streamable HTTP
Sign-in: OAuth 2.1 — Render asks you to allow the connectionOpen claude.ai or the Claude desktop app and go to Settings → Connectors.
Add custom connector. Name it Render, paste the server URL, click Add.
Click Connect. A Render page opens; sign in and click Allow.
Settings → Connectors. If there is no Create button, turn on Developer mode under Advanced first.
Create: name it Render, paste the server URL as the MCP server URL, leave authentication on OAuth, click Create.
In a new chat, choose Render under Tools — or simply ask for an ad and let it pick the tool.
Add the URL to the client’s MCP configuration; the sign-in opens in your browser the first time. A client that only accepts a fixed header can use an API key instead of OAuth — same server, no sign-in:
{
"mcpServers": {
"render": {
"url": "https://tryrender.ai/api/mcp",
"headers": { "Authorization": "Bearer rk_live_..." }
}
}
}Talk to it the way you would brief a person. The more specific the brief — what is sold, who buys it, the tone, what should be on screen — the better the ad.
It does not spend money unless you say so. Making an ad stops at a plan by default; generating is a separate approve_ad step, so the assistant shows you the plan and the cost and gets a yes first. When you tell it to just make the ad, it can pass mode: "auto" — the tool itself warns it that this charges your account.
Connected assistants appear on the API & MCP page; disconnecting one there cuts it off immediately. The tools the assistant gets are listed in the tool reference.
Make a key on the API & MCP page. It is shown once — copy it. A key spends your credits, so treat it like a password.
Check it works.
curl https://tryrender.ai/api/v1/credits \
-H "Authorization: Bearer rk_live_..."Make an ad in review mode. It returns at once with an id and status: "planning"; the plan is free.
curl https://tryrender.ai/api/v1/ads \
-H "Authorization: Bearer rk_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"brief": "30 second vertical ad for Bright Smile Dental: same-day crowns, evening hours until 8pm. Calm narration over B-roll of the practice, soft piano, the phone number on screen at the end.",
"format": "9:16",
"duration_seconds": 30,
"mode": "review"
}'Read the plan back. After about 30 seconds the status is awaiting_approval and the response carries the whole plan and its cost.
curl https://tryrender.ai/api/v1/ads/AD_ID \
-H "Authorization: Bearer rk_live_..."Approve it. Generation starts and the credits come out of your pool. Poll until status: "ready", then download output.url — or register a webhook and be told.
curl -X POST https://tryrender.ai/api/v1/ads/AD_ID/approve \
-H "Authorization: Bearer rk_live_..."Leave mode out (or pass "auto") and the ad approves itself the moment its plan exists — one call in, one file out. The same thing with the SDK:
import { createClient } from "@render/sdk";
const render = createClient(process.env.RENDER_API_KEY!);
const ad = await render.ads.createAndWait(
{ brief: "...", format: "9:16", duration_seconds: 30 },
{
onPlan(ad) {
console.log(ad.plan!.angle, "—", ad.plan!.estimated_credits, "credits");
return true; // false walks away without paying
},
}
);
console.log(ad.status, ad.output?.url);Every request carries Authorization: Bearer with either an API key (rk_live_…) or an OAuth access token minted for an MCP client. Keys are made on the API & MCP page and shown once; the server stores only a hash. You can hold up to 20 active keys, and each can carry a per-request credit ceiling (default 2,500, at most 15,000) so a runaway brief cannot ask for a ten-minute film.
A key cannot mint another key, and revoking one stops everything using it immediately. 401 means the key is wrong, revoked, or the token expired (AUTH_EXPIRED — refresh it); 403 means the account has no active subscription or no way to bill.
review planning ──▶ awaiting_approval ──▶ generating ──▶ ready
│ └──▶ failed
└──▶ needs_input
auto planning ──▶ (queued) ──▶ generating ──▶ ready
└──▶ failed| Status | Meaning |
|---|---|
planning | The agent is writing the plan. About 30 seconds. |
awaiting_approval | Review mode: the plan is ready in plan, with its cost, and costs nothing until you approve it. A plan expires after 24 hours. |
needs_input | Review mode only: the agent asked something. The question is in question; answer with POST /v1/ads/{id}/messages. An auto-mode ad answers its own questions and never shows this. |
queued | Auto mode: the plan is accepted and the ad is waiting for a generation slot behind your plan’s concurrency cap. It starts on its own; it waits up to an hour. |
generating | Running. progress counts finished pieces. Minutes, not seconds. |
ready | output.url is a signed download link, good for 24 hours — fetch the ad again for a fresh one. output also carries width, height, duration and size. |
failed | failure_reason says why: a planning failure (planning_failed, planning_timeout, needs_input_unattended), an approval refusal in auto mode (credit_limit_exceeded, forbidden, concurrency_limit after an hour of waiting), or a generation or export failure. Credits already spent on shots that did generate are not refunded by a later failure. |
canceled | You cancelled it. Free before generation; during generation the shots already made are still paid for. |
A brief is a paragraph: what is being sold, who it is for, the tone, what should be on screen. Vague briefs make generic ads, and in auto mode a brief with no brand, no product and no specifics is refused up front rather than paid for.
format is the delivered aspect: 9:16 (default), 16:9, 1:1, 4:5, 4:3, 3:4. duration_seconds is 5–120 (default 30) and it is a contract: the plan’s shots must add up to it, a plan that does not fit is sent back to the planner before you see it, and the delivered file is exactly that long — output.duration_seconds confirms it.
Everything the brief could only hint at, as fields the agent must honour. All optional; leave one out and the agent decides.
| Field | What it does |
|---|---|
brand_id | From POST /v1/brands. The ad speaks in that brand's voice and about that business. Strongly recommended. |
product_id | From POST /v1/products. The ad sells this and builds shots around its exact photos. |
references | Uploads with a job. role: product (show exactly this), person (cast them from the photo), style (match the look), scene (this is the setting), motion (a clip whose movement and pacing to reproduce). Each may carry a short note. |
language | BCP-47 code ("es", "pt-BR", "ja") for every spoken line and on-screen word. Any language the voice model speaks. |
voice_id | A narration voice from GET /v1/voices — the preset roster, or your own cloned voice. |
script | The exact spoken words, used verbatim and in order; the agent only splits them across shots and decides narration versus on-camera. A script that cannot be spoken in the time is refused with the arithmetic. |
style | ugc (one person to a phone camera) · cinematic (film-grade footage, narration) · product_demo (the product in use, close) · motion_graphics (typography and shapes, no live action). |
captions | true burns word-timed captions into the finished file. With a script, the captions are your exact words timed to the narration. |
music | false for no music, or direction such as "warm acoustic, 80bpm". |
presenter | "me" puts the account owner on camera, cast from the photo enrolled under Settings → Your appearance. |
mode | "auto" (default) approves the plan the moment it exists; "review" stops at awaiting_approval. |
name | A label for your own records. |
metadata | Up to 16 string key/values of your own. Echoed on the ad and in every webhook, so a delivery ties back to your order. |
await render.ads.create({
brief: "Talking to camera about why this candle is the one for winter nights",
brand_id: brand.id,
product_id: product.id,
style: "ugc",
language: "es",
voice_id: "jHprmvvyQreWpRuutdmV",
captions: true,
music: false,
references: [{ upload_id: heroShot, role: "product", note: "label facing camera" }],
metadata: { order_id: "o_8812", campaign: "winter" },
});plan is what the agent intends: the angle, the premise, the opening line, every shot with its length, description and spoken line (and whether that line is on-camera dialogue or narration), the CTA, the music, and the cost — billable_seconds, estimated_credits, and what it would cost in dollars if the pool were empty. In review mode you read it before paying; in auto mode it is still returned on the ad for your records.
A brand is what stops every ad inventing a new tone and a new product name. Create one per business — from its website, and the agent reads it, or from fields you supply — then pass brand_id on every ad.
curl https://tryrender.ai/api/v1/brands \
-H "Authorization: Bearer rk_live_..." \
-H "Content-Type: application/json" \
-d '{"url": "https://acme.com"}'
# or, without a website:
-d '{"name": "Cosy Nights", "description": "Hand-poured lavender soy candles for slow evenings.", "tone_of_voice": "warm, unhurried, honest"}'A product is a specific thing you sell — name, description, and its photos as upload ids. Pass product_id on an ad and the agent names it, describes it accurately, and builds shots around those exact photos rather than inventing a look-alike.
const product = await render.products.create({
name: "Lavender Dreams 200g",
description: "Hand-poured soy candle, 40-hour burn",
image_ids: [await render.uploads.fromUrl("https://acme.com/img/lavender.jpg")],
});
await render.ads.create({
brief: "15s vertical ad, cosy evening, ends on the jar",
brand_id: brand.id,
product_id: product.id,
});Two ways in. If the file is already on the web, import it by URL (up to 50MB):
const id = await render.uploads.fromUrl("https://acme.com/img/candle.jpg");If you hold the bytes: reserve an id (you get a signed URL good for 60 minutes), PUT the file to it (up to 200MB — images or video), then optionally confirm.
const id = await render.uploads.upload({
file_name: "candle.jpg",
content_type: "image/jpeg",
body: bytes,
});
await render.ads.create({
brief: "Ad built around this exact product shot",
references: [{ upload_id: id, role: "product" }],
});An upload is checked the moment an ad references it: one with no bytes behind it is refused by id, and a file whose type contradicts what was reserved (video bytes under an image) is refused too. Uploads belong to your account; an id from another account is rejected.
POST /v1/ads/{id}/messages (or /revise, the same thing) sends the agent a message in plain English: an answer to its question, or a change to the plan — “make the opening funnier”, “it’s for dog owners, not cats”. The ad goes back to planning and comes back with a new plan; nothing is charged for a re-plan. On a finished ad the same call starts a new cut.
While an ad is generating a revision is refused (409): wait for ready and revise the finished ad, or cancel it. POST /v1/ads/{id}/cancel stops it at any point; shots already generated stay paid for.
The reason to use the API rather than the app: one ad that works becomes ten. POST /v1/ads/{id}/variants makes up to 10 more that keep the original’s brand, format, length, cast and structure and change only the opening — or the CTA, or the angle.
curl -X POST https://tryrender.ai/api/v1/ads/AD_ID/variants \
-H "Authorization: Bearer rk_live_..." \
-H "Content-Type: application/json" \
-d '{"count": 8, "vary": "hook", "direction": "lead with the 40-hour burn", "mode": "auto"}'Each variant is a full, independent ad with variant_of_ad_id set; cancelling or revising one does not touch the others.
Generation comes out of your plan credits first, at 25 credits per generated second. When the pool runs out, the remaining seconds bill to your card at $0.25 a second as a separate line on your next invoice. A single ad can be paid for both ways: a 30-second ad needs 750 credits; with 300 left, credits cover 12 seconds and the other 18 bill at $0.25 — $4.50. Nothing stops midway.
CREDIT_LIMIT_EXCEEDED) rather than billed, so a runaway script cannot empty your card. Ask support to raise it.POST /v1/ads/estimate prices a batch against your actual balance before you commit — how many seconds credits cover, how many meter, and whether you are inside the monthly ceiling. GET /v1/credits is the balance; GET /v1/usage is what the API has spent in a period: seconds, credits, dollars, ads created and delivered.
curl https://tryrender.ai/api/v1/ads/estimate \
-H "Authorization: Bearer rk_live_..." \
-H "Content-Type: application/json" \
-d '{"duration_seconds": 30, "quantity": 10}'Polling is fine for a handful of ads. Past that, register an endpoint (up to 5) and be told.
curl https://tryrender.ai/api/v1/webhooks \
-H "Authorization: Bearer rk_live_..." \
-H "Content-Type: application/json" \
-d '{"url": "https://you.example.com/render", "events": ["ad.ready", "ad.failed"]}'| Event | When |
|---|---|
ad.plan_ready | Review mode: a plan is waiting for approval. |
ad.needs_input | The agent asked a question. |
ad.generating | Approved; generation started. |
ad.ready | Finished. Fetch the ad for a fresh download URL. |
ad.failed | Failed; failure_reason is in the payload. |
Every delivery carries the same ad object GET /v1/ads/{id} returns, under data.ad, with your metadata on it. The signing secret (whsec_…) is shown once, when the endpoint is created. Each request is signed the way Stripe signs theirs:
Render-Signature: t=1757836800,v1=<hex hmac-sha256 of "{t}.{body}">Verify the MAC over `${t}.${rawBody}` with your secret and reject a timestamp older than five minutes. The SDK does both:
import { verifyWebhook } from "@render/sdk";
const ok = await verifyWebhook({
payload: rawBody,
signature: request.headers.get("render-signature") ?? "",
secret: process.env.RENDER_WEBHOOK_SECRET!,
});
if (!ok) return new Response("bad signature", { status: 400 });A delivery that does not get a 2xx within ten seconds is retried 3 times with backoff. After 20 consecutive failures the endpoint is disabled and GET /v1/webhooks/{id} says why, with its recent deliveries.
Send an Idempotency-Key header on every POST. Generations take minutes and cost money, so retries happen — with a key, a retry within 24 hours returns the first answer instead of buying a second ad. Reusing a key with a different body is a CONFLICT. The SDK sends one on every write.
Lists take limit and cursor and return data, has_more and next_cursor. Keep following next_cursor while has_more is true; a status-filtered page of ads can be short or empty while more remain.
| Limit | Value |
|---|---|
| Requests per minute, per key | 120 (20 of them ad creations). Responses carry X-RateLimit-Limit, -Remaining and -Reset. |
| Generations in flight | 3 clips on Starter, 10 on Pro, 25 on Ultimate. Approving past it is a retriable CONCURRENCY_LIMIT; an auto-mode ad queues for a slot on its own. |
| Request body | 256KB. Files go through uploads. |
| Credits per request | 2,500 by default, settable per key up to 15,000. |
| Ad length | 5–120 seconds; variants 10 per request; estimate quantity up to 500. |
Failure is { success: false, error: { code, message, isRetriable, retryAfter, details } }. Branch on code, never on the message.
| Code | What to do |
|---|---|
AUTH_REQUIRED · AUTH_INVALID | 401. No key, or a wrong or revoked one. |
AUTH_EXPIRED | 401. An OAuth access token has expired. Refresh it; MCP clients do this themselves. |
FORBIDDEN | 403. No active subscription, or credits are used up with no card to bill the rest to. |
RESOURCE_NOT_FOUND | 404. Not yours, or not a valid id. |
VALIDATION_FAILED · INVALID_REQUEST | 400. error.details names the fields. Unknown fields are rejected too, so a typo cannot silently change what you are charged for. |
PAYLOAD_TOO_LARGE | 413. Bodies are capped; upload files through /v1/uploads and pass ids. |
CONFLICT | 409. Wrong state for that action (approving a generating ad, revising during generation), or an Idempotency-Key reused with a different body. |
INSUFFICIENT_CREDITS | 402. Not enough credits and no way to meter the rest. |
CREDIT_LIMIT_EXCEEDED | 402. Over the key's per-request ceiling or the account's monthly metered ceiling. Ask for less, or raise the limit. |
CONCURRENCY_LIMIT | 429, retriable. Too many generations already running. Wait and retry; auto-mode ads queue on their own. |
RATE_LIMITED | 429, retriable. Slow down; honour Retry-After. |
DISPATCH_FAILED | 503, retriable. The queue was briefly unavailable. Nothing was billed. |
@render/sdk is a small TypeScript client with no dependencies that runs anywhere fetch does. It wraps the two things everyone otherwise gets wrong: waiting (waitUntilReady polls with backoff and stops on a terminal state) and retrying (every write carries an idempotency key automatically).
npm install @render/sdk
import { createClient, RenderError } from "@render/sdk";
const render = createClient({ apiKey: process.env.RENDER_API_KEY! });
render.ads create · get · list · approve · message · revise · cancel · variants · estimate
waitUntilReady(id) · createAndWait(params, { onPlan })
render.brands create · get · list · delete
render.products create · list
render.uploads create · complete · upload({ file_name, content_type, body }) · fromUrl(url)
render.voices list
render.webhooks create · list · get · delete
render.credits() render.usage({ start, end })
try { ... } catch (e) { if (e instanceof RenderError) console.log(e.code, e.status, e.retryAfter); }waitUntilReady treats awaiting_approval and needs_input as terminal by default, because both wait on a person; createAndWait handles the whole create → plan → approve → wait cycle with a callback where the human decision goes.
MCP clients sign a person in with OAuth 2.1 rather than a pasted key. The server publishes the standard discovery documents, so a conforming client needs only the server URL:
GET /.well-known/oauth-protected-resource (RFC 9728)
GET /.well-known/oauth-authorization-server (RFC 8414)
POST /oauth/register dynamic client registration (RFC 7591)
GET /oauth/authorize authorization code + PKCE (S256), the person clicks Allow
POST /oauth/token code → tokens; refresh_token → new tokens
POST /oauth/revoke RFC 7009Access tokens live 24 hours and refresh tokens 90 days; the scope is ads. Each connection is a key on the person’s account, listed as a connected assistant on the API & MCP page and revocable there.
Generated from the server’s own validation schemas — the same ones behind openapi.json. Fields marked * are required. Every path is relative to https://tryrender.ai/api.
Create, track and revise ads.
List ads
Create an ad
Returns immediately with `status: "planning"`. In `review` mode the ad reaches `awaiting_approval` with a plan and costs nothing until approved; in `auto` mode it is approved and billed as soon as the plan exists (`queued` while it waits for a generation slot behind the account's concurrency cap).
Estimate what an ad will cost
Prices a batch against your current balance: how much comes out of credits and how much lands on the invoice. Nothing is created and nothing is charged.
Fetch an ad
The polling endpoint. Carries the plan, the progress counter, the question if it is stuck, and a signed download URL once ready.
Cancel an ad
Before approval this stops the ad and costs nothing. After approval the generation jobs are already running and their credits are already spent.
Approve a plan and start generating
Synchronous, so you learn here whether the account can afford it (402) or too much is already running (429).
Answer a question, or ask for a revision
Free before approval. Revising a generated ad produces a new plan that must be approved like any other.
Same as /messages
An alias of POST /ads/{id}/messages under the name the MCP tool uses (revise_ad).
Make variants of an ad that worked
Each variant is a full independent ad inheriting this one's brand, format, length and structure, varying the hook (or CTA, or angle).
Who the ad is for.
List brands
Create a brand
From a website URL, or from explicit fields.
Shape 1 of 2
Shape 2 of 2
Fetch a brand
Delete a brand
What the ad is selling.
List products
Create a product
Your own images and footage.
Start an upload
Returns a presigned URL. PUT the bytes to it with the exact `content_type`, then call the complete endpoint.
Shape 1 of 2
Shape 2 of 2
Finish an upload
Verifies the bytes arrived and marks the upload usable. An upload that is never completed cannot be used in an ad.
Be told, instead of polling.
List webhook endpoints
Register a webhook endpoint
The signing secret is returned once. Every delivery carries `Render-Signature: t=<unix seconds>,v1=<hex>`; verify it as HMAC-SHA256, keyed by the secret, over the timestamp followed by a dot followed by the raw request body.
Fetch an endpoint and its recent deliveries
Delete a webhook endpoint
Credits and usage.
Narration voices
The preset roster plus your own cloned voice when you have one. Pass an id as `voice_id` on POST /ads.
Credit balance
What the API has spent
What a connected assistant can do, as the tools it sees. The descriptions below are the ones the assistant reads, which is why they say when a tool charges.
List the brands saved on this Render account. A brand gives an ad its tone, audience and products, so check here before making one.
Read a business's website and save it as a brand: name, what they do, tone, products, colours. Do this once per business, then reuse the brand id on every ad.
Save a brand without a website: the business's name, what it does, who it is for, and its tone. Use create_brand_from_url instead when there is a site to read.
The products saved on this account, with their ids. Pass a product_id to create_ad so the ad sells that exact product and uses its photos.
Save a product so ads can sell it by id: name, a short description, and optionally its photos (upload ids from add_reference_from_url).
Fetch a public image, video or audio file by URL and store it on the account. Returns an upload id to pass in create_ad's references (with a role) or create_product's image_ids.
The narration voices available, with ids for create_ad's voice_id. Includes the account owner's own cloned voice when they have one.
How many credits the account has left, and what a second of generated video costs. Ads spend credits first and bill the overflow on the invoice.
Work out what an ad of a given length will cost — how much comes out of the credit pool and how much bills on the invoice. Use this before making anything, especially more than one.
Start making a video ad from a description. By default it returns a plan to review — NOTHING is generated or charged until approve_ad is called. Pass mode "auto" to skip the review and generate straight away (this CHARGES the account) — only when the person has clearly asked for that. Write the brief in as much detail as you have: what is being sold, who it is for, the tone, what should be on screen.
See where an ad has got to: the plan waiting for approval, how far generation has got, the finished download link, or the question it is stuck on. Ads take a few minutes — call this again rather than waiting.
Generate the video. This SPENDS the account's credits, and bills the overflow on their invoice if the pool runs out. ONLY call it after showing the person the plan and the cost from check_ad and getting a clear yes — it cannot be undone.
Send a note about an ad in plain English — a change to the plan, or the answer to something it asked. Free while the plan is still unapproved. Revising an already-generated ad produces a new plan that has to be approved again.
Take an ad that has a plan (or is finished) and make more like it, changing only the hook, the CTA, or the angle. Each variant is its own ad; by default each stops at a plan and nothing is generated or charged until approve_ad. mode "auto" generates every variant straight away and CHARGES for each — only when the person has clearly asked for that.
Stop an ad. Free before it is approved. After approval the generation is already running and already paid for — cancelling then only stops the polling.
The ads made on this account recently, with their status. Use it to find an ad id you have lost, or to see what is still running.