
How Much Does Daily AI Video Cost? A 30-Day API Budget
Calculate a 30-day vertical AI video budget, including retries, free-credit limits, safe API polling, storage, and human review in one practical guide.
For the configuration tested here, a one-video-per-day schedule costs $2.52
in generation fees over 30 days. Each attempt is one five-second, 480p, 9:16
clip with seedance-2.0-mini. The single smoke test cost 84 reAPI credits, or
$0.084, completed in 107.2 seconds, and returned one video URL.[8]
That is the simple answer, but it is not yet a production budget. A completed API task is not necessarily a clip you will publish. If review rejects half of your completed attempts, the generation budget doubles to $5.04 per month. Storage, scheduling, editing, and human review sit outside that figure.
This guide calculates the honest number and builds a single-worker example that stops for manual reconciliation instead of guessing whether an uncertain POST should be repeated.
TL;DR
- One tested 5-second, 480p, 9:16 Seedance 2.0 Mini task used 84 credits = $0.084. Thirty identical completed attempts would cost $2.52.
- Budget by completed attempts per accepted video, not just scheduled posts. At 1.5 attempts per accepted clip, the 30-day generation budget is $3.78; at two attempts, it is $5.04.
- Eligible new accounts may receive promotional test credits, but the public pricing page does not promise a fixed amount or daily reset.[5] Treat them as prototype runway, not recurring capacity.
- Save the task ID immediately. Every accepted repeated
POSTcreates a separate task and credit reservation; each completed task settles its own usage.Idempotency-Keyrecords a trace value but does not deduplicate video requests.[6] - Poll the task with
GETabout every five seconds, archive the returned file to storage you control, and keep publishing behind a manual review step.[7] - The 107.2-second result is one smoke test, not a claim about average speed, uptime, or visual quality.
The daily video question people are actually asking
One recent “free text-to-video API” question was unusually specific. The developer wanted a real REST API that could make one or two short vertical clips each day, return a public HTTPS URL, and work inside an automated posting flow. The prototype target was roughly four to five seconds at 9:16.[1]
Recovery matters as soon as a prototype becomes a schedule. In a separate r/n8n thread, a user building daily video automation reported repeated server errors and no clear recovery path from their chosen provider.[2] That is one user's experience, not a measured failure rate. It is still a useful design warning: the workflow needs durable state and a deliberate retry policy before it needs a clever prompt.
One 107-second run, with a narrow conclusion
The test used the least expensive current reAPI configuration we found that accepts a text prompt and explicitly supports both 9:16 and a four-to-five second duration: Seedance 2.0 Mini at 480p.[3] The model documentation allows 480p or 720p, 9:16 output, and durations from 4 to 15 seconds.[4]
| Test field | Recorded value |
|---|---|
| Date | September 3, 2026 |
| Task ID | task_01a06547163473ecb7dab4478ee1abaa |
| Model | seedance-2.0-mini |
| Request | 480p, 9:16, 5 seconds |
| Audio | Off |
| Watermark | Off |
| Submissions | 1 |
| Final status | Completed |
| Elapsed time | 107.2 seconds |
| Reported usage | 84 credits / $0.084 |
| Returned output | 1 video URL |
The archived output from this exact smoke test: five seconds, 480p, 9:16, audio off, and watermark off. Technical completion and billing were verified; creative acceptance was not scored.
The current 480p rate is $0.016625 per billable second.[3] reAPI bills one credit as $0.001 and rounds a task up to the next whole credit.[4] For the test request:
ceil($0.016625 × 5 seconds × 1,000 credits per dollar)
= ceil(83.125 credits)
= 84 credits
= $0.084An accepted POST reserves credits for its own task. The reservation settles
when that task reaches a terminal state: read status together with
usage.credits rather than treating the admission hold as the final bill.
Seedance 2.0 Mini refunds a failed generation under its documented billing
policy.[4][7]
This one result confirms the request path, completion status, returned URL, and charge. It cannot establish a typical render time, success rate, acceptance rate, or visual-quality score.
Download the smoke-test record. It contains the request settings, task ID, elapsed time, terminal status, and settled credits. The temporary task URL is omitted; the record points to the same video in permanent reAPI CDN storage.
Pricing can change. Check the live Seedance 2.0 Mini model page before turning these figures into a customer quote or a hard spending limit.
A 30-day budget starts with completed attempts
The minimum monthly calculation is straightforward:
30 days × 84 credits = 2,520 credits = $2.52That assumes one completed attempt becomes one accepted video every day. A more useful planning formula is:
monthly generation cost
= days × videos per day × completed attempts per accepted video × cost per attemptFor one accepted clip per day at the tested rate:
| Completed attempts per accepted video | Monthly attempts | 30-day API budget |
|---|---|---|
| 1.0 | 30 | $2.52 |
| 1.5 | 45 | $3.78 |
| 2.0 | 60 | $5.04 |
| 3.0 | 90 | $7.56 |
“1.5 attempts” is a monthly average, not half a request: 15 clips might pass first time and 15 need one replacement. Count completed generations rejected for bad motion, composition, or a mismatch with the brief.
Resolution and duration also change the starting unit cost. The following figures apply the current documented rate and billing formula; only the five-second 480p row was run as the smoke test for this article.
| Configuration | Credits per attempt | Cost per attempt | 30 attempts |
|---|---|---|---|
| 480p, 4 seconds | 67 | $0.067 | $2.01 |
| 480p, 5 seconds | 84 | $0.084 | $2.52 |
| 720p, 4 seconds | 144 | $0.144 | $4.32 |
| 720p, 5 seconds | 180 | $0.180 | $5.40 |
These are generation costs, not total content costs. They exclude prompt writing, source assets, editing, captions, music licensing, object storage, automation hosting, platform API fees, and the time spent approving each clip.
Why free credits are not sustainable capacity
Free credits can prove that authentication, request fields, polling, and download work. They cannot support a recurring promise without a known quantity and renewal rule.
Eligible new accounts may receive promotional test credits; availability and amount are shown at signup or in the dashboard. The public pricing page does not promise a fixed amount or daily reset.[5] That makes the grant unsuitable as dependable monthly capacity. The same rule applies to consumer-app allowances: without documented API access, limits, and reset behavior, UI credits are not an API budget.
Use the free balance to test one narrow path. Then budget the steady-state workflow at the paid rate and treat any free credit as a temporary reduction. That makes the project viable after launch rather than only on demo day.
Before you automate the first request
For a beginner-friendly setup, prepare these pieces:
- A reAPI account and server-side API key. Never put the key in browser code, a public repository, or a downloadable workflow.
- A prompt that describes one short shot. Five seconds is not enough time for a multi-scene story.
- A scheduler such as cron, GitHub Actions, n8n, or a queue worker.
- A durable place to save the daily job key and reAPI task ID.
- Storage you control for completed files.
- A person who will review the clip before it is published.
The examples below use UTC dates. If the content calendar follows a local time zone, calculate the job date in that zone consistently. Otherwise a job near midnight can receive the wrong day's key.
Submit one 9:16 video with cURL
Start by setting the API key in your shell:
export REAPI_API_KEY='replace-with-your-server-side-key'Submit one task and save the response before doing anything else:
curl --fail-with-body --silent --show-error \
--request POST 'https://reapi.ai/api/v1/videos/generations' \
--header "Authorization: Bearer $REAPI_API_KEY" \
--header 'Content-Type: application/json' \
--data '{
"model": "seedance-2.0-mini",
"prompt": "A handmade ceramic cup beside an open notebook, soft morning window light, one slow push-in shot, natural motion, no text",
"resolution": "480p",
"aspect_ratio": "9:16",
"duration": 5,
"generate_audio": false,
"watermark": false
}' \
--output submit.jsonOpen submit.json and copy its id. If you have jq, this extracts it:
TASK_ID="$(jq -r '.id' submit.json)"
test -n "$TASK_ID" && test "$TASK_ID" != 'null' || exit 1
printf '%s\n' "$TASK_ID" > task-id.txtPolling uses GET, which does not consume credits:[7]
curl --fail-with-body --silent --show-error \
--header "Authorization: Bearer $REAPI_API_KEY" \
"https://reapi.ai/api/v1/tasks/$TASK_ID" \
--output task.json
jq '{id, status, usage, output, error}' task.jsonRun the last command again after roughly five seconds while the status is
processing. The documented terminal states are completed and failed.
Do not resubmit the POST just because the first poll has no video yet.
Build a safer daily workflow in Node.js
The cURL example proves the fields. The script below is a single-worker, at-most-once guard, not an exactly-once queue. It saves intent before submission, records the task ID as soon as the response arrives, bounds each network call, archives locally, and stops for manual reconciliation whenever it cannot know whether a POST was accepted. It uses Node.js 20 or later and no external packages. Do not run two copies against the same JSON file.
Save it as daily-video.mjs:
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
const API_BASE = 'https://reapi.ai/api/v1';
const STORE_PATH = './daily-video-jobs.json';
const OUTPUT_DIR = './daily-video-output';
const POLL_MS = 5_000;
const POLL_LIMIT_MS = 30 * 60 * 1_000;
const POST_TIMEOUT_MS = 30_000;
const GET_TIMEOUT_MS = 20_000;
const DOWNLOAD_TIMEOUT_MS = 120_000;
const apiKey = process.env.REAPI_API_KEY;
if (!apiKey) {
throw new Error('Set REAPI_API_KEY in the server environment.');
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function retryAfterMs(value) {
if (!value) return POLL_MS;
const seconds = Number(value);
if (Number.isFinite(seconds)) return Math.max(seconds * 1_000, 0);
const date = Date.parse(value);
return Number.isNaN(date) ? POLL_MS : Math.max(date - Date.now(), 0);
}
async function loadJobs() {
try {
return JSON.parse(await readFile(STORE_PATH, 'utf8'));
} catch (error) {
if (error.code === 'ENOENT') return {};
throw error;
}
}
async function saveJobs(jobs) {
const temporaryPath = `${STORE_PATH}.tmp`;
await writeFile(temporaryPath, `${JSON.stringify(jobs, null, 2)}\n`);
await rename(temporaryPath, STORE_PATH);
}
async function requestJson(url, options) {
const response = await fetch(url, options);
const body = await response.text();
let data;
try {
data = JSON.parse(body);
} catch {
data = { raw: body };
}
if (!response.ok) {
const error = new Error(`HTTP ${response.status}: ${body.slice(0, 300)}`);
error.status = response.status;
error.retryAfterMs = retryAfterMs(response.headers.get('retry-after'));
error.requestId = data?.error?.request_id ?? null;
throw error;
}
return data;
}
async function archiveVideo(url, jobKey) {
const response = await fetch(url, {
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
});
if (!response.ok) {
throw new Error(`Video download failed with HTTP ${response.status}.`);
}
await mkdir(OUTPUT_DIR, { recursive: true });
const safeName = jobKey.replace(/[^a-z0-9-]/gi, '_');
const outputPath = join(OUTPUT_DIR, `${safeName}.mp4`);
const bytes = Buffer.from(await response.arrayBuffer());
await writeFile(outputPath, bytes);
return outputPath;
}
async function main() {
const utcDate = new Date().toISOString().slice(0, 10);
const jobKey = `daily-short:${utcDate}`;
const jobs = await loadJobs();
if (!jobs[jobKey]) {
// Persist intent before POST. A crash after this point must not cause an
// automatic second submission on the next scheduler run.
jobs[jobKey] = {
state: 'submitting',
createdAt: new Date().toISOString(),
idempotencyKey: jobKey,
};
await saveJobs(jobs);
try {
const task = await requestJson(`${API_BASE}/videos/generations`, {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
// Trace-only on reAPI. This header does not deduplicate the POST.
'Idempotency-Key': jobKey,
},
signal: AbortSignal.timeout(POST_TIMEOUT_MS),
body: JSON.stringify({
model: 'seedance-2.0-mini',
prompt:
'A handmade ceramic cup beside an open notebook, soft morning ' +
'window light, one slow push-in shot, natural motion, no text',
resolution: '480p',
aspect_ratio: '9:16',
duration: 5,
generate_audio: false,
watermark: false,
}),
});
if (!task.id) throw new Error('Submission returned no task ID.');
jobs[jobKey] = {
...jobs[jobKey],
state: 'processing',
taskId: task.id,
submittedAt: new Date().toISOString(),
};
await saveJobs(jobs);
} catch (error) {
const receivedHttpResponse = Number.isInteger(error.status);
jobs[jobKey] = {
...jobs[jobKey],
state: receivedHttpResponse ? 'submit_rejected' : 'submit_uncertain',
error: error instanceof Error ? error.message : String(error),
requestId: error.requestId ?? null,
};
await saveJobs(jobs);
throw new Error(
receivedHttpResponse
? `Submission for ${jobKey} was rejected. Review the HTTP error ` +
'before creating a separately counted retry.'
: `Submission for ${jobKey} is uncertain. Use ${jobKey} and the ` +
'saved request ID, if present, to inspect logs or the dashboard. ' +
'Do not submit again until a person reconciles the result.',
);
}
}
let job = jobs[jobKey];
if (job.state === 'pending_review' || job.state === 'approved') {
console.log(`${jobKey} already has an archived output: ${job.archivePath}`);
return;
}
if (job.state === 'completed_no_output') {
throw new Error(
`${jobKey} completed without a video URL. Inspect ${job.taskId}; do not POST again.`,
);
}
if (job.state === 'failed') {
throw new Error(
`${jobKey} failed. Review its settled usage before creating a new retry key.`,
);
}
if (!job.taskId) {
throw new Error(
`${jobKey} has no task ID. Resolve ${job.state} manually; do not resubmit.`,
);
}
if (!job.outputUrl) {
const deadline = Date.now() + POLL_LIMIT_MS;
let task;
while (Date.now() < deadline) {
try {
task = await requestJson(`${API_BASE}/tasks/${job.taskId}`, {
headers: { Authorization: `Bearer ${apiKey}` },
signal: AbortSignal.timeout(GET_TIMEOUT_MS),
});
} catch (error) {
const remainingMs = Math.max(deadline - Date.now(), 0);
if (error.status === 429) {
const waitMs = Math.min(error.retryAfterMs ?? POLL_MS, remainingMs);
console.warn(`Rate limited; waiting ${waitMs} ms before another GET.`);
if (waitMs > 0) await sleep(waitMs);
continue;
}
const networkError = !Number.isInteger(error.status);
const serverError = error.status >= 500 && error.status < 600;
if (!networkError && !serverError) {
throw error;
}
const waitMs = Math.min(POLL_MS, remainingMs);
console.warn('Network or server error; retrying the same task GET.');
if (waitMs > 0) await sleep(waitMs);
continue;
}
if (task.status === 'completed' || task.status === 'failed') break;
if (task.status !== 'processing') {
throw new Error(`Unexpected task status: ${task.status}`);
}
await sleep(Math.min(POLL_MS, Math.max(deadline - Date.now(), 0)));
}
if (!task || (task.status !== 'completed' && task.status !== 'failed')) {
jobs[jobKey] = { ...job, state: 'poll_timeout' };
await saveJobs(jobs);
throw new Error(
'Polling timed out. The task may still be running; do not POST again.',
);
}
if (task.status === 'failed') {
jobs[jobKey] = {
...job,
state: 'failed',
usageCredits: task.usage?.credits ?? null,
taskError: task.error ?? null,
};
await saveJobs(jobs);
throw new Error(
'Generation failed. Review settled usage before scheduling a retry.',
);
}
const outputUrl = task.output?.video_urls?.[0];
if (!outputUrl) {
jobs[jobKey] = {
...job,
state: 'completed_no_output',
usageCredits: task.usage?.credits ?? null,
completedAt: new Date().toISOString(),
};
await saveJobs(jobs);
throw new Error('Completed task returned no video URL; do not POST again.');
}
// Save completion before downloading so storage trouble cannot trigger POST.
jobs[jobKey] = {
...job,
state: 'completed',
outputUrl,
usageCredits: task.usage?.credits ?? null,
completedAt: new Date().toISOString(),
};
await saveJobs(jobs);
job = jobs[jobKey];
}
try {
const archivePath = await archiveVideo(job.outputUrl, jobKey);
jobs[jobKey] = {
...job,
state: 'pending_review',
archivePath,
archiveError: null,
};
await saveJobs(jobs);
console.log(`Saved ${archivePath}. Review it before publishing.`);
} catch (error) {
jobs[jobKey] = {
...job,
state: 'archive_failed',
archiveError: error instanceof Error ? error.message : String(error),
};
await saveJobs(jobs);
throw new Error(
'Archiving failed. The task remains completed; rerun to retry only the download.',
);
}
}
await main();Run it once:
node daily-video.mjsThe JSON file is restart-readable on one machine, but it is not a multi-worker
lock. In production, insert a database row with a unique job_key, then let
only the worker that wins an atomic claim move it from scheduled to
submitting. A database claim prevents two live workers from submitting
together. It cannot make a database write and an external API request atomic.
What happens after a crash
The safe next action depends on the last state that reached storage:
| Last durable state | What the workflow knows | Next action |
|---|---|---|
| No row | The intent save did not finish, and this code never POSTs before that save | Start normally |
submitting or submit_uncertain, no task ID | The POST may not have started, or it may have been accepted before the response was lost | Stop and reconcile manually; never auto-POST |
submit_rejected | A non-2xx response arrived | Review the error, then create a separately counted retry deliberately |
processing or poll_timeout, task ID present | The task exists | Resume GET polling for that ID |
failed | The task reached a terminal error and settled its usage | Review usage.credits; use a new retry key only after a deliberate decision |
completed or archive_failed, output URL present | Generation is finished | Retry only the download |
completed_no_output | The task ended without the expected URL | Inspect that task; do not POST again |
pending_review or approved | The file is archived | Exit without API work |
The uncertain state is the unavoidable gap. A connection can fail after reAPI
accepts a request but before the task ID reaches your process. The script sends
the daily job key as Idempotency-Key so logs can correlate the attempt, but
reAPI records that header for tracing only; it does not deduplicate.
[6] An accepted second POST creates another
task and reservation, and a second completion settles another charge. If logs
or the dashboard cannot settle the ambiguity, the operator must choose between
skipping that day's clip and risking a duplicate. There is no honest automatic
exactly-once answer in this example.
Task lookup is different. Retrying GET neither creates a generation nor uses
credits. The example caps each request, stops on deterministic 4xx errors,
honors Retry-After on 429, and retries network errors or 5xx responses within
the 30-minute polling window. A five-second interval also avoids repeatedly
reading the task endpoint's five-second in-flight cache.[7]
Review and store the clip before publishing
The API's job ends when it delivers the file. A technically completed clip can still misrepresent the brief or fail on a social feed. Review every result for:
- the intended subject, action, and camera movement;
- distorted objects, hands, faces, packaging, or logos;
- unwanted words or invented product claims;
- flicker, sudden scene changes, or unsafe crops;
- rights to the prompt, reference material, music, and likenesses;
- the destination platform's current technical and disclosure requirements.
Mark a clip approved only after that check. If it fails, create a separately
counted retry such as daily-short:2026-09-03:retry-1; never erase the first
attempt from the cost ledger.
Move the MP4 before adding it to tomorrow's publishing queue. reAPI's output URL is a handoff location, not a permanent media library.[7] The tutorial saves locally; use S3, R2, or your normal object store on an ephemeral deployment.
What the $2.52 figure does not promise
The monthly number is a transparent scenario, not a guarantee. It assumes the same price, duration, resolution, and number of completed attempts for 30 days. It does not establish:
- normal queue or generation time from one 107.2-second observation;
- service uptime or a failure rate;
- a visual acceptance rate for your prompts;
- exact output dimensions beyond the requested 480p and 9:16 settings;
- successful publication to any social platform;
- future prices or free-credit availability.
Keep those variables visible in the dashboard. At minimum, record job_key,
task_id, requested settings, final status, usage.credits, review decision,
and archived object URL. After 30 days, replace the assumed attempts-per-video
number with your own measured acceptance rate.
Frequently asked questions
How much does one five-second vertical AI video cost per day?
The tested Seedance 2.0 Mini configuration cost 84 credits, or $0.084, for one completed five-second 480p 9:16 task. At one completed attempt per day, 30 days cost $2.52. Check the live model price before budgeting a future month.
What if I want two videos per day?
At one completed attempt per accepted video, 60 attempts would cost $5.04 at the tested rate. Multiply that by your measured attempts per accepted video. For example, 1.5 attempts per acceptance would make the generation budget $7.56.
Is there a free text-to-video API that resets every day?
Do not assume one unless the provider documents API-key access, the exact free quantity, eligible models, and a reset schedule. Eligible reAPI accounts may receive promotional test credits, with availability and amount shown at signup or in the dashboard; the public pricing page does not promise a fixed grant or daily reset.[5]
Why count completed attempts instead of published clips?
Because an API can complete a technically valid video that fails creative, brand, legal, or crop review. The bill follows generation usage; your content calendar follows accepted clips. Recording both numbers reveals the real cost per publishable result.
Do failed Seedance 2.0 Mini tasks cost credits?
The model documentation says failed generations are refunded
automatically.[4] Still record the final task
status and usage.credits rather than assuming the value. A failed request can
also cost time and may lead to a separately billed replacement attempt.
Can I safely retry the submission with the same idempotency key?
No. Each accepted POST creates a separate task and reserves credits; each
completed task then settles its own usage.credits. Idempotency-Key is a
trace value, not deduplication.[6] Persist your
own job key and task ID, and stop for manual investigation when the submission
result is uncertain.
Can I publish directly from the returned video URL?
Archive the file first. Task output links are not a substitute for storage you control, and direct publication skips the review step. Save the video, review it, then pass your stored object to the publishing workflow.[7]
Is 107.2 seconds the normal generation time?
We do not know. It is the elapsed time of one smoke test. Measuring normal latency or reliability requires many dated runs and a defined sample, including failed and timed-out tasks.
Conclusion
The useful answer to “How much does daily AI video cost?” is not merely $0.084 per call. It is $2.52 for a perfect 30-day run, or $3.78–$7.56 when the workflow averages 1.5–3 completed attempts per accepted clip. Start with that range, measure your own review pass rate, and keep free credits out of the recurring capacity plan.
The first production milestone is equally practical: one atomically claimed
daily job key, one saved task ID, safe GET polling, one archived file, and one
human approval. Once that loop works, scheduling it every day is the easy part.
References
- Reddit, r/generativeAI. “Is there a free text-to-video API with credits that reset daily?”. Published August 3, 2026; retrieved September 3, 2026.
- Reddit, r/n8n. “Kie.ai Sora2 API frequent errors — any reliable alternatives?”. Published February 1, 2026; retrieved September 3, 2026.
- reAPI. Video model catalog and Seedance 2.0 Mini model page. Retrieved September 3, 2026.
- reAPI. Seedance 2.0 Mini API documentation. Retrieved September 3, 2026.
- reAPI. Pricing. Retrieved September 3, 2026.
- reAPI. API overview. Retrieved September 3, 2026.
- reAPI. Task status and polling documentation. Retrieved September 3, 2026.
- reAPI. First-party Seedance 2.0 Mini smoke-test
record. Task
task_01a06547163473ecb7dab4478ee1abaa, completed September 3, 2026. One 480p, 9:16, five-second request with audio and watermark disabled; 84 credits, 107.2 seconds, and one returned video URL.
Further reading
Author

Categories
More Posts

Seedance 2.0 Safety Filters: What They Block and Why
Seedance 2.0 safety filtering runs in several layers, and a refusal rarely says which one fired. What each layer blocks and which limits never move.


MiniMax H3 vs Seedance 2.5: Which AI Video Model Wins?
Compare MiniMax H3 vs Seedance 2.5 on duration, 2K output, native audio, multimodal references, editing, pricing, API access, and best use cases.


How to Turn One Product Photo into a Multi-Scene AI Video Ad
Turn one product photo into a planned multi-scene AI video ad with a creative brief, shot cards, reference rules, prompts, API examples, and QA checks.
