The third can be your coding agent’s.
Keys need a paid plan or one top-up, from $10. Welcome credit doesn’t count.
It’s shown once. Keep it on your server as LOOPLING_API_KEY.
Submit, poll every 5 seconds, download. A render takes a minute or a few.
Submit a prompt, read it until it’s done, save the MP4.
curl
# 1. Submit: 202 with the generation id.
curl https://loopling.ai/v1/video/generations \
-H "Authorization: Bearer $LOOPLING_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance-2.5",
"prompt": "A paper lantern drifting over a night harbour, slow dolly in",
"aspect_ratio": "16:9",
"resolution": "1080p",
"duration": 5
}'
# 2. Poll every few seconds until "status" is "succeeded" or "failed".
curl https://loopling.ai/v1/video/generations/vgen_01J9X6K3M8Q2 \
-H "Authorization: Bearer $LOOPLING_API_KEY"
# 3. Download.
# 302 to a fresh signed URL; -L follows it and -o saves the MP4.
curl -L https://loopling.ai/v1/video/generations/vgen_01J9X6K3M8Q2/content \
-H "Authorization: Bearer $LOOPLING_API_KEY" \
-o lantern.mp4Node
import fs from 'node:fs'
// 1. Submit
const res = await fetch('https://loopling.ai/v1/video/generations', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LOOPLING_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'seedance-2.5',
prompt: 'A paper lantern drifting over a night harbour, slow dolly in',
aspect_ratio: '16:9',
resolution: '1080p',
duration: 5,
}),
})
const generation = await res.json() // { id: 'vgen_…', status: 'queued', ink_reserved, … }
// 2. Poll
const headers = { Authorization: `Bearer ${process.env.LOOPLING_API_KEY}` }
async function waitForVideo(id) {
for (;;) {
const res = await fetch(`https://loopling.ai/v1/video/generations/${id}`, { headers })
const gen = await res.json()
if (gen.status === 'succeeded') return gen.video // { url, expires_at, duration_seconds, … }
if (gen.status === 'failed') throw new Error(gen.error?.message ?? 'generation failed')
await new Promise((r) => setTimeout(r, 5000))
}
}
const video = await waitForVideo(generation.id)
// video.url is signed and valid for 15 minutes; read the generation again for a fresh one.
// 3. Download the MP4 while the link is fresh.
const file = await fetch(video.url)
await fs.promises.writeFile('lantern.mp4', Buffer.from(await file.arrayBuffer()))Python
# 1. Submit
import os, requests
res = requests.post(
"https://loopling.ai/v1/video/generations",
headers={"Authorization": f"Bearer {os.environ['LOOPLING_API_KEY']}"},
json={
"model": "seedance-2.5",
"prompt": "A paper lantern drifting over a night harbour, slow dolly in",
"aspect_ratio": "16:9",
"resolution": "1080p",
"duration": 5,
},
)
generation = res.json() # {"id": "vgen_…", "status": "queued", "ink_reserved": …}
# 2. Poll
import time
headers = {"Authorization": f"Bearer {os.environ['LOOPLING_API_KEY']}"}
def wait_for_video(generation_id: str) -> dict:
while True:
gen = requests.get(f"https://loopling.ai/v1/video/generations/{generation_id}", headers=headers).json()
if gen["status"] == "succeeded":
return gen["video"] # {"url": …, "expires_at": …, "duration_seconds": …}
if gen["status"] == "failed":
raise RuntimeError(gen.get("error", {}).get("message", "generation failed"))
time.sleep(5)
video = wait_for_video(generation["id"])
# video["url"] is signed and valid for 15 minutes; read the generation again for a fresh one.
# 3. Download the MP4 while the link is fresh.
open("lantern.mp4", "wb").write(requests.get(video["url"]).content)When it succeeds, the generation carries video.url: a signed link for 15 minutes. Clips are kept 7 days.
One prompt: the base URL, auth, the models, the three calls and every error.
Agent prompt
# Task: generate Seedance video through the Loopling API
Add Seedance video generation to this codebase using the Loopling REST API. If the code already calls fal for Seedance, replace those calls with this contract; otherwise add one small, typed client module. Follow the contract exactly and do not invent endpoints or fields.
## Contract
- Base URL: https://loopling.ai/v1
- Auth: every request sends `Authorization: Bearer $LOOPLING_API_KEY`. Read the key from the LOOPLING_API_KEY environment variable on the server. Never hardcode it, never log it, never ship it to a browser or a mobile app.
- Requests and responses are JSON (`Content-Type: application/json`). Field names are snake_case.
## Models (one endpoint; choose the model per request)
- `seedance-2.5` (Seedance 2.5): resolution "480p" | "720p" | "1080p"; with aspect_ratio "9:16" or "adaptive" the ceiling is 720p; duration 4–30 s (integer); up to 50 references (30 images, 10 videos, 10 audio).
- `seedance-2.0` (Seedance 2.0): resolution "480p" | "720p" | "1080p"; duration 4–15 s (integer); up to 12 references (9 images, 3 videos, 3 audio).
- `seedance-2.0-fast` (Seedance 2.0 Fast): resolution "480p" | "720p"; duration 4–15 s (integer); up to 12 references (9 images, 3 videos, 3 audio).
## The three calls
1. Submit — `POST /v1/video/generations` (https://loopling.ai/v1/video/generations)
Body: {"model": "seedance-2.5", "prompt": "…", "resolution": "720p", "aspect_ratio": "16:9", "duration": 5, "generate_audio": true}
Defaults: resolution "720p", aspect_ratio "16:9", duration 5, generate_audio true. aspect_ratio is one of "21:9", "16:9", "4:3", "1:1", "3:4", "9:16", "adaptive".
Optional: seed (integer); first_frame_url and last_frame_url (last needs first); reference_image_urls, reference_video_urls, reference_audio_urls (arrays). Every input is a public https URL. Frames and reference_* inputs cannot be combined in one request.
Send an `Idempotency-Key` header (1–128 of letters, digits, `.` `_` `:` `-`), unique per intent, so a retried submit never starts a second render.
Answer: 202 with {"id": "vgen_…", "status": "queued", "ink_reserved": …, "request": {…}}.
2. Poll — `GET /v1/video/generations/{id}` every 5 seconds until status is "succeeded" or "failed" (statuses: queued, running, succeeded, failed; `progress` 0–100 may appear while running). Give up after a timeout you choose (renders usually take one to a few minutes).
On "succeeded": `video.url` is a signed MP4 link valid for 15 minutes. Download it straight away, or read the generation again for a fresh link. Results are kept for 7 days.
On "failed": `error.code` and `error.message` explain it, and `ink_charged` is 0.
3. Download — `GET /v1/video/generations/{id}/content` answers 302 to a fresh signed URL; follow the redirect. It answers 409 `generation_not_ready` before the render finishes.
## Errors
Every error is {"error": {"type", "code", "message", "param", "request_id"}}.
- 400 `invalid_request_error`: fix the field named in `param`; never retry it unchanged.
- 401 `authentication_error`: the key is missing, unknown or revoked.
- 402 `insufficient_ink_error`: the balance (`insufficient_ink`) or the key's spending limit (`key_ink_limit_reached`) cannot cover the request. Surface it; do not retry.
- 429 `rate_limit_error`: wait for the `Retry-After` seconds, then retry. Limits: 5 video generations running at once per account and 60 requests per minute per key.
- 502 / 503 `provider_error`: retry later with backoff, honouring `Retry-After`, and with a new `Idempotency-Key`: a 502, or a 503 saying the generation could not be admitted, has already bound the old key to a failed generation, so resending it only replays that failure. Nothing is charged for a request that never ran.
## Moving from fal
- Replace `Authorization: Key $FAL_KEY` with `Authorization: Bearer $LOOPLING_API_KEY`.
- fal picks the model by endpoint path; here every model shares one endpoint and the body carries `model`.
- prompt, resolution, aspect_ratio, generate_audio and seed keep their names. `duration` must be an integer number of seconds (convert a string such as "5"); there is no "auto".
- The image-to-video start image (`image_url` on fal) becomes `first_frame_url`; an end image becomes `last_frame_url`; reference lists become `reference_image_urls`, `reference_video_urls` and `reference_audio_urls`.
- fal's queue status and result calls become one poll: `GET /v1/video/generations/{id}` returns the status and, once done, `video`.
- Remove the fal client and its key from configuration once nothing else uses them.
## Done when
- One function submits, polls with a timeout, and returns the finished MP4 (bytes or a stored copy), surfacing `error.message` on failure.
- The key is read from LOOPLING_API_KEY only, and tests mock the HTTP calls instead of spending credit.
- Reference: https://loopling.ai/seedance/docs/create-video-generation
- Keys: https://loopling.ai/seedance/keys (a paid plan or one completed top-up is needed to create one).Every model takes the same request. Its ID goes in the model field.
seedance-2.5 · 4–30 s · up to 1080p · from $0.0884/s
seedance-2.0 · 4–15 s · up to 1080p · from $0.0604/s
seedance-2.0-fast · 4–15 s · up to 720p · from $0.0483/s
What changes for the same request.
| What | On fal | On Loopling |
|---|---|---|
| Base URL | https://queue.fal.run | https://loopling.ai/v1 |
| Auth header | Authorization: Key $FAL_KEY | Authorization: Bearer $LOOPLING_API_KEY |
| Choosing the model | The endpoint path | The model field; one endpoint for all |
| Polling | Queue status, then a separate result call | One read: the status, then the video |
| Result link | A file URL in the result | Signed video.url, valid 15 minutes. Kept 7 days. |
prompt, resolution, aspect_ratio, generate_audio and seed keep their names. Add model, and send duration in whole seconds.
loopling.ai · hello@loopling.ai · for agents · community · pricing · API · terms · privacy