Reelora API
Generate images and video from text or reference frames — the same engine that powers Reelora Studio, exposed as a plain REST API.
v1 beta
Base URL https://reelora.cc/v1
Introduction #
The API is asynchronous: you submit a request, get back one or more
generation objects with status: "queued", and then either poll them or
receive a webhook. A single request can create up to 500 generations, and you can keep
up to 1000 in flight at a time.
What v1 covers
| Supported | Not in v1 |
|---|---|
| Text → image, text → video, image → video, keyframe transitions, reference-based images, file uploads, batching, cancellation | Video editing / montage, voiceover, subtitles, transcription, storyboard automation, result streaming |
/v1 is versioned. Additive changes
(new fields, new models) ship without notice; breaking changes ship as /v2.Quickstart #
- Create an API key — open Reelora → Account → API keys → Create key. The full key is shown exactly once (only a hash is stored), so put it straight into a secret manager. Never ship it to a browser or mobile app.
- Submit a generation
curl https://reelora.cc/v1/images/generations \ -H "Authorization: Bearer $REELORA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "a cat astronaut floating over neon Tokyo, cinematic", "model": "nano-banana-2", "aspect": "16:9", "resolution": "1K" }' - Poll until it finishes
curl https://reelora.cc/v1/generations/gen_a1b2c3d4e5f6 \ -H "Authorization: Bearer $REELORA_API_KEY" - Download the result —
output.urlis a pre-signed link, valid for 24 hours, that needs no authentication header.curl -L -o result.jpg "<output.url>"
Authentication #
Every request carries your key in the Authorization header. The
X-API-Key header is accepted as an alternative for tools that cannot set
Authorization.
Authorization: Bearer rl_live_a1b2c3d4e5f6_0123456789abcdef0123456789abcdef| Rule | Detail |
|---|---|
| Format | rl_<env>_<key_id>_<secret>, where env is live or test |
| Storage | Only a SHA-256 hash is stored — a lost key cannot be recovered, only rotated |
| Rotation | Rotating issues a new key and revokes the old one immediately |
| Server-side only | The key grants spending rights on your balance. Never ship it to a browser or mobile app |
| Verified email | Required — otherwise 403 email_not_verified |
Restricting a key
Each key can carry two optional limits, chosen when you create it. With them, a leaked key costs exactly what you allotted it — and nothing more.
| Limit | If exceeded | Notes |
|---|---|---|
| Expiry date | 403 key_expired | Useful for contractors and trials — the key dies on its own. |
| Budget (tokens) | 402 key_budget_exceeded | A spending ceiling for this key, separate from your account balance. Failed generations are refunded and do not count against it. |
Rotating a key carries its limits over to the replacement, so rotation never
silently drops your protection. GET /v1/account reports the current
state of the key you are using, under key.
Pricing & tokens #
Usage is billed in tokens drawn from your account balance. Price depends on
the model and its parameters — a Veo clip and a 15-second Grok clip in Full HD are
not remotely the same cost. Each model reports its own range in
GET /v1/models under price_tokens; the
figures below are only there to give you a sense of scale.
| Example | Tokens | Notes |
|---|---|---|
| Nano Banana 2, one image | 38 | same price at any resolution; 4K requires a Pro plan |
GPT Image 2, low · 1K | 75 | rises to 1 175 at high · 4K |
| Veo 3.1 Lite, one clip | 60 | fixed 8 seconds, same in 720p and 1080p |
| Grok Lower, 480p · 6 s | 95 | 1 238 at 1080p · 15 s |
Kling 2.6 standard | 75 | per second — a 10-second clip is 750 |
Cost is reserved when the request is accepted and charged only when a
generation succeeds. Failed and canceled generations are refunded automatically — you
never pay for output you did not receive. If the balance is short, the whole request is
rejected with 402 insufficient_credits and nothing is queued.
Generation modes #
The mode is inferred from which fields you send — there is no mode
parameter to set.
Text → image
A prompt becomes a still frame.
POST /v1/images/generations
{"prompt": "…"}Text → video
A prompt becomes an 8-second clip.
POST /v1/videos/generations
{"prompt": "…"}Image → video
Your frame becomes the first frame of a clip.
POST /v1/videos/generations
{"image_file_id": "file_…",
"prompt": "slow dolly in"}Keyframe chain
N ordered frames → N−1 transition clips (A→B, B→C, …).
POST /v1/videos/generations
{"keyframe_file_ids":
["file_a","file_b","file_c"]}Reference image
Up to 3 reference images blended into one new frame — for consistent characters, products or locations.
POST /v1/images/generations
{"prompt": "…",
"reference_file_ids":
["file_a","file_b"]}Prompt-count rule for batches
When a request targets N items (N files, or N clips in a keyframe chain):
| Prompts sent | Behaviour |
|---|---|
| 0 | a sensible default motion / transition prompt is used |
| 1 | the same prompt applies to every item |
| exactly N | matched one-to-one, in order |
| anything else | 400 prompt_count_mismatch |
Async & polling #
Statuses: queued → running → succeeded |
failed, plus canceled.
| Type | Typical time | Suggested poll interval |
|---|---|---|
| image | 10–40 s | every 3 s |
| video | 1–6 min | every 10 s |
Generations that exceed the provider timeout end as failed and are
refunded. Poll a batch efficiently with
GET /v1/generations?status=running rather than one
request per id.
Webhooks #
Pass webhook_url (HTTPS, publicly routable) on a generation request and
Reelora POSTs the generation object when it finishes.
POST https://your.app/hooks/reelora
X-Reelora-Event: generation.succeeded
X-Reelora-Signature: t=1756512240,v1=<hex hmac_sha256(secret, "$t.$body")>Verify the signature before trusting the payload, respond 2xx within
10 s. Failed deliveries retry 3 times (5 s, 30 s, 5 min). Redirects are not followed, and
the body is the generation object under data.
import hmac, hashlib
def verify(secret: str, header: str, body: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
expected = hmac.new(secret.encode(),
f'{parts["t"]}.{body}'.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Your signing secret is shown next to the key in Account → API keys.
GET /v1/models #
Every model with the exact parameters it accepts and what it costs. No API key required. Read this endpoint instead of hard-coding model ids — new models appear here first, and each one has its own resolutions, durations and quality modes.
{
"image": {
"models": [
{
"id": "nano-banana-2",
"label": "Nano Banana 2",
"provider": "google",
"aspects": ["1:1","16:9","9:16","4:3","3:4"],
"resolutions": ["1K","2K"],
"pro_only_resolutions": [],
"durations_sec": [],
"quality_modes": [],
"backgrounds": [],
"max_reference_files": 3,
"reference_modes": ["ingredient"],
"audio": false,
"audio_toggle": false,
"max_results": 1,
"pro_only": false,
"price_tokens": {"min": 38, "max": 38}
},
{"id": "grok-image", "quality_modes": ["SPEED","QUALITY"], "max_results": 8, "…": "…"},
{"id": "gpt-image-2", "quality_modes": ["low","medium","high"],
"backgrounds": ["auto","transparent","opaque"], "pro_only": true, "…": "…"}
],
"default": "nano-banana-2"
},
"video": {
"models": [
{"id": "veo-3.1-lite", "resolutions": ["720p","1080p"], "durations_sec": [8],
"price_tokens": {"min": 60, "max": 60}, "…": "…"},
{"id": "grok-lower", "resolutions": ["480p","720p","1080p"],
"durations_sec": [6,10,15], "quality_modes": ["custom","normal"],
"audio_toggle": true, "price_tokens": {"min": 95, "max": 1238}, "…": "…"},
{"id": "kling-video-2-6", "durations_sec": [5,10],
"quality_modes": ["standard","professional","professional_audio"], "…": "…"}
],
"default": "veo-3.1-lite"
},
"max_batch": 500,
"max_queued": 1000,
"available": true
}| Field | Meaning |
|---|---|
durations_sec | Clip lengths the model accepts. Empty for images. A single value means the length is fixed. |
quality_modes | Values allowed in quality_mode. Empty means the model has none. |
backgrounds | Values allowed in background (GPT Image only). |
max_reference_files | How many reference images this model takes. |
reference_modes | frame (start/end frames) and/or ingredient (blend into one shot). |
audio / audio_toggle | Whether the model produces sound, and whether you may switch it off with audio. |
price_tokens | Cost range across all valid parameter combinations. Equal min and max means one fixed price. |
400 invalid_request
with the allowed list — we never silently downgrade your request, because you would
only find out from the invoice.POST /v1/images/generations #
| Field | Type | Description |
|---|---|---|
prompt | string | A single prompt. Mutually exclusive with prompts. |
prompts | string[] | Batch — one image per prompt. |
n | integer | Variants per prompt. 1–10, default 1. |
model | string | Any id from GET /v1/models. Default nano-banana-2. Nano Banana, Grok Image and GPT Image are available. |
resolution | string | One of the model's resolutions. 4K is Pro-only. |
aspect | string | One of the model's aspects. Default 16:9. |
quality_mode | string | Models that have one: Grok Image SPEED|QUALITY, GPT Image low|medium|high. Changes the price — see price_tokens. |
background | string | GPT Image only: auto | transparent | opaque. |
reference_file_ids | string[] | Uploaded images to blend, up to the model's max_reference_files (3 for Nano Banana, 4 for Grok Image, 5 for GPT Image). Switches to reference mode (single prompt only). |
webhook_url | string | HTTPS callback. |
metadata | object | Up to 16 string key/values, echoed back. |
Response 202 Accepted
{
"id": "batch_9f2c1a7d4b30",
"object": "batch",
"created": 1756512000,
"generations": [
{
"id": "gen_a1b2c3d4e5f6",
"object": "generation",
"type": "image",
"status": "queued",
"mode": "text2image",
"model": "nano-banana-2",
"aspect": "16:9",
"resolution": "1K",
"prompt": "a cat astronaut floating over neon Tokyo, cinematic",
"output": null,
"error": null,
"cost_tokens": 35,
"created": 1756512000,
"metadata": {}
}
],
"cost_tokens": 35,
"balance_tokens": 12480
}POST /v1/videos/generations #
Clip length depends on the model: Veo is fixed at 8 seconds, Grok accepts 6, 10 or
15, Kling and Seedance are priced per second. Check durations_sec in
GET /v1/models before you send duration.
| Field | Type | Description |
|---|---|---|
prompt / prompts | string / string[] | See the prompt-count rule above. |
n | integer | Variants per prompt (text→video only). |
model | string | Any id from GET /v1/models. Default veo-3.1-lite. Veo, Omni Flash, Grok, Kling and Seedance are available. |
resolution | string | One of the model's resolutions. Kling has none — there the resolution comes from quality_mode. |
duration | integer | Clip length in seconds, one of the model's durations_sec. Omit to use the model's default. |
quality_mode | string | Models that have one, e.g. Grok custom|normal, Kling standard|professional. Changes the price. |
aspect | string | One of the model's aspects. |
audio | boolean | Switch the model's own soundtrack off. Only for models with audio_toggle (Grok). |
image_file_id | string | Start frame → image-to-video. |
image_file_ids | string[] | Batch of start frames — one clip each. |
keyframe_file_ids | string[] | ≥2 ordered frames → N−1 transition clips. |
video_file_ids | string[] | Source clips → video-to-video: the model reworks an existing clip from your prompt. Only models with reference_modes that accept video (Omni Flash, Seedance Omni). |
audio_file_ids | string[] | Audio references (voice or rhythm the model follows). Seedance Omni only, up to 15 seconds each. |
webhook_url, metadata | As for images. |
curl https://reelora.cc/v1/videos/generations \
-H "Authorization: Bearer $REELORA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image_file_id": "file_1a2b3c4d5e6f",
"prompt": "slow dolly in, dust motes drifting through the light",
"model": "veo-3.1-lite",
"resolution": "1080p"
}'GET /v1/generations/:id #
{
"id": "gen_a1b2c3d4e5f6",
"object": "generation",
"type": "video",
"status": "succeeded",
"model": "veo-3.1-lite",
"aspect": "16:9",
"resolution": "720p",
"prompt": "slow dolly in…",
"output": {
"url": "https://reelora.cc/v1/files/a1b2c3d4e5f6/content?u=…&exp=1756598400&sig=…",
"poster_url": "https://reelora.cc/v1/files/a1b2c3d4e5f6/content?…&kind=poster",
"mime": "video/mp4",
"expires_at": 1756598400
},
"error": null,
"cost_tokens": 60,
"created": 1756512000,
"metadata": {}
}GET /v1/generations #
| Query | Description |
|---|---|
status | queued | running | succeeded | failed |
type | image | video |
limit | 1–100, default 20 |
starting_after | cursor — a generation id from the previous page |
{"object":"list","data":[ /* generations, newest first */ ],
"has_more":true,"next_cursor":"gen_a1b2c3d4e5f6"}POST /v1/generations/:id/cancel #
Cancels a generation that is still queued and refunds its reservation.
Once a generation reaches running it has been submitted to the model
provider and can no longer be stopped — the call returns
409 already_running.
Deleting generations #
Results stay on our servers after they finish, so you can fetch them later and
your users can see them in their account. That also means your storage fills up.
When it is full, new generations are rejected with
413 storage_full — so clean up after you have downloaded your files.
Deleting removes the file and the history row permanently. It cannot be undone.
One at a time
curl -X DELETE https://reelora.cc/v1/generations/gen_9f5bef69b0a6 -H "Authorization: Bearer $RELORA_API_KEY"
{"id": "gen_9f5bef69b0a6", "object": "generation", "deleted": true}In bulk
Pass exactly one of ids or older_than_days.
# explicit list (up to 500 per request)
curl -X POST https://reelora.cc/v1/generations/delete -H "Authorization: Bearer $RELORA_API_KEY" -H "Content-Type: application/json" -d '{"ids": ["gen_9f5bef69b0a6", "gen_a83908ed01c6"]}'
# or sweep everything older than a week — the usual nightly cron
curl -X POST https://reelora.cc/v1/generations/delete -H "Authorization: Bearer $RELORA_API_KEY" -H "Content-Type: application/json" -d '{"older_than_days": 7}'
{
"object": "delete_result",
"deleted": 42,
"storage": {"bytes_used": 1173741824, "bytes_quota": 2147483648,
"bytes_remaining": 973741824, "unlimited": false, "percent": 55}
}The response carries the fresh storage block, so a cleanup job can
loop until bytes_remaining is comfortable again. Keep an eye on the same
block in GET /v1/account and delete before you hit
the wall, rather than after.
POST /v1/files #
Upload a reference, a start frame, a source clip or an audio reference. Send the raw
bytes as the request body — not multipart. The file type is detected from the bytes
themselves, so a wrong Content-Type is harmless but a mislabelled file is
rejected with 415.
?kind= | Formats | Limit |
|---|---|---|
image (default) | JPEG, PNG, WebP | 20 MB |
video | MP4, WebM, AVI, FLV, MPEG | 200 MB — for video_file_ids |
audio | MP3, M4A, WAV, OGG, FLAC | 20 MB, trimmed to 15 s — for audio_file_ids |
curl https://reelora.cc/v1/files \
-H "Authorization: Bearer $REELORA_API_KEY" \
-H "Content-Type: image/jpeg" \
-H "X-Filename: hero.jpg" \
--data-binary @hero.jpgcurl "https://reelora.cc/v1/files?kind=video" -H "Authorization: Bearer $REELORA_API_KEY" -H "X-Filename: source.mp4" --data-binary @source.mp4{"id":"file_1a2b3c4d5e6f","object":"file","kind":"image",
"filename":"hero.jpg","bytes":184320,"created":1756512000,
"url":"https://reelora.cc/v1/files/…/content?u=…&exp=…&sig=…","expires_at":1756598400}GET /v1/files/:id/content #
Returns the binary file. Two ways to authorise:
| Method | When to use |
|---|---|
| Bearer key | server-to-server download |
| Signed link | the output.url you already received — works in a browser or CDN, expires after 24 h |
Add &kind=poster to fetch a video's poster frame. Expired or tampered
links return 403 invalid_signature.
GET /v1/account #
{
"object": "account",
"plan": "pro",
"balance_tokens": 12480,
"tokens_per_usd": 1000,
"limits": {"rpm": 60, "concurrent_generations": 8, "max_batch": 500, "max_queued": 1000},
"prices_tokens": {"image": 35, "video": 60},
"storage": {
"bytes_used": 1173741824,
"bytes_quota": 2147483648,
"bytes_remaining": 973741824,
"unlimited": false,
"percent": 55
},
"key": {
"id": "2f76ae81b134",
"expires_at": "2026-11-28T09:00:00+00:00",
"budget_tokens": 5000,
"budget_used_tokens": 1240
}
}When bytes_remaining reaches 0, new generations are rejected with
413 storage_full. Anything already queued or running still finishes —
we never abandon work you have paid for. See
deleting generations for how to free space.
Errors #
Every failure returns the same shape, so one handler covers all of them.
{
"error": {
"type": "billing_error",
"code": "insufficient_credits",
"message": "Not enough tokens. (need=60, have=12)",
"param": null
}
}| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_request | Malformed or contradictory fields |
| 400 | prompt_count_mismatch | Prompt count is neither 1 nor N |
| 400 | batch_too_large | More than 500 generations in one request |
| 401 | invalid_api_key | Missing, malformed or revoked key |
| 402 | insufficient_credits | Balance too low; nothing was queued |
| 403 | email_not_verified | Verify your account email |
| 403 | pro_required | 4K needs a Pro plan |
| 403 | invalid_signature | Signed link expired or altered |
| 403 | key_expired | The key passed its expiry date |
| 402 | key_budget_exceeded | This key's own budget is used up |
| 404 | not_found | Unknown id, or it belongs to another account |
| 409 | already_running | Cannot cancel — already with the provider |
| 413 | storage_full | Account storage is full — delete some generations |
| 413 | file_too_large | Upload exceeds the limit for its kind (20 MB images/audio, 200 MB video) |
| 415 | unsupported_media_type | Bytes are not a supported image |
| 429 | rate_limit_exceeded | Slow down; see Retry-After |
| 429 | queue_full | Too many generations already in flight; let some finish |
| 503 | provider_unavailable | Generation backend is down; retry shortly |
Retry 429 and 503 with exponential backoff.
Do not retry 4xx validation errors — they will fail identically.
Rate limits #
| Limit | Default | Notes |
|---|---|---|
| Requests per minute | 60 / key | Per key, sliding window |
| Concurrent generations | 4 Free · 8 Pro | Extra work waits in your queue, it is not rejected |
| Generations per request | 500 | Prompts, frames or transitions combined |
| Generations in flight | 1000 | Queued + running across all your requests; over this → queue_full |
Variants per prompt (n) | 10 | |
| Upload size | 20 / 200 MB | Images and audio 20 MB, source video 200 MB |
| Result retention | 7 days Free · 30 days Pro | Download what you need to keep |
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining
and X-RateLimit-Reset (seconds until the window resets) — read them to pace
yourself instead of waiting for a 429.
Idempotency #
Send an Idempotency-Key header on any POST. A repeat of the same key
within 24 hours returns the original response with
Idempotent-Replay: true instead of spending tokens twice — which makes
network retries safe.
-H "Idempotency-Key: 8f14e45f-ea0c-4b39-9f6d-2a1c7b3e9d40"Code examples #
Generate and wait for the result:
import os, time, requests
API = "https://reelora.cc/v1"
AUTH = {"Authorization": f"Bearer {os.environ['REELORA_API_KEY']}"}
def generate_image(prompt, **opts):
r = requests.post(f"{API}/images/generations",
headers=AUTH, json={"prompt": prompt, **opts}, timeout=30)
r.raise_for_status()
return r.json()["generations"][0]["id"]
def wait(gen_id, every=3, timeout=900):
deadline = time.time() + timeout
while time.time() < deadline:
g = requests.get(f"{API}/generations/{gen_id}", headers=AUTH, timeout=30).json()
if g["status"] in ("succeeded", "failed", "canceled"):
return g
time.sleep(every)
raise TimeoutError(gen_id)
gen = wait(generate_image("a cat astronaut over neon Tokyo", aspect="9:16"))
if gen["status"] != "succeeded":
raise RuntimeError(gen["error"]["message"])
open("out.jpg", "wb").write(requests.get(gen["output"]["url"]).content)const API = "https://reelora.cc/v1";
const auth = { Authorization: `Bearer ${process.env.REELORA_API_KEY}` };
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function generateVideo(body) {
const res = await fetch(`${API}/videos/generations`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error((await res.json()).error.message);
return (await res.json()).generations;
}
async function wait(id, every = 10_000) {
for (;;) {
const g = await (await fetch(`${API}/generations/${id}`, { headers: auth })).json();
if (["succeeded", "failed", "canceled"].includes(g.status)) return g;
await sleep(every);
}
}
const [gen] = await generateVideo({ prompt: "rain on a neon street", resolution: "1080p" });
const done = await wait(gen.id);
console.log(done.status === "succeeded" ? done.output.url : done.error.message);# submit
ID=$(curl -s https://reelora.cc/v1/images/generations \
-H "Authorization: Bearer $REELORA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt":"a cat astronaut over neon Tokyo"}' \
| jq -r '.generations[0].id')
# poll
while :; do
J=$(curl -s "https://reelora.cc/v1/generations/$ID" -H "Authorization: Bearer $REELORA_API_KEY")
S=$(echo "$J" | jq -r .status)
[ "$S" = "queued" ] || [ "$S" = "running" ] || break
sleep 3
done
# download
echo "$J" | jq -r '.output.url' | xargs curl -sL -o out.jpgFAQ #
Can I stream results as they render?
No. Generation is a batch process on the provider side; poll or use a webhook.
How long are results stored?
7 days on Free, 30 days on Pro. Signed links expire after 24 hours but can be re-issued by fetching the generation again while the file still exists.
There is also a size limit — 2 GB on Free, 5 GB on Pro — shared with everything
you make in the web app. Whichever comes first wins: age or size. Check
storage in GET /v1/account and
delete what you have already downloaded.
Do failed generations cost tokens?
No. The reservation is released automatically and no charge is recorded.
Can one key be shared across environments?
You can, but don't — issue one key per environment so a leak can be revoked without
downtime elsewhere. Keys carry an env tag (live /
test) to keep them apart in the dashboard.
Is there a machine-readable spec?
Yes — /v1/openapi.json, importable into
Postman or Insomnia.
What content is allowed?
The upstream models apply their own safety filters; rejected prompts come back as a
failed generation and are not charged. Your usage must also follow the
Reelora Terms of Service.
Need something that isn't here — montage, voiceover, subtitles? Those ship in a later version. Tell us what you need at support@reelora.cc.