> ## Documentation Index
> Fetch the complete documentation index at: https://tfstudio.truefan.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Generate videos

> Render one video, or many, from a template in a single call

Every video — whether it's one or thousands — is generated the same way: call this endpoint with a list of `rows`, one object per video (up to 20,000 per request). For a single video, pass a `rows` list with one item.

Use it for a single generation triggered by a user action in your own app, or with a list of people or items — customers, leads, students — to generate one personalized video per row, all from the same template.

## Why `rows`, not a CSV

Generation takes `rows` — a JSON array of objects — directly in the request body. There's no CSV to format and no file to host anywhere first: if your data already lives in a database, spreadsheet export, or CRM, you serialize it straight into the request.

```json theme={null}
{
  "rows": [
    { "customer_name": "Anmol", "score": "95" },
    { "customer_name": "Rahul", "score": "88" }
  ],
  "batch_name": "August campaign"
}
```

Each row's keys should match the variable names from [Get template schema](/docs/api-reference/templates/get-template-schema). A row missing a key just leaves that one variable blank in that row's video — the rest of the batch is unaffected.

<Tip>
  Generating just one video? Send `rows` with a single item — `batch_name` is optional either way.
</Tip>

## Correlating results back to your own data

Add an `external_id` to any row — your own identifier (a user ID, order ID, anything) — and it comes back unchanged in the [CSV export](/docs/api-reference/status/export-bulk-job-csv) once the batch is done, or immediately via [Get single video status](/docs/api-reference/status/get-generation-status-by-external-id) while it's still processing. This is the reliable way to match a result to the input that produced it, since it doesn't depend on remembering the original order you sent `rows` in:

```json theme={null}
{
  "rows": [
    { "customer_name": "Anmol", "score": "95", "external_id": "usr_9182" },
    { "customer_name": "Rahul", "score": "88", "external_id": "usr_4471" }
  ]
}
```

`external_id` is a reserved key — it's stripped out before the rest of the row is used to fill in the template's variables, so it never conflicts with a variable of the same name.

## Submitting a batch

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://dev-backend-ai.truefans.in/api/external/v1/templates/{template_id}/bulk-generate/" \
    -H "Authorization: Bearer $TRUEFAN_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "rows": [
        {"customer_name": "Anmol", "score": "95"},
        {"customer_name": "Rahul", "score": "88"}
      ],
      "batch_name": "August campaign"
    }'
  ```

  ```python Python theme={null}
  import requests

  rows = [
      {"customer_name": row["name"], "score": str(row["score"])}
      for row in my_customers  # however you already have this data
  ]

  resp = requests.post(
      f"https://dev-backend-ai.truefans.in/api/external/v1/templates/{template_id}/bulk-generate/",
      headers={"Authorization": f"Bearer {api_key}"},
      json={"rows": rows, "batch_name": "August campaign"},
  )
  resp.raise_for_status()
  bulk_job_id = resp.json()["bulk_job_id"]
  ```
</CodeGroup>

```json Response theme={null}
{
  "bulk_job_id": "8c1f2e3a-...",
  "status": "pending",
  "row_count": 2,
  "estimated_credits": "1.36"
}
```

<Note>
  The template's scenes are snapshotted at submission time — the whole batch renders consistently against that snapshot even if the template is edited in the editor while the batch is still processing.
</Note>

## Tracking progress

```bash theme={null}
curl "https://dev-backend-ai.truefans.in/api/external/v1/bulk-jobs/{bulk_job_id}/status/" \
  -H "Authorization: Bearer $TRUEFAN_API_KEY"
```

```json Response — while processing theme={null}
{
  "bulk_job_id": "8c1f2e3a-...",
  "status": "processing",
  "batch_name": "August campaign",
  "summary": { "total": 2, "success": 1, "failed": 0, "pending": 1 },
  "csv_ready": false
}
```

`summary` is the headline most integrations poll for while a batch is still running. This endpoint doesn't return per-row detail — to check on a specific row before the batch finishes, use [Get single video status](/docs/api-reference/status/get-generation-status-by-external-id).

Once `status` is `done`, `csv_ready` flips to `true` — at that point, download every row in one file via [Download CSV export](/docs/api-reference/status/export-bulk-job-csv):

```bash theme={null}
curl "https://dev-backend-ai.truefans.in/api/external/v1/bulk-jobs/{bulk_job_id}/export.csv" \
  -H "Authorization: Bearer $TRUEFAN_API_KEY" \
  -O -J
```

<Tip>
  Configure a [webhook](/docs/guides/webhooks) to receive a `bulk_job.completed` event when the whole batch finishes — its payload includes the export's path directly, so you don't need to poll status first just to check `csv_ready`.
</Tip>

## Cost

A batch costs `template.estimated_credits × row_count`, deducted upfront when the batch is accepted. If the workspace doesn't have enough balance, the whole batch is rejected with `402` before anything is created — no partial batches.
