DEVELOPER API

Build with the
RoyalTechno API

The same AI models behind the bot — Veo 3.1, Seedance, Nano Banana and ElevenLabs voices — video, images and voiceovers over a clean REST API. Async submit, status polling, stable storage URLs. Built for batch pipelines.

BASE URLhttps://api.royaltechno.cc/v1
QUICKSTART

Your first clip in three calls

Authenticate, submit a job, and receive the finished video or image by polling its status.

  1. 1

    Authenticate

    Pass your key as Authorization: Bearer. Get one from the bot with /api_key_new.

  2. 2

    Submit a job

    POST a model and an input block (prompt, aspect_ratio). You get back a job id instantly.

  3. 3

    Receive the result

    Poll GET /v1/jobs/{id} until status is succeeded.

Video

curl https://api.royaltechno.cc/v1/jobs \  -H "Authorization: Bearer $RT_API_KEY" \  -H "Content-Type: application/json" \  -d '{    "model": "veo-3.1",    "input": {      "prompt": "a cinematic drone shot over snowy mountains at sunrise",      "aspect_ratio": "landscape",      "duration_sec": 8    }  }'

Image — same endpoint, image model

curl https://api.royaltechno.cc/v1/jobs \  -H "Authorization: Bearer $RT_API_KEY" \  -H "Content-Type: application/json" \  -d '{    "model": "nano-banana-pro",    "input": {      "prompt": "a royal crown made of glass, studio lighting",      "aspect_ratio": "landscape"    }  }'

Image → video — animate a still you generated

# output.url of a finished image job — public + stable, so it goes# straight into the next request. No download, no re-hosting, no base64.curl https://api.royaltechno.cc/v1/jobs \  -H "Authorization: Bearer $RT_API_KEY" \  -H "Content-Type: application/json" \  -d '{    "model": "veo-3.1",    "input": {      "prompt": "the crown slowly rotates, light glinting across the glass",      "start_image_url": "https://cdn.royaltechno.cc/saas/images/job_9fK2xQ.png"    }  }'
// The output.url of a finished image job is public and stable, so it goes straight into the next job's start_image_url — no download, re-hosting or base64 in between. Any HTTPS URL or data URI of your own works too. On Veo, the Seedance 2.0 family and Omni 1.1 Flash, add end_image_url for first+last frame: the model animates the transition between the two images. On the Seedance 2.0 family both frames must have the same aspect ratio: a difference of a few percent is corrected automatically, a larger one fails the job with invalid_input (refunded) — send both frames at the same pixel size. Veo has no such rule.
POSTResponse · 201 Created
{  "id": "job_3Xk9aQ2",  "object": "job",  "model": "veo-3.1",  "status": "queued",  "input": {    "prompt": "a cinematic drone shot over snowy mountains at sunrise",    "aspect_ratio": "landscape",    "duration_sec": 8  },  "cost_usd_cents": 2,  "created_at": "2026-07-29T09:41:00Z"}
GETPoll · GET /v1/jobs/{id}
# Poll every 10 s (video) / 5 s (images), all your jobs from ONE loop, sending# the last ETag as If-None-Match → 304 with no body when nothing changed.# On 429 sleep Retry-After. While queued, `queue_position` shows your place# among jobs of the same media type; `retrying: true` = an automatic re-run.# Video takes minutes — never resubmit a slow job: you would pay twice.GET https://api.royaltechno.cc/v1/jobs/job_3Xk9aQ2If-None-Match: "3f9c1e2b7a4d5c60"​{  "id": "job_3Xk9aQ2",  "status": "succeeded",  "output": {    "type": "video",    "url": "https://cdn.royaltechno.cc/saas/videos/job_3Xk9aQ2.mp4",    "mime_type": "video/mp4",    "duration_sec": 8  },  "cost_usd_cents": 2,​  // null unless the job shipped below what you ordered (e.g. an upstream  // upscale is down and 1080p came back as 720p). Never silent.  "notice": null}
RULES FOR SCRIPTS & AI AGENTS

Write a client that never sees a 429

Every limit here is enforced. Follow these rules and you are never throttled; ignore them and your key is slowed, then blocked. Live numbers for your own key: GET /v1/account → limits.

Poll every 10 s for video, 5 s for images

Send the last ETag as If-None-Match — a 304 means nothing changed, keep the same interval. Chains: every 30 s. Never faster than every 5 s, and poll all your jobs from ONE loop, not a thread per job.

Keep at most 20 jobs in flight per key

Generation runs at your plan's concurrency per family (2–10 by plan, 10 for wallet-funded jobs), up to ×4 while the fleet is idle, and never more than 20 per key. Anything beyond only waits in the queue (cap 100) and finishes no sooner — submit the next job when one completes.

Never resubmit a queued or processing job

Slow is not failed: video takes minutes, tens of minutes at peak — queue_position shows your place among jobs of the same media type, and retrying: true means an automatic re-run. Resubmitting doubles your spend and the load.

On 429: sleep Retry-After, retry once, then back off

2 s doubling to 60 s (± 20 %), 6 attempts, same Idempotency-Key. On 429 queue_full never retry the POST: poll what you have and submit again after one finishes (Retry-After 60 s). Ignoring Retry-After gains nothing — refused calls are slowed to ~2 s, 30 refusals in a minute pause the endpoint for 120 s, and persistent flooding is blocked with 403 access_banned for 6 h.

Stop on 400 / 401 / 402 / 403 — retry only 503

Those never clear by retrying: fix the request, the key, the balance or plan, or wait out the ban. A 503 is the one refusal worth an automatic retry (same backoff, same Idempotency-Key). Exception: a 503 model_unavailable with param input.resolution means only that resolution is off sale — the message names the ones you can order; change the resolution instead of retrying.

One Idempotency-Key per job · 25 req/s ceiling

One UUID per distinct job, reused only for its retries: a different body under a reused key returns the first response unchanged for 24 h, with no error. An obedient client sends well under 1 request/s; the ceiling is 25 req/s (bursts to 100) per key and 50 req/s (bursts to 200) per IP. There is no bulk endpoint — loop with the window above.

Reference client — copy it as-is (Python · Node)

#!/usr/bin/env python3"""RoyalTechno AI - reference API client (Python 3.9+, `requests` only).​Follows every rule in https://royaltechno.ai/api-docs#rules, so copy it as-is:a sliding window of MAX_IN_FLIGHT jobs, ONE poll loop for all jobs withIf-None-Match, Retry-After + exponential backoff on 429/503, stop on 4xx,one Idempotency-Key per job, and it never resubmits a job that is in flight.​    RT_API_KEY=rt_live_... python rt_client.py --model nano-banana-pro \\        --prompt "a royal crown made of glass, studio lighting" --count 2​Live numbers for YOUR key: GET /v1/account -> "limits" (they can be higherthan the defaults below after a per-key allowance)."""import argparseimport osimport randomimport timeimport uuid​import requests​API = "https://api.royaltechno.cc/v1"MAX_IN_FLIGHT = 20                  # per API key: GET /v1/account -> limits.recommended_max_in_flightPOLL_SEC = {"image": 5, "video": 10, "chain": 30}BACKOFF_BASE_SEC, BACKOFF_MAX_SEC, MAX_ATTEMPTS = 2, 60, 6RETRYABLE = {429, 503}              # honour Retry-After, then back offFATAL = {400, 401, 402, 403, 404}   # never clears by retrying: fix the request, key, balance or planTIMEOUT_SEC = 30​session = requests.Session()session.headers["Authorization"] = f"Bearer {os.environ['RT_API_KEY']}"​​class Fatal(Exception):    """A refusal that retrying can never fix."""​​def backoff(attempt):    """2 s doubling to 60 s, +/- 20 % jitter so retries never line up."""    return min(BACKOFF_MAX_SEC, BACKOFF_BASE_SEC * 2 ** attempt) * random.uniform(0.8, 1.2)​​def request(method, path, **kw):    """One API call with the rules baked in: 4xx raises, 429/503 sleep for    max(Retry-After, backoff) and retry, at most MAX_ATTEMPTS tries.    A `queue_full` 429 is returned to the caller instead of retried: the    right reaction is to wait for one of YOUR jobs to finish, not to POST again."""    for attempt in range(MAX_ATTEMPTS):        r = session.request(method, f"{API}{path}", timeout=TIMEOUT_SEC, **kw)        if r.status_code in FATAL:            raise Fatal(r.json()["error"])        if r.status_code not in RETRYABLE:            return r        if r.json().get("error", {}).get("code") == "queue_full":            return r        time.sleep(max(float(r.headers.get("Retry-After", 0)), backoff(attempt)))    raise RuntimeError(f"gave up after {MAX_ATTEMPTS} attempts: {method} {path}")​​def submit(model, job_input):    """POST one job -> (job, 0) or (None, seconds to wait) when the queue is full."""    headers = {"Idempotency-Key": str(uuid.uuid4())}  # one UUID per job, reused only for its retries    r = request("POST", "/jobs", json={"model": model, "input": job_input}, headers=headers)    if r.status_code == 429:        return None, float(r.headers.get("Retry-After", 60))    return r.json(), 0.0​​def poll(job_id, etag):    """GET one job with If-None-Match -> (job or None on 304, etag)."""    r = request("GET", f"/jobs/{job_id}", headers={"If-None-Match": etag} if etag else {})    if r.status_code == 304:        return None, etag    return r.json(), r.headers.get("ETag", etag)​​def run(jobs, kind="image"):    """Sliding window: <= MAX_IN_FLIGHT in flight, one poll loop, never resubmits.    `jobs` is a list of (model, input) tuples; returns the finished job dicts."""    todo, inflight, done = list(jobs), {}, []    interval, next_submit = POLL_SEC[kind], 0.0    while todo or inflight:        now = time.time()        while todo and len(inflight) < MAX_IN_FLIGHT and now >= next_submit:            job, wait = submit(*todo[0])            if job is None:                       # queue_full: stop filling, wait Retry-After                next_submit = now + wait                break            todo.pop(0)            inflight[job["id"]] = {"etag": None, "next_poll": now + interval}        for job_id, st in list(inflight.items()):            if st["next_poll"] > now:                continue            job, st["etag"] = poll(job_id, st["etag"])            st["next_poll"] = now + interval            if job and job["status"] in ("succeeded", "failed", "canceled"):                done.append(job)                del inflight[job_id]        wakeups = [st["next_poll"] for st in inflight.values()] + ([next_submit] if todo else [])        if wakeups:            time.sleep(max(0.2, min(wakeups) - time.time()))    return done​​def main():    ap = argparse.ArgumentParser(description="RoyalTechno AI reference client")    ap.add_argument("--model", default="nano-banana-pro")    ap.add_argument("--prompt", required=True)    ap.add_argument("--count", type=int, default=1, help="how many separate jobs to submit")    ap.add_argument("--kind", choices=sorted(POLL_SEC), default="image", help="picks the poll cadence")    a = ap.parse_args()    for job in run([(a.model, {"prompt": a.prompt})] * a.count, kind=a.kind):        detail = (job.get("output") or {}).get("url") or (job.get("error") or {}).get("message")        print(job["id"], job["status"], detail)​​if __name__ == "__main__":    main()
// Building with an AI agent? Hand it https://api.royaltechno.cc/llms.txt (plain-text rules) and https://api.royaltechno.cc/openapi.json (info.x-client-rules) — every number above is in both, and they are read straight from the server's own limits.
MODELS

Available models

Pass the id as the model field. Fetch the full live list and pricing from GET /v1/pricing.

Model IDNameTypeAspect ratiosPrice per generation
veo-3.1Veo 3.1video16:9 · 9:16$0.02
omni-flashOmni 1.1 Flashvideo16:9 · 9:16$0.04+
seedance-2.0-miniSeedance 2.0 Minivideo16:9 · 9:16$0.40+
seedance-2.0-fastSeedance 2.0 Fastvideo16:9 · 9:16$0.45+
seedance-2.0Seedance 2.0video16:9 · 9:16$0.55+
seedance-2.5Seedance 2.5 480pvideo16:9 · 9:16$0.50+
grok-imagine-1.5Grok Imaginevideo16:9 · 9:16$0.07+
grok-imagine-1.5-720pGrok Imagine 720pvideo16:9 · 9:16$0.14+
nano-banana-proNano Banana Proimage16:9 · 9:16$0.02
nano-banana-2Nano Banana 2.1image16:9 · 9:16$0.02
seedream-4.6Seedream 4.6image16:9 · 9:16$0.03
eleven-v4Eleven v4audio—$0.03 / 1,000 chars
eleven-v3Eleven v3audio—$0.03 / 1,000 chars
eleven-multilingual-v2Eleven Multilingual v2audio—$0.03 / 1,000 chars
eleven-flash-v2.5Eleven Flash v2.5audio—$0.03 / 1,000 chars
// Prices are live per-generation examples (variable-duration video = “from”); the full matrix is GET /v1/pricing. Live latency medians are on the status page.
VOICEOVER

Text-to-speech with ElevenLabs voices

The same POST /v1/jobs with an audio model: the prompt is the text to speak, the result is one MP3. Poll it like an image job.

Models

eleven-v4 (the newest ElevenLabs model), eleven-v3, eleven-multilingual-v2 and eleven-flash-v2.5 — one price for all, per 1,000 characters. The live list and prices: GET /v1/pricing.

Voice

input.voice is a 20-character ElevenLabs voice id. Any voice from the Voice Library works (elevenlabs.io/app/voice-library); find one with GET /v1/voices (?language=uk for featured voices, ?q=name to search, ?q=<id> for an exact match). Omit it for our default Ukrainian voice. Voices with free: false are refused (voice_not_available).

Language & long texts

input.language is the language of the text as an ISO 639 code (uk, en, id, fil, …). Each model reads its own list of languages (full lists in the API reference, Text-to-speech); any other code is refused with unsupported_language before any charge. There is no automatic detection: with no code, numbers and dates are read in English, so send the language for any non-English text. The default is the voice's first language the model reads. Long texts are split at sentence ends, voiced in parallel and joined into one MP3 — usually in under a minute. The per-request character limit is in the full API reference (Text-to-speech).

Price

Per 1,000 characters, rounded up to the cent, at least 1 cent. Voice-plan characters (ElevenLabs 1M / 2M / 5M packs, 30 days — unused characters expire when the plan lapses) are used first, then free characters, then your balance; both balances are allowances in GET /v1/account. A failed job is refunded in full.

Not accepted

aspect_ratio is ignored; duration_sec, resolution (other than auto), image or video inputs and count > 1 → 400 before any charge. Your key's style text is never added to a voiceover.

Voiceover — same endpoint, audio model

curl https://api.royaltechno.cc/v1/jobs \  -H "Authorization: Bearer $RT_API_KEY" \  -H "Content-Type: application/json" \  -d '{    "model": "eleven-v3",    "input": {      "prompt": "Привіт! Це озвучка, згенерована через RoyalTechno API.",      "voice": "Ntd0iVwICtUtA6Fvx27M",      "language": "uk"    }  }'​# Poll GET /v1/jobs/{id} like an image job. When it succeeds:#   "output": { "type": "audio", "url": "https://cdn.royaltechno.cc/…/job_….mp3",#               "mime_type": "audio/mpeg", "duration_sec": 4, "chars": 54 }
CORE CONCEPTS

How it works

Async + polling

Submit returns immediately with a job id. Poll GET /v1/jobs/{id} (ETag-aware → 304 when unchanged). Webhooks are coming soon.

Credit wallet

Each job is charged upfront from your balance in USD cents. The response includes cost_usd_cents so you can reconcile per request.

Auto-refund on failure

If a generation fails, the charge is refunded to your balance automatically — you only pay for clips you receive.

Per-account keys

Keys are issued per account. Mint one from the bot with /api_key_new; rotate or revoke any time.

Reference images

Pass reference_image_urls to condition on multiple inputs — up to 10 on Nano Banana Pro / 2.1, 9 on Seedance 2.0, 7 on Omni 1.1 Flash, 6 on Seedream 4.6, 3 on Veo 3.1. Full rules in the API reference.

Video inputs

Seedance 2.5 takes a video: reference_video_url — a new clip follows the reference's motion and style (@video1 in the prompt, combinable with reference_image_urls); edit_video_url — re-cuts the clip itself (motion and framing stay, what the prompt describes changes). The input clip's seconds are billed on top of the output (an edit twice), and billed_sec in the response says the number. Omni 1.1 Flash edits with a trim window up to 10 s. Full rules in the API reference.

Resolution & variants

input.resolution selects the tier — 1080p on veo-3.1 / seedance-2.0, 2k/4k on Nano Banana Pro / 2.1. count makes up to 4 variants in one request. Unsupported values are rejected up front; a delivered downgrade is always flagged via notice, never silent.

Retries & rate limits

Send an Idempotency-Key header to make submits safely retriable (24 h replay). Successful throughput is never throttled, but 30 rejections in a minute pause that endpoint for 120 s with 429 too_many_failed_requests (per endpoint — status polling keeps working). A 429 queue_full also pauses submits for ~15 s per key, and an absolute ceiling of 25 req/s per key (bursts to 100) answers 429 too_many_requests before auth; honour Retry-After. The full contract: Rules for scripts and AI agents above.

ENDPOINTS

Reference at a glance

POST/v1/jobsSubmit a video, image or voiceover job
GET/v1/jobs/{id}Fetch job status & output URL
POST/v1/jobs/{id}/cancelCancel a job
POST/v1/chainsClip chain → one continuous movie (1080p finish)
GET/v1/chains/{id}Chain progress: clips + final movie
POST/v1/chains/{id}/cancelStop a chain (unstarted clips never charged)
GET/v1/voicesSearch ElevenLabs voices for text-to-speech
GET/v1/voices/{id}Look up one voice by id
GET/v1/accountBalance, plans & daily quotas + API key info
GET/v1/pricingLive model & price list
GET/v1/statusLive model availability & latency — no auth

Ready to build?

Full endpoint reference, schemas and error codes live in the API docs.

RoyalTechno AI — API