By Reviral Team · September 4, 2026 · 9 min read
Build a Video Ad Generator in 60 Seconds
Create a key, read the model catalog, start a render, and poll it to done — the whole API loop, with real request and response bodies.
Create a server-side key at /app/api-keys, then call POST /api/v1/generate-media with a model ID, a prompt, and a duration. Poll GET /api/v1/jobs/{id} until the status is ready, then download the signed output URL. That full loop — key, generate, poll, download — is the whole surface, and it fits in about 60 seconds of code.
A "video ad generator" is not a special endpoint. It is the same three calls every integration on Reviral's API uses — list a model, start a job, poll it — pointed at a prompt that describes an ad instead of a landscape. This walkthrough builds that loop from a blank terminal: one real key, one real render request, one real poll, using the exact field names the live API documentation and the checked-in OpenAPI schema define. Nothing below is simplified pseudo-code — every request body was checked against public/openapi.json before publishing this page.
What you're building
By the end of this page you will have sent one authenticated request that starts a video render, watched its status move from queued to running to ready, and pulled back a signed URL you can drop into an ad account, a landing page, or your own pipeline. The same loop works for every model in the catalog — swap the model field and the accepted durationSec, resolution, and aspectRatio values change with it, but the four steps never do.
Step 1 — Create a key
Open /app/api-keys while signed in and generate a key. It is shown once — copy it into an environment variable immediately, because Reviral cannot show it to you again after you leave the page.
export REVIRAL_API_KEY="rvr_your_key_here"Keep it server-side. A key pasted into browser code or committed to a public repository is a key you should revoke and rotate, not a key you should keep using.
Step 2 — Read the catalog before you render
GET /api/v1/models is public — no key required — and it returns every callable model with its accepted durations, resolutions, aspect ratios, and current credit price. Read it before you hardcode a model ID, because the catalog is the only place that stays current when a price or a duration option changes.
curl "https://reviral.ai/api/v1/models"One entry from that response, trimmed to the fields this walkthrough uses:
{
"id": "seedance-2",
"kind": "video",
"label": "Seedance 2",
"durations": { "kind": "range", "min": 4, "max": 15 },
"resolutions": ["480p", "720p", "1080p", "4k"],
"aspects": ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"],
"credits": {
"720p": { "perSecond": 21.2, "creditsAtDefaultDuration": 106 }
},
"defaults": { "durationSec": 5, "resolution": "720p", "aspectRatio": "16:9" }
}That last block is the whole point of reading the catalog first: a 5-second, 720p clip on this model is 106 credits, computed live, before you spend anything. Nothing here is estimated.
Step 3 — Start the render
POST /api/v1/generate-media takes a model, a prompt, and — for video — durationSec, resolution, and aspectRatio. Add an Idempotency-Key header so a dropped connection or a retried request cannot start (and charge for) a second job by accident; send the same key with the same body and you get the original job back instead of a duplicate. maxCredits is optional but worth including — it locks the charge to the number you just read from the catalog, and the request fails with a clear error instead of debiting a different amount if the live price moved between your two calls.
curl -X POST "https://reviral.ai/api/v1/generate-media" \
-H "Authorization: Bearer $REVIRAL_API_KEY" \
-H "Idempotency-Key: first-ad-render" \
-H "Content-Type: application/json" \
--data '{
"model": "seedance-2",
"prompt": "A creator holds up a ceramic travel mug on a sunlit kitchen counter, turns it toward camera, and smiles",
"durationSec": 5,
"resolution": "720p",
"aspectRatio": "16:9",
"maxCredits": 106
}'The same request in Node, using the built-in fetch — no SDK to install:
const response = await fetch("https://reviral.ai/api/v1/generate-media", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.REVIRAL_API_KEY}`,
"Idempotency-Key": "first-ad-render",
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "seedance-2",
prompt: "A creator holds up a ceramic travel mug on a sunlit kitchen counter, turns it toward camera, and smiles",
durationSec: 5,
resolution: "720p",
aspectRatio: "16:9",
maxCredits: 106,
}),
});
if (response.status !== 202) {
const body = await response.json();
throw new Error(`Render did not start: ${response.status} ${JSON.stringify(body)}`);
}
const { data: job } = await response.json();
console.log(job.jobId, job.status); // "queued"A successful call returns 202 Accepted immediately — the video is not ready yet, only the job is. The response tells you what was actually charged and where to check on it next:
{
"data": {
"jobId": "8f14e45f-9d76-4e21-8b2a-2c9f6a11b7de",
"kind": "video",
"model": "seedance-2",
"creditsCharged": 106,
"status": "queued",
"statusUrl": "/api/v1/jobs/8f14e45f-9d76-4e21-8b2a-2c9f6a11b7de"
}
}Step 4 — Poll until it's ready
GET /api/v1/jobs/{id} returns the job's current state: queued, running, ready, or failed. Poll it — a sensible interval is every few seconds — until the status stops changing. This endpoint is limited to 120 requests per minute per key, which is generous enough that a simple polling loop never needs to think about the limit.
async function waitForJob(jobId) {
while (true) {
const response = await fetch(`https://reviral.ai/api/v1/jobs/${jobId}`, {
headers: { Authorization: `Bearer ${process.env.REVIRAL_API_KEY}` },
});
const { data: job } = await response.json();
if (job.status === "ready") return job;
if (job.status === "failed") throw new Error(job.error ?? "Render failed");
await new Promise((resolve) => setTimeout(resolve, 4000));
}
}
const job = await waitForJob("8f14e45f-9d76-4e21-8b2a-2c9f6a11b7de");
console.log(job.outputUrl);A finished job looks like this:
{
"data": {
"jobId": "8f14e45f-9d76-4e21-8b2a-2c9f6a11b7de",
"kind": "video",
"model": "seedance-2",
"status": "ready",
"outputUrl": "https://reviral.ai/.../signed-output.mp4",
"durationSec": 5,
"creditsCharged": 106
}
}outputUrl is signed and downloadable directly — pull it into your own storage, hand it straight to an ad platform's upload API, or serve it from wherever your pipeline expects a finished asset. If you would rather not poll at all, add a webhookUrl to the original request body and Reviral sends one best-effort callback to that URL when the job reaches a terminal state — useful for a queue worker, less useful if you need the result inline in the same request, since delivery is not retried on failure and polling stays the source of truth either way.
What it actually costs
Every model on Reviral shares one credit balance, and the API charges the same credit rate as the app. Credit prices differ by model, resolution, and sometimes duration: an 8-second Veo 3.1 Quality flat render is 50 credits at any resolution up to 4K, a Hailuo 2.3 flat render is 33 credits, and a GPT-Image-2 Standard image is 3 credits at any resolution. Read GET /api/v1/models before rendering for the current price and settings. The documentation pages and /api-status show a USD figure next to every credit price, and the cost is always shown before the charge.
New accounts start with 100 free credits and no card required, so the first several renders in this walkthrough cost nothing. Beyond that, packs bought once never expire; only the monthly subscription tiers refill and reset on a monthly cycle. Full numbers are on the pricing page and the machine-readable pricing.md.
Checking on a render without polling forever
reviral.ai/api-status is a live page, not a status claim — it publishes current per-model availability and price directly from the same catalog this walkthrough reads from. If a render is taking longer than the numbers above suggest, or a call comes back with a 502 or 503, check that page before assuming your own integration is broken; both codes mean the generation or status service is temporarily unavailable upstream, and the fix is a retry with the same idempotency key, not a code change. The errors reference also links an operational-notices channel for anyone who wants a push rather than a page to check.
Where the "60 seconds" number comes from
It is not a marketing round number — it is the same claim the quickstart page in the actual documentation makes about this exact loop: create a key, list models, generate, poll, download. This page walks through the same four steps with the same field names for a specific use case — an ad clip instead of a generic prompt — so you can see the real request and response bodies once, in context, rather than piecing them together from a longer reference.
If you want the guided studio instead of raw calls
Everything above is the same model catalog and render pipeline behind Reviral's own UGC ad generator — a browser studio that handles the product photo, the script, the creator, and the storyboard for you, with a free draft before anything renders. The API is the right tool when you already have your own product data, your own script, and want the render step wired into a pipeline you control; the studio is the right tool when you want a guided brief. Both spend from the same credit balance and the same live prices.
Questions people ask before their first API render
Do I need a subscription to call the API?
No. Pay-once credit packs cover API calls the same way they cover the app, and they never expire. The free tier's 100 signup credits work here too — no card required to start.
What happens if I retry a request that already succeeded?
Send the same Idempotency-Key with the same request body and you get the original job back, not a second charge. Send a different body with a key you already used and the request is rejected as a conflict — that is the API refusing to guess which version you meant.
Can I lock the exact price before I render?
Yes — set maxCredits to the number you read from GET /api/v1/models. If the live charge would be different by the time your request is processed, the call fails with a 409 price_changed error instead of debiting a different amount than you approved.