Seedance 2.5 is live — 30-second cinematic video with native audio & real-person references
How Much Does Daily AI Video Cost? A 30-Day API Budget
2026/09/03

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 POST creates a separate task and credit reservation; each completed task settles its own usage. Idempotency-Key records a trace value but does not deduplicate video requests.[6]
  • Poll the task with GET about 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 fieldRecorded value
DateSeptember 3, 2026
Task IDtask_01a06547163473ecb7dab4478ee1abaa
Modelseedance-2.0-mini
Request480p, 9:16, 5 seconds
AudioOff
WatermarkOff
Submissions1
Final statusCompleted
Elapsed time107.2 seconds
Reported usage84 credits / $0.084
Returned output1 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.084

An 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.52

That 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 attempt

For one accepted clip per day at the tested rate:

Completed attempts per accepted videoMonthly attempts30-day API budget
1.030$2.52
1.545$3.78
2.060$5.04
3.090$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.

ConfigurationCredits per attemptCost per attempt30 attempts
480p, 4 seconds67$0.067$2.01
480p, 5 seconds84$0.084$2.52
720p, 4 seconds144$0.144$4.32
720p, 5 seconds180$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.json

Open 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.txt

Polling 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.json

Run 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.mjs

The 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 stateWhat the workflow knowsNext action
No rowThe intent save did not finish, and this code never POSTs before that saveStart normally
submitting or submit_uncertain, no task IDThe POST may not have started, or it may have been accepted before the response was lostStop and reconcile manually; never auto-POST
submit_rejectedA non-2xx response arrivedReview the error, then create a separately counted retry deliberately
processing or poll_timeout, task ID presentThe task existsResume GET polling for that ID
failedThe task reached a terminal error and settled its usageReview usage.credits; use a new retry key only after a deliberate decision
completed or archive_failed, output URL presentGeneration is finishedRetry only the download
completed_no_outputThe task ended without the expected URLInspect that task; do not POST again
pending_review or approvedThe file is archivedExit 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

  1. Reddit, r/generativeAI. “Is there a free text-to-video API with credits that reset daily?”. Published August 3, 2026; retrieved September 3, 2026.
  2. Reddit, r/n8n. “Kie.ai Sora2 API frequent errors — any reliable alternatives?”. Published February 1, 2026; retrieved September 3, 2026.
  3. reAPI. Video model catalog and Seedance 2.0 Mini model page. Retrieved September 3, 2026.
  4. reAPI. Seedance 2.0 Mini API documentation. Retrieved September 3, 2026.
  5. reAPI. Pricing. Retrieved September 3, 2026.
  6. reAPI. API overview. Retrieved September 3, 2026.
  7. reAPI. Task status and polling documentation. Retrieved September 3, 2026.
  8. 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