API Documentation

Complete reference for the Viffly.app REST API

Authentication

The API supports two authentication methods. Both require aBusinessplan or higher for external calls.

Method 1: API key (recommended)
curl -X POST https://viffly.app/api/generate-sync \
  -H "X-API-Key: vf_votre_cle_api" \
  -F "audio=@narration.mp3" \
  -F "title=Ma vidéo"
Method 2: Bearer Token
# Obtenir un token via /api/login
# session : "short" (12 h) | "default" (7 j) | "long" (30 j)
curl -X POST https://viffly.app/api/login \
  -H "Content-Type: application/json" \
  -d '{"email":"vous@email.com","password":"...","session":"short"}'

# Réponse : { "token": "eyJhbG...", "expires_at": "...", "session_ttl": "12h" }

# Utiliser le token
curl -H "Authorization: Bearer eyJhbG..." \
  https://viffly.app/api/me
Bearer Tokens expire after12 h, 7 d or 30 d depending on the `session` parameter. API keys (vf_...) are scoped, revocable, rotatable and rate-limited individually — this is the method to use in production.
Key permissions (scopes)

Every key carries a list of scopes. A call to an endpoint it does not cover is rejected with a 403 naming the missing scope — grant the minimum needed.

ScopeEndpoints covered
generate/api/ltx/* · /api/generate · /api/generate-sync · talking-head · restyle · montage · longshort · loop-assemble · director · slideshow · reframe · autocut · audio-clean · watermark · voiceover · media-jobs
ai_images/api/image-jobs · /api/generate-images · /api/images/edit · /api/characters/*
ai_script/api/ai/*
videos/api/videos
credits/api/me · /api/credits/*

A key with no stored scopes (created before scopes existed) passes everywhere. Rotate it to apply a permission set.

Key lifecycle
  • Up to 5 keys per account. The full secret is shown only at creation — it can never be displayed again.
  • Per-key rate limit (60 req/min by default, 1000 max). Exceeding it returns 429 with a Retry-After header.
  • Expiry of your choice: 7, 30, 90, 365 days or never. An expired key is rejected without being deleted.
  • Rotation: a new secret replaces the old one instantly, keeping name, scopes and limits. Use it the moment a key may have leaked.
# Renouveler le secret d'une clé (nom, scopes et limites conservés)
curl -X POST https://viffly.app/api/keys/{id}/rotate \
  -H "Authorization: Bearer eyJhbG..."

# Déconnecter tous les autres appareils
curl -X POST https://viffly.app/api/account/revoke-sessions \
  -H "Authorization: Bearer eyJhbG..."

Credit system

Each AI operation consumes credits (fixed cost for standard features, per-duration for AI video). Free operations (Pexels, Whisper) consume nothing.

🎬
Video render
5 cr.
📝
AI script
1 cr.
🎙️
AI voice
4 cr.
🖼️
AI image (×1)
2 cr.
🎵
AI music
5 cr.
💡
Image prompts
Free
📷
Pexels images
Free
💬
Transcription
Free
Examples:Budget video (your audio + Pexels) =5 cr.| Standard video (script + AI voice) =10 cr.| Full-AI video (fully automatic) =21 cr.
GET/api/credits/costs

Returns the credit cost of each operation, the available plans and usage scenarios.

Response
{
  "costs": { "render": 5, "ai_script": 1, "ai_voice": 4, "ai_image": 2, "ai_music": 5, "pexels": 0 },
  "packages": [
    { "slug": "free", "label": "Free", "monthlyCredits": 15 },
    { "slug": "pro", "label": "Pro", "monthlyCredits": 200 }, ...
  ],
  "scenarios": [ { "name": "Budget", "credits": 5 }, ... ]
}
GET/api/meAuth

Returns the account information, including the credit balance.

Response
{
  "id": "abc123",
  "client_email": "vous@email.com",
  "package": "pro",
  "creditsTotal": 142,
  "creditsMonthly": 130,
  "creditsPack": 12,
  "monthlyAllowance": 150,
  "packageLabel": "Pro",
  "creditCosts": { "render": 5, "ai_voice": 4, ... }
}

AI video generation (MiniMax / Alibaba)

Generates a video fromtext(t2v) or from animage(i2v) via the AI models. This is the API to use from n8n. The flow isasynchronous: you submit a job, then poll its status untildone.

🌊 Hailuo 2.3 (MiniMax)
MiniMax Hailuo 2.3 · model: "hailuo23"
🎬 Wan 2.2
Alibaba Wan 2.7 · model: "wan27"
Requires aProplan or higher. Authentication viaX-API-Key.
GET/api/ltx/pricing

Lists the tiers (resolution × duration), aspect ratios and scenarios. No auth required — useful to populate a UI or pick a tier_id.

Response
{
  "tiers": [
    { "id": "tier1", "label": "720p · 5s",  "resolution": "720p",  "seconds": 5,  "credits": 10 },
    { "id": "tier2", "label": "720p · 5s",  "resolution": "720p",  "seconds": 5,  "credits": 10 },
    { "id": "tier3", "label": "720p · 10s", "resolution": "720p",  "seconds": 10, "credits": 20 },
    { "id": "tier6", "label": "1080p · 5s", "resolution": "1080p", "seconds": 5,  "credits": 10 },
    { "id": "tier7", "label": "1080p · 10s","resolution": "1080p", "seconds": 10, "credits": 20 }
  ],
  "models": [
    { "id": "wan27", "provider": "dashscope_wan", "tiers": [...] },
    { "id": "hailuo23", "provider": "minimax_hailuo", "tiers": [...] }
  ],
  "aspects": [ { "id": "9:16" }, { "id": "1:1" }, { "id": "16:9" } ],
  "scenarios": [ { "id": "cinematic" }, { "id": "product" }, ... ],
  "default_negative": "ugly, blurry, low quality, ..."
}
POST/api/ltx/generate10 cr.Auth

Submits a video generation job. Returns a job_id immediately (non-blocking). JSON body.

Request body
FieldTypeDescription
mode*string"t2v" (text→video) or "i2v" (image→video)
prompt*stringVideo description (≥ 3 characters)
tier_idstringtier1, tier2, tier3, tier6, or tier7. Check /pricing for engine-specific combinations.
modelstring"hailuo23" (MiniMax), "happyhorse" or "wan27" (Alibaba)
image_base64stringStarting image in base64 (required if mode=i2v). Without the data: prefix
image_urlstringAlternative: public HTTPS URL of the starting image (i2v)
negativestringNegative prompt (default: see /api/ltx/pricing)
aspectstring"9:16" (default), "1:1", "16:9"
scenario_typestringCreative direction (see /api/ltx/pricing)
fpsnumberFrames/second (default: 24)
seednumberSeed (reproducibility). Random if omitted
audio_onbooleanKeep the generated audio track (default: true)
Response
{
  "job_id": "915c93ea-58b7-4808-a83f-5dfa98ee1ef5",
  "status": "processing",
  "tier": { "id": "tier2", "label": "720p · 5s", "credits": 10 }
}
Example — text → video:
curl -X POST https://viffly.app/api/ltx/generate \
  -H "X-API-Key: vf_votre_cle" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "t2v",
    "model": "hailuo23",
    "prompt": "a sports car drifting on a coastal road at sunset, cinematic",
    "tier_id": "tier2",
    "aspect": "9:16"
  }'
Example — image → video:
curl -X POST https://viffly.app/api/ltx/generate \
  -H "X-API-Key: vf_votre_cle" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "i2v",
    "model": "wan27",
    "prompt": "slow cinematic push in, gentle motion",
    "tier_id": "tier2",
    "image_base64": "'"$(base64 -w0 photo.jpg)"'"
  }'
POST/api/ltx/generate-sync10 cr.Auth

SYNCHRONOUS version: submits, waits for the render internally, then returns the video — in ONE single call (no wait/poll loop). Ideal for a single n8n node. Set a long timeout (e.g. 1200000 ms).

Request body
FieldTypeDescription
(same as /api/ltx/generate)*jsonmode, prompt, tier_id, model, image_base64/image_url, aspect…
Response
// Par défaut → renvoie le FICHIER MP4 (binaire). Dans n8n : Response Format = File.
// Avec ?format=json → renvoie l'URL à la place :
{
  "status": "done",
  "job_id": "915c93ea-...",
  "output_url": "/videos/ltx_915c93ea-....mp4",
  "url": "https://viffly.app/videos/ltx_915c93ea-....mp4"
}
# Récupère directement le MP4 (sortie binaire)
curl -X POST https://viffly.app/api/ltx/generate-sync \
  -H "X-API-Key: vf_votre_cle" -H "Content-Type: application/json" \
  -d '{"mode":"t2v","model":"hailuo23","prompt":"...","tier_id":"tier2"}' \
  --max-time 1200 -o video.mp4

# Ou récupère juste l'URL (JSON)
curl -X POST "https://viffly.app/api/ltx/generate-sync?format=json" \
  -H "X-API-Key: vf_votre_cle" -H "Content-Type: application/json" \
  -d '{"mode":"t2v","prompt":"...","tier_id":"tier2"}'
GET/api/ltx/status/:idAuth

(Async mode) Polls the state of a job. Call it in a loop (every ~5-10 s) until status = done or error. Not needed if you use /generate-sync.

Response
// en cours
{ "id": "915c93ea-...", "status": "processing", "progress": 50 }

// terminé
{
  "id": "915c93ea-...",
  "status": "done",
  "output_url": "/videos/ltx_915c93ea-....mp4",  // relatif → préfixer https://viffly.app
  "progress": 100
}

// échec (crédits remboursés)
{ "id": "915c93ea-...", "status": "error", "error": "..." }
output_urlisrelative. Full download URL =https://viffly.app+output_url.

Smart thumbnails

Turn landscape, vertical, or square video into multi-format thumbnails. Choose real frames, AI-retouched frames, or new visuals generated from the topic and transcript.

16:9 · 9:16 · 1:1 · 4:5Smart crop for each destination
1–6 × formatGenuinely different moments
Async · n8n readyDurable jobs with progress tracking
POST/api/thumbnails5 cr.Auth

Start a job from a video URL, authorized S3 source, or a file already hosted by Viffly.

Request body
FieldTypeDescription
source_url*stringURL HTTPS publique, s3:// autorisé ou /videos/...
countnumber1 à 6 variantes par format (défaut : 3)
formatsstring[]auto, 16:9, 9:16, 1:1, 4:5
visual_modestringframes (défaut), enhance ou generate
ai_qualitystringstandard (2 cr./image) ou premium (5 cr./image)
image_promptstringDirection facultative pour la retouche ou la génération
languagestringLangue du titre automatique : auto, fr, en, ar, es, de ou pt
titlestringTitre optionnel ; généré depuis le contenu si omis
subtitlestringSous-titre optionnel
stylestringeditorial, bold, clean, minimal
tonestringauto, warm, cool, mono
smartbooleanAnalyse visuelle + transcription + point focal
include_textbooleanAjouter ou non la composition typographique
accentstringCouleur hexadécimale, ex. #6D4AFF
Response
{
  "job_id": "53a9c34d-...",
  "status": "processing",
  "cost": 11,
  "visual_mode": "generate",
  "cost_breakdown": { "composition": 5, "ai_images": 6 },
  "outputs_planned": 3,
  "status_url": "/api/thumbnails/53a9c34d-..."
}
curl -X POST https://viffly.app/api/thumbnails \
  -H "X-API-Key: vf_votre_cle" \
  -H "Content-Type: application/json" \
  -d '{
    "source_url": "https://cdn.exemple.com/video.mp4",
    "count": 3,
    "formats": ["16:9", "9:16"],
    "visual_mode": "generate",
    "ai_quality": "standard",
    "language": "fr",
    "image_prompt": "documentary editorial visual, authentic texture, no text",
    "style": "editorial",
    "smart": true
  }'
POST/api/thumbnails/upload5 cr.Auth

Convenient multipart upload for n8n and files up to 200 MB.

Request body
FieldTypeDescription
video*fileMP4, MOV, M4V, MKV, WebM, AVI ou MPEG · 200 Mo max
formatsstringTableau JSON ou liste séparée par des virgules
countnumber1 à 6 variantes par format
visual_modestringframes, enhance ou generate
ai_qualitystringstandard ou premium
image_promptstringDirection facultative pour l’IA
languagestringLangue du titre automatique : auto, fr, en, ar, es, de ou pt
stylestringeditorial, bold, clean, minimal
Response
{ "job_id": "53a9c34d-...", "status": "processing", "status_url": "/api/thumbnails/53a9c34d-..." }
curl -X POST https://viffly.app/api/thumbnails/upload \
  -H "X-API-Key: vf_votre_cle" \
  -F "video=@episode.mp4" \
  -F 'formats=["16:9","9:16"]' \
  -F "count=3" \
  -F "visual_mode=enhance" \
  -F "ai_quality=standard" \
  -F "language=fr" \
  -F "image_prompt=cinematic light, cleaner background" \
  -F "style=editorial"
GET/api/thumbnails/:idAuth

Track the job and retrieve analysis, source frames, and every final thumbnail.

Response
{
  "job_id": "53a9c34d-...",
  "status": "done",
  "progress": 100,
  "analysis": { "summary": "..." },
  "visual_mode": "enhance",
  "outputs": [
    { "url": "/videos/thumb_....jpg", "format": "16:9", "source_kind": "enhanced", "master_url": "/videos/mmedit_....png", "raw_frame_url": "/videos/thumb_..._frame_1.jpg", "width": 1280, "height": 720, "timestamp": 42.1, "score": 91 }
  ]
}
n8n

Recommended chain: HTTP Request POST → Wait 3 s → HTTP Request GET → IF status = done. Loop back to Wait while status is processing.

GET https://viffly.app/api/thumbnails/{{ $('Viffly · Miniatures').item.json.job_id }}

Webhooks — get notified when a render finishes

Add callback_url to any job creation call: instead of polling for status, Viffly POSTs to you the moment the render completes. Your n8n or Make execution never sits blocked for minutes.

POST /api/ltx/generate
{
  "mode": "i2v",
  "prompt": "…",
  "callback_url": "https://votre-domaine.com/viffly-hook"
}

The address must be HTTPS and publicly reachable. Private addresses, localhost and plain HTTP are rejected at creation time, before any credits are charged.

What you receive
{
  "event": "job.completed",      // ou "job.failed"
  "job_id": "3f2a…",
  "kind": "video",
  "status": "done",              // ou "error"
  "output_url": "/videos/ltx_3f2a….mp4",
  "error": null,
  "delivered_at": "2026-07-27T19:44:12.031Z"
}
Headers
X-Viffly-Eventevent name, same as the event field in the body
X-Viffly-Deliveryunique delivery id — use it to discard duplicates
X-Viffly-Timestampunix timestamp in seconds, part of the signature computation
X-Viffly-Signaturesha256=… — HMAC of the body, lets you verify the call really comes from us
Verifying the signature

Compute the HMAC-SHA256 of "timestamp.body" with your secret and compare it to the header. Use the RAW request body: re-serialising the JSON changes whitespace and breaks the comparison.

const crypto = require('crypto');

function verifie(req, secret) {
  const ts   = req.headers['x-viffly-timestamp'];
  const recu = req.headers['x-viffly-signature'];
  const attendu = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(ts + '.' + req.rawBody)   // corps BRUT, non re-sérialisé
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(recu), Buffer.from(attendu));
}

On failure, up to 4 spaced attempts (immediate, +2s, +15s, +60s). A 4xx response is treated as a definitive refusal and is not retried. Return 2xx on receipt.

n8n integration — HTTP Request node

The video flow is asynchronous. In n8n, the pattern is:SubmitWaitCheck the status→ loop while ≠done.

① HTTP Request (POST /generate)② Wait 10s③ HTTP Request (GET /status)④ IF status = done?→ no: back to ②→ yes: download
n8n node builder

Configure the options below then generate the HTTP Request node JSON — copy it and paste it directly into the n8n canvas (Ctrl+V), or download the file.

Or configure the nodes manually:
NODE 1Submit the job — HTTP Request
MethodPOST
URLhttps://viffly.app/api/ltx/generate
AuthenticationNone (we pass the key in the header below)
Send HeadersON → Name: X-API-Key | Value: vf_votre_cle
Send BodyON → Body Content Type: JSON
Specify BodyUsing JSON
Body (JSON)
{
  "mode": "i2v",
  "model": "hailuo23",
  "prompt": "{{ $json.prompt }}",
  "tier_id": "tier2",
  "aspect": "9:16",
  "image_base64": "{{ $binary.data.data }}"
}

i2v: if the image comes from a previous node (e.g. Telegram, Read Binary File), reference its base64 binary data with{{ $binary.data.data }}. For t2v, removeimage_base64and set"mode": "t2v".

NODE 2 + 3Wait then check the status

Node 2 — Wait: 10 seconds.Node 3 — HTTP Request(poll):

MethodGET
URLhttps://viffly.app/api/ltx/status/{{ $('NODE 1').item.json.job_id }}
Send HeadersON → X-API-Key: vf_votre_cle
NODE 4Loop — IF status = done?

NodeIF— condition (String):{{ $json.status }} equals done.

  • false(not ready yet) → connect the output toNode 2 (Wait): the loop continues.
  • true(done) → continue. The final URL =https://viffly.app{{ $json.output_url }}
Download the video (HTTP Request)
MethodGET
URLhttps://viffly.app{{ $json.output_url }}
Response FormatFile / Binary
n8n tips
  • To avoid an infinite loop: add a counter (Set node + IF on a max number of iterations, e.g. 60 ≈ 10 min).
  • Wan 2.2 takes 3-5 min: set the Wait to 15-20 s to reduce the number of calls.
  • The cost is charged once at launch andautomatically refundedif the render fails.
  • No incoming webhook: it's polling. An API keyvf_…("API" tab) is enough.

Reel generation (audio + images)

POST/api/generate-sync5 cr.Auth

Generates a video and waits for the result (20 min timeout). Returns the download URL.

Request body
FieldTypeDescription
audio*fileMP3 audio file of the narration
imagesfile[]Images (1 to 4 files). Alternative: image_url
image_urlstringURL of an image (alternative to images)
image_url_0..NstringIndexed image URLs, no longer capped at 3 (up to 30 media). An .mp4/.mov URL here plays as a video clip at that position.
mediastring[]Ordered JSON array mixing images AND videos — array order = display order. Videos detected by extension (.mp4, .mov, .webm…).
formatstringOutput format: 9:16 (default), 16:9, 1:1, 4:5, 4:3
titlestringTitle shown in the intro
sourcestringSource / site name
script_textstringScript text for the subtitles
templatestring32 templates : default, bold, clean, news, minimal, highlight, cinematic, neon, retro, gaming, story, meme, comic, glitch, noir, vaporwave, duotone, matrix, ticker…
caption_stylestringkaraoke, glow, outline, neon, block, shadow, gradient, typewriter
caption_sizestringS, M, L, XL
caption_positionstringtop, center, bottom
languagestringfr, en, ar, es, de, it, pt
profilestringRender profile name (e.g. shadesplays)
music_urlstringBackground music URL (loops automatically when the video outlasts the track)
music_start_atnumberMusic start offset (seconds)
voice_typestring"ai" for MiniMax narration (+4 cr.)
voice_idstringMiniMax voice ID
outro_textstringEnd screen text
outro_substringEnd screen subtitle
Response
{
  "videoId": "7865aac1-c301-4060-bd6e-6ca4fe3f6859",
  "url": "https://s3.eu-west-3.amazonaws.com/.../render.mp4",
  "thumbnail": "https://s3.eu-west-3.amazonaws.com/.../thumb.jpg",
  "duration": 27.5,
  "size": 8542190
}
Full example:
curl -X POST https://viffly.app/api/generate-sync \
  -H "X-API-Key: vf_votre_cle" \
  -F "audio=@narration.mp3" \
  -F "format=16:9" \
  -F 'media=["https://cdn.exemple.com/clip.mp4","https://cdn.exemple.com/photo1.jpg","https://cdn.exemple.com/photo2.jpg"]' \
  -F "title=Mon titre" \
  -F "source=MonSite.com" \
  -F "template=gaming" \
  -F "caption_style=karaoke" \
  -F "caption_size=L" \
  -F "script_text=Voici le texte des sous-titres" \
  -F "profile=shadesplays"
POST/api/generate5 cr.Auth

Starts the generation in the background. Returns a jobId to track progress via SSE.

Response
{ "jobId": "e9e2d4f9-..." }
Track progress:GET /api/videos/events?jobId=...(Server-Sent Events)

Videos

GET/api/videosAuth

Lists all the account's videos, sorted by descending date.

Response
[
  {
    "id": "7865aac1-...",
    "title": "Mon titre",
    "status": "done",
    "url": "https://...",
    "thumbnail": "https://...",
    "size": 8542190,
    "duration": 27.5,
    "created": "2026-03-28T..."
  }, ...
]
GET/api/videos/:id.mp4Auth

Download a video's MP4 file.

DELETE/api/videos/:idAuth

Delete a video and its associated files.

AI services

POST/api/ai/script1 cr.Auth

Generates a narration script optimized for virality.

Request body
FieldTypeDescription
prompt*stringTopic or instruction
existingstringExisting script to improve
contextstringAdditional context (web content)
languagestringTarget language (fr, en, ar...)
Response
{ "script": "Saviez-vous que...", "charsUsed": 342, "charsMax": 700 }
POST/api/generate-images2 cr.Auth

Generates images via FAL Flux 2 Pro.

Request body
FieldTypeDescription
prompt*stringImage description
countnumberNumber of images (1-3, default: 3)
Response
{ "images": ["https://fal.media/...", ...] }
Cost: 2 credits × number of images
POST/api/ai/image-promptAuth

Generates 3 image prompts optimized for a given topic.

Request body
FieldTypeDescription
topic*stringVideo topic
stylestringDesired visual style
Response
{ "prompts": ["A cinematic...", "An aerial...", "A close-up..."] }
POST/api/generate-music5 cr.Auth

Generates background music via MiniMax Music.

Request body
FieldTypeDescription
promptstringDescription of the musical style
stylestringMusic genre (pop, electronic, cinematic...)
instrumentalbooleanInstrumental only (default: true)
titlestringTrack title
Response
{ "taskId": "abc123", "status": "pending" }
Check the status:GET /api/generate-music/:taskId
GET/api/voicesAuth

Lists the available MiniMax voices (1h cache).

Response
{ "voices": [{ "voice_id": "pNInz6ob...", "name": "Adam", "labels": {...} }, ...] }

API key management

GET/api/keysAuth

Lists all your API keys.

Response
[{
  "id": "uuid", "name": "Production", "key_prefix": "vf_a1b2c3d4",
  "scopes": ["generate","videos"], "rate_limit": 60,
  "expires_at": null, "last_used_at": "2026-03-28T...",
  "usage_count": 42, "is_active": true
}]
POST/api/keysAuth

Creates a new API key. The full key is only returned at creation.

Request body
FieldTypeDescription
namestringKey name (e.g. Production)
scopesstring[]Permissions: generate, videos, credits, ai_script, ai_images, ai_music
rate_limitnumberRequests per minute (1-1000, default: 60)
expires_in_daysnumberDays before expiration (null = never)
Response
{
  "id": "uuid",
  "key": "vf_a1b2c3d4e5f6...",  // ⚠️ Affiché UNE SEULE FOIS
  "name": "Production",
  "scopes": ["generate", "videos", "credits"]
}
PATCH/api/keys/:idAuth

Edit a key (name, permissions, rate limit, enable/disable).

Request body
FieldTypeDescription
namestringNew name
is_activebooleanEnable/disable
scopesstring[]New permissions
rate_limitnumberNew rate limit
DELETE/api/keys/:idAuth

Permanently deletes an API key.

Media library

GET/api/musicAuth

Lists the music library (uploaded tracks + MiniMax).

GET/api/pexels/imagesAuth

Pexels image search.

Request body
FieldTypeDescription
qquerySearch term
pagequeryPage (default: 1)
per_pagequeryResults per page (default: 20)
GET/api/pexels/videosAuth

Pexels video search.

Error codes

CodeMeaningAction
401Invalid or expired tokenRenew the token or create a new API key
402Insufficient creditsBuy a pack or upgrade to a higher plan
403Insufficient plan (Business+ required)Upgrade to Business or Agency
429Rate limit exceededWait 1 minute or increase the rate limit
500Server errorRetry or contact support
Standard error format
{
  "error": "Message descriptif de l'erreur",
  "creditsNeeded": 5  // (optionnel) crédits nécessaires
}
Rate limiting

The default limits are:

  • API keys: configurable per key (1-1000 req/min, default 60)
  • Bearer Token: 100 req/min global
  • AI endpoints (script, images, music): 10 req/min
  • Video generation: 3 concurrent max per account