Disclosure: I work on Revana, a pay-as-you-go API for image and video models. The workflow below calls its REST API, but the shape (price first, cap the spend, one idempotency key per job, wait without hammering the API) is what you want around any paid generation API in n8n.
Most "AI video in n8n" tutorials stop at one HTTP Request node that fires a prompt at a model. That works once in a demo. In a real workflow it breaks in three ways:
- You find out the price afterwards. Someone changes the resolution from 720p to 1080p in a Set node and the next 200 runs cost 2.5x.
- n8n's "Retry On Fail" can double-charge you. If the create request times out after the provider accepted it, a retry starts a second paid video.
- Video takes minutes. A single request can't wait that long, so you need a wait loop or a callback.
This post builds a workflow out of stock n8n nodes (HTTP Request, IF, Wait, Stop and Error) that handles all three. No community node is required, though there is one at the end if you prefer it.
What you're building
Manual Trigger
-> Settings (Set)
-> Quote (HTTP Request, free)
-> Within budget? (IF) --no--> Stop and Error
-> Create generation (HTTP Request, Idempotency-Key)
-> Wait 20 s
-> Get generation (HTTP Request)
-> Succeeded? (IF) --yes--> Download video (HTTP Request, file)
--no---> Failed? (IF) --yes--> Stop and Error
--no---> back to Wait
Every Revana model goes through the same two endpoints, so the workflow is the same for Seedance 2.5, Kling 4.0, Wan 3.0, Veo 3.1 Fast and the rest. Only the model value and its options change.
1. The credential
Create an API key at revana.si/dashboard. In n8n, go to Credentials > Add credential > Header Auth:
- Name:
Authorization - Value:
Bearer sr_live_...
Call it Revana API. On each HTTP Request node below, set Authentication to Generic Credential Type, Generic Auth Type to Header Auth, and pick that credential.
2. Settings
A Set node holds everything you'd want to change per run:
| Field | Example |
|---|---|
model |
seedance-2.5 |
prompt |
a paper boat drifting down a rainy street at night, neon reflections, rain sounds |
resolution |
720p |
duration |
5 |
aspect_ratio |
9:16 |
audio |
true |
budget_usd |
2 |
Valid values differ by model. GET https://api.revana.si/v1/models (no key needed) lists each model's resolutions, durations and aspect ratios, for example Seedance 2.5 takes 4 to 30 seconds and Kling 4.0 takes 3 to 30. A value a model doesn't offer is refused with 422 invalid_input before anything is charged, so a typo costs nothing.
3. Quote before you spend
POST /v1/generations/quote takes exactly the body you're about to send and answers what it will cost. It's free: nothing is created, held or charged.
Quote node (HTTP Request):
- Method:
POST - URL:
https://api.revana.si/v1/generations/quote - Send Body: on, Body Content Type: JSON, Specify Body: Using JSON
- JSON:
={{ JSON.stringify({
model: $json.model,
input: {
prompt: $json.prompt,
resolution: $json.resolution,
duration: $json.duration,
aspect_ratio: $json.aspect_ratio,
audio: $json.audio
}
}) }}
The answer has two numbers worth knowing:
-
quoted_usd: the price on the first route tried. This is what a successful job normally costs. -
reserved_usd: the most it could cost if the first provider fails and a dearer backup delivers instead. That amount is held from your balance while the job runs.
With the settings above (Seedance 2.5, 720p, 5 s, 9:16, audio), the quote on November 5, 2026 was $1.05 ($0.21 per second), with up to $4.05 reserved if every backup were allowed. The same settings on Kling 4.0 quoted $0.68, on Wan 3.0 $0.35, and Veo 3.1 Fast at 6 seconds (it takes 4, 6 or 8) $0.60. Re-quote rather than copying these: prices change, and some inputs cost more than the model's headline rate. Seedance 2.5 with a first frame quoted $0.25 a second instead of $0.21.
Within budget? (IF node), Number condition:
{{ $json.quoted_usd }} is less than or equal to {{ $('Settings').item.json.budget_usd }}
The false branch goes to a Stop and Error node with a message like Costs ${{ $json.quoted_usd }}, over the budget.
4. Create the job, safely
Create generation (HTTP Request):
- Method:
POST, URL:https://api.revana.si/v1/generations - Send Headers: on, add
Idempotency-Key=n8n-{{ $execution.id }} - Body: the same JSON as the quote, plus a cap:
={{ JSON.stringify({
model: $('Settings').item.json.model,
input: {
prompt: $('Settings').item.json.prompt,
resolution: $('Settings').item.json.resolution,
duration: $('Settings').item.json.duration,
aspect_ratio: $('Settings').item.json.aspect_ratio,
audio: $('Settings').item.json.audio
},
max_cost_usd: $json.quoted_usd
}) }}
Two things make this node safe to retry:
-
The idempotency key is tied to the execution, not the HTTP call. Turn on Retry On Fail in the node's Settings tab. Each retry sends the same key with the same body, so if the first attempt reached the API, the retry gets the original job back (
200) instead of starting a second one. Sending the same key with a different body returns409 idempotency_conflict, which is what you want if someone edits the node between attempts. If you loop over many items in one execution, make the key per item, for examplen8n-{{ $execution.id }}-{{ $itemIndex }}. -
max_cost_usdis a hard ceiling. Setting it toquoted_usdmeans a dearer backup provider is skipped and the job fails instead of costing more. If you'd rather get the video, set it toreserved_usd(or your budget) and let backups run; you're charged only for the route that delivers.
The response is 202 with a generation object: id, status (queued), quoted_usd, reserved_usd, and cost_usd (null until it finishes).
5. Wait without hammering the API
A 5-second 720p clip usually takes one to a few minutes, sometimes longer. Polling every second wastes executions and can hit the per-second burst guard (429 rate_limited). A loop with a Wait node is enough:
- Wait: After Time Interval, 20 seconds.
-
Get generation (HTTP Request):
GET https://api.revana.si/v1/generations/{{ $('Create generation').item.json.id }} -
Succeeded? (IF):
{{ $json.status }}equalssucceeded→ download. -
Failed? (IF):
{{ $json.status }}equalsfailed→ Stop and Error with{{ $json.error.code }}: {{ $json.error.message }}. Otherwise connect the false branch back to Wait.
Two details:
- A failed generation is charged
$0(cost_usd: 0).error.retryabletells you whether sending the same request later can work (model_busy,timeout,all_routes_failed) or whether the request itself has to change (content_policy,invalid_input,max_cost_exceeded). - Put a ceiling on the loop so a stuck job can't run forever. In the Failed? node, add an OR condition
{{ $runIndex }}is greater than60(about 20 minutes at 20 s per lap).$runIndexcounts how many times that node has run in this execution.
6. Download the file
Download video (HTTP Request):
GET https://api.revana.si/v1/generations/{{ $json.id }}/content- Options > Response > Response Format: File
The API redirects to a short-lived file link, and n8n follows it. The video arrives as binary data, ready for a Google Drive, S3, Slack or Write Binary File node. Result files expire, so store them; don't keep the link.
Alternative: a webhook instead of the polling loop
If your n8n is reachable on public HTTPS (n8n Cloud, or self-hosted behind a domain), you can drop the loop. Revana's queue endpoint accepts a completion webhook, and n8n's Wait node can pause until a URL is called.
- Change Wait to Resume: On Webhook Call, HTTP Method
POST, and set Limit Wait Time (for example 1 hour). - Change Create generation to the queue endpoint. It takes the model input directly, without the
{model, input}wrapper, withmax_cost_usdalongside, and the callback in thefal_webhookquery parameter:
POST https://api.revana.si/v1/queue/revana/{{ $('Settings').item.json.model }}?fal_webhook={{ encodeURIComponent($execution.resumeUrl) }}
Idempotency-Key: n8n-{{ $execution.id }}
{ "prompt": "...", "resolution": "720p", "duration": 5, "aspect_ratio": "9:16", "audio": true, "max_cost_usd": 1.05 }
- After the Wait node, don't trust the callback body. Revana's callbacks are unsigned, so fetch the result yourself with your key:
GET https://api.revana.si/v1/queue/revana/{model}/requests/{{ $('Create generation').item.json.request_id }}/status, and when it'sCOMPLETEDwithout anerror, download.../requests/{request_id}/contentwith Response Format: File.
The callback URL must be public HTTPS on port 443 with no redirects, so this won't work against localhost:5678. For local development, keep the polling loop.
Testing for $0
minimax-h3-free is a free model in the same catalog (5 to 15 seconds, 480p/720p/768p, sound always on). Set model to it in Settings, and the quote comes back $0.00. The account needs one top-up first (the smallest pack is $10, an anti-abuse rule), but free jobs never spend it. It's a good way to run the whole workflow end to end before you point it at a paid model.
If you'd rather not build it
-
Importable workflows. revana-skills/n8n has two ready-made JSON workflows (a text/image-to-video generator and a product-photo-to-UGC-ad flow) built the same way. Import with Workflows > Import from File, or
n8n import:workflow --input=seedance-2.5-video-generator.json. -
A community node.
n8n-nodes-revana(Settings > Community Nodes > Install, if your instance allows community nodes) wraps this into Generate Media, Get Generation, List Models and Chat operations, with a max-cost field and an optional wait for the result.
The same three calls work anywhere
Nothing here is n8n-specific. Make's HTTP module, Zapier's Webhooks by Zapier, Pipedream or a cron job can run the same sequence: POST /v1/generations/quote, POST /v1/generations with an Idempotency-Key and max_cost_usd, then GET /v1/generations/{id} on an interval and GET /v1/generations/{id}/content at the end. The full API is in the docs and openapi.json, and every model and price is at revana.si/pricing.
Prices and quotes in this post are from Revana's live API on November 5, 2026.
Top comments (0)