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

SupportedNot 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
Everything under /v1 is versioned. Additive changes (new fields, new models) ship without notice; breaking changes ship as /v2.

Quickstart #

  1. Create an API key — open ReeloraAccountAPI keysCreate 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.
  2. 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"
      }'
  3. Poll until it finishes
    curl https://reelora.cc/v1/generations/gen_a1b2c3d4e5f6 \
      -H "Authorization: Bearer $REELORA_API_KEY"
  4. Download the resultoutput.url is 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
RuleDetail
Formatrl_<env>_<key_id>_<secret>, where env is live or test
StorageOnly a SHA-256 hash is stored — a lost key cannot be recovered, only rotated
RotationRotating issues a new key and revokes the old one immediately
Server-side onlyThe key grants spending rights on your balance. Never ship it to a browser or mobile app
Verified emailRequired — 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.

LimitIf exceededNotes
Expiry date403 key_expiredUseful for contractors and trials — the key dies on its own.
Budget (tokens)402 key_budget_exceededA 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.

ExampleTokensNotes
Nano Banana 2, one image38same price at any resolution; 4K requires a Pro plan
GPT Image 2, low · 1K75rises to 1 175 at high · 4K
Veo 3.1 Lite, one clip60fixed 8 seconds, same in 720p and 1080p
Grok Lower, 480p · 6 s951 238 at 1080p · 15 s
Kling 2.6 standard75per 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 sentBehaviour
0a sensible default motion / transition prompt is used
1the same prompt applies to every item
exactly Nmatched one-to-one, in order
anything else400 prompt_count_mismatch

Async & polling #

Statuses: queuedrunningsucceeded | failed, plus canceled.

TypeTypical timeSuggested poll interval
image10–40 severy 3 s
video1–6 minevery 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 #

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
}
FieldMeaning
durations_secClip lengths the model accepts. Empty for images. A single value means the length is fixed.
quality_modesValues allowed in quality_mode. Empty means the model has none.
backgroundsValues allowed in background (GPT Image only).
max_reference_filesHow many reference images this model takes.
reference_modesframe (start/end frames) and/or ingredient (blend into one shot).
audio / audio_toggleWhether the model produces sound, and whether you may switch it off with audio.
price_tokensCost range across all valid parameter combinations. Equal min and max means one fixed price.
Sending a value the model does not accept returns 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 #

POST /v1/images/generations
FieldTypeDescription
promptstringA single prompt. Mutually exclusive with prompts.
promptsstring[]Batch — one image per prompt.
nintegerVariants per prompt. 1–10, default 1.
modelstringAny id from GET /v1/models. Default nano-banana-2. Nano Banana, Grok Image and GPT Image are available.
resolutionstringOne of the model's resolutions. 4K is Pro-only.
aspectstringOne of the model's aspects. Default 16:9.
quality_modestringModels that have one: Grok Image SPEED|QUALITY, GPT Image low|medium|high. Changes the price — see price_tokens.
backgroundstringGPT Image only: auto | transparent | opaque.
reference_file_idsstring[]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_urlstringHTTPS callback.
metadataobjectUp 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 #

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.

FieldTypeDescription
prompt / promptsstring / string[]See the prompt-count rule above.
nintegerVariants per prompt (text→video only).
modelstringAny id from GET /v1/models. Default veo-3.1-lite. Veo, Omni Flash, Grok, Kling and Seedance are available.
resolutionstringOne of the model's resolutions. Kling has none — there the resolution comes from quality_mode.
durationintegerClip length in seconds, one of the model's durations_sec. Omit to use the model's default.
quality_modestringModels that have one, e.g. Grok custom|normal, Kling standard|professional. Changes the price.
aspectstringOne of the model's aspects.
audiobooleanSwitch the model's own soundtrack off. Only for models with audio_toggle (Grok).
image_file_idstringStart frame → image-to-video.
image_file_idsstring[]Batch of start frames — one clip each.
keyframe_file_idsstring[]≥2 ordered frames → N−1 transition clips.
video_file_idsstring[]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_idsstring[]Audio references (voice or rhythm the model follows). Seedance Omni only, up to 15 seconds each.
webhook_url, metadataAs 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 #

GET /v1/generations/{generation_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 #

GET /v1/generations
QueryDescription
statusqueued | running | succeeded | failed
typeimage | video
limit1–100, default 20
starting_aftercursor — 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 #

POST /v1/generations/{generation_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 #

DELETE /v1/generations/{generation_id}
POST /v1/generations/delete

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 #

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=FormatsLimit
image (default)JPEG, PNG, WebP20 MB
videoMP4, WebM, AVI, FLV, MPEG200 MB — for video_file_ids
audioMP3, M4A, WAV, OGG, FLAC20 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.jpg
curl "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}
Uploads are normalised to the project aspect ratio before they reach the model, so mixed-size references stay consistent.

GET /v1/files/:id/content #

GET /v1/files/{file_id}/content

Returns the binary file. Two ways to authorise:

MethodWhen to use
Bearer keyserver-to-server download
Signed linkthe 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 #

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
  }
}
HTTPcodeMeaning
400invalid_requestMalformed or contradictory fields
400prompt_count_mismatchPrompt count is neither 1 nor N
400batch_too_largeMore than 500 generations in one request
401invalid_api_keyMissing, malformed or revoked key
402insufficient_creditsBalance too low; nothing was queued
403email_not_verifiedVerify your account email
403pro_required4K needs a Pro plan
403invalid_signatureSigned link expired or altered
403key_expiredThe key passed its expiry date
402key_budget_exceededThis key's own budget is used up
404not_foundUnknown id, or it belongs to another account
409already_runningCannot cancel — already with the provider
413storage_fullAccount storage is full — delete some generations
413file_too_largeUpload exceeds the limit for its kind (20 MB images/audio, 200 MB video)
415unsupported_media_typeBytes are not a supported image
429rate_limit_exceededSlow down; see Retry-After
429queue_fullToo many generations already in flight; let some finish
503provider_unavailableGeneration backend is down; retry shortly

Retry 429 and 503 with exponential backoff. Do not retry 4xx validation errors — they will fail identically.

Rate limits #

LimitDefaultNotes
Requests per minute60 / keyPer key, sliding window
Concurrent generations4 Free · 8 ProExtra work waits in your queue, it is not rejected
Generations per request500Prompts, frames or transitions combined
Generations in flight1000Queued + running across all your requests; over this → queue_full
Variants per prompt (n)10
Upload size20 / 200 MBImages and audio 20 MB, source video 200 MB
Result retention7 days Free · 30 days ProDownload 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.

Nothing is rejected for being busy: work above your concurrency limit waits in your own queue. 1000 images take roughly 2–4 hours at 4–8 at a time — plan for that rather than expecting an immediate result.

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.jpg

FAQ #

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.