# Tscaps Developer Documentation — Complete API Reference
> Complete, aggregated technical documentation for the Tscaps Automation API.
> Generated automatically for Large Language Models and AI coding agents.
> Canonical documentation: https://tscaps.io/docs
---
## Document: Automation API
> Description: Create captioned videos programmatically from your product or workflow.
> Path: /docs/api
The tscaps automation API lets you generate videos with burned-in captions programmatically. Submit a video source and a caption template, track processing progress, and download the finished video file.
All rendering and subtitle burn-in runs on our cloud infrastructure, powered by the same typography and animation engine as the tscaps Studio editor.
> **Building with AI coding agents?**
>
> You can equip your AI assistant (Cursor, Claude Code, GitHub Copilot, Windsurf, etc.) with the complete API specification or install our official skill:
> - **Full documentation dump:** [llms-full.txt](/docs/llms-full.txt) (single markdown file containing all endpoint schemas and parameters).
> - **Documentation map:** [llms.txt](/docs/llms.txt) (structured index per [llmstxt.org](https://llmstxt.org)).
> - **Official Agent Skill (`tscaps-api`):** Install via `npx skills add francozanardi/tscaps --skill tscaps-api` or inspect [SKILL.md on GitHub](https://github.com/francozanardi/tscaps/tree/main/skills/tscaps-api).
## Authentication
All requests to the Automation API require an API key. Pass your secret key in the `Authorization` header as a Bearer token:
```http
Authorization: Bearer TSCAPS_API_KEY
```
You can generate and manage keys in your [API keys dashboard](/app/developers/keys). Keep your keys secure and do not expose them in client-side code.
The API base URL is:
```http
https://api.tscaps.io
```
---
## Quickstart
Follow these three steps to submit your first video, track rendering progress, and retrieve the finished output.
### 1. Start a job
Send a `POST` request to `/v1/automation-jobs` with a publicly accessible video URL and the ID of the template you want to apply.
```bash
curl -X POST https://api.tscaps.io/v1/automation-jobs \
-H "Authorization: Bearer $TSCAPS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"videoUrl": "https://example.com/reel.mp4",
"templateId": "mira"
}'
```
```javascript
const response = await fetch("https://api.tscaps.io/v1/automation-jobs", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TSCAPS_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
videoUrl: "https://example.com/reel.mp4",
templateId: "mira",
}),
});
const job = await response.json();
console.log(`Job queued: ${job.id}`);
```
```python
import os
import requests
response = requests.post(
"https://api.tscaps.io/v1/automation-jobs",
headers={"Authorization": f"Bearer {os.environ['TSCAPS_API_KEY']}"},
json={
"videoUrl": "https://example.com/reel.mp4",
"templateId": "mira",
},
)
job = response.json()
print(f"Job queued: {job['id']}")
```
The API responds immediately with `202 Accepted` and a job object:
```json
{
"id": "9f2c48e1-5b7a-4a29-8874-9dfc8227b610",
"status": "queued",
"phase": "queued",
"templateId": "mira",
"quotedSeconds": null,
"format": "mp4",
"quality": "high",
"resolution": null,
"percent": null,
"downloadUrl": null,
"outputExpiresAt": null,
"failureReason": null,
"failureCode": null,
"settledAt": null,
"createdAt": "2026-09-12T10:11:38.000Z"
}
```
> **Direct file uploads:** If your video file is stored locally or behind a private firewall, use our [Direct file uploads](/docs/api/uploads) flow to upload the binary file directly via presigned URLs.
### 2. Check job status
Video processing is asynchronous. Poll `GET /v1/automation-jobs/:id` until `status` resolves to `succeeded` or `failed`.
```bash
curl https://api.tscaps.io/v1/automation-jobs/9f2c48e1-5b7a-4a29-8874-9dfc8227b610 \
-H "Authorization: Bearer $TSCAPS_API_KEY"
```
```javascript
async function waitForVideo(jobId) {
while (true) {
const response = await fetch(`https://api.tscaps.io/v1/automation-jobs/${jobId}`, {
headers: { Authorization: `Bearer ${process.env.TSCAPS_API_KEY}` },
});
const state = await response.json();
if (state.status === "succeeded") {
return state.downloadUrl;
}
if (state.status === "failed") {
throw new Error(`Job failed: ${state.failureCode} (${state.failureReason})`);
}
if (state.status === "cancelled") {
throw new Error("Job was cancelled.");
}
// Wait 5 seconds before checking again
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
const downloadUrl = await waitForVideo(job.id);
console.log(`Finished video: ${downloadUrl}`);
```
```python
import os
import time
import requests
def wait_for_video(job_id):
headers = {"Authorization": f"Bearer {os.environ['TSCAPS_API_KEY']}"}
while True:
response = requests.get(
f"https://api.tscaps.io/v1/automation-jobs/{job_id}",
headers=headers,
)
state = response.json()
if state["status"] == "succeeded":
return state["downloadUrl"]
if state["status"] == "failed":
raise RuntimeError(f"Job failed: {state['failureCode']} ({state['failureReason']})")
if state["status"] == "cancelled":
raise RuntimeError("Job was cancelled.")
# Wait 5 seconds before checking again
time.sleep(5)
download_url = wait_for_video(job["id"])
print(f"Finished video: {download_url}")
```
As the job runs, the `phase` property progresses through:
- `queued`: Waiting for processing capacity.
- `ingesting`: Downloading and validating the source video.
- `transcribing`: Extracting speech and aligning timestamps.
- `rendering`: Drawing caption animations and burning them into the video frames (`percent` reflects render completion from 0 to 100).
- `succeeded`: The output file is rendered, uploaded, and ready to download.
> **Real-time webhook notifications**
>
> Instead of polling, configure a `webhookUrl` to receive an instant callback as soon as the job finishes. Learn more in the [Webhooks guide](/docs/api/webhooks).
### 3. Download the result
When `status` reaches `succeeded`, the response includes `downloadUrl`, a secure presigned URL pointing directly to the final rendered video, and `outputExpiresAt` indicating when the link expires (stored for 7 days).
```json
{
"id": "9f2c48e1-5b7a-4a29-8874-9dfc8227b610",
"status": "succeeded",
"phase": "succeeded",
"templateId": "mira",
"quotedSeconds": 48,
"format": "mp4",
"quality": "high",
"resolution": { "width": 1080, "height": 1920 },
"percent": null,
"downloadUrl": "https://storage.tscaps.io/outputs/9f2c48e1-...?token=...",
"outputExpiresAt": "2026-09-19T10:14:00.000Z",
"failureReason": null,
"failureCode": null,
"settledAt": "2026-09-12T10:12:26.000Z",
"createdAt": "2026-09-12T10:11:38.000Z"
}
```
---
## Next steps
- **[API reference](/docs/api/reference)**: Inspect every endpoint, request parameter, frame budget rule, and failure code.
- **[Direct file uploads](/docs/api/uploads)**: Learn how to upload private video files without a public URL.
- **[Webhooks](/docs/api/webhooks)**: Receive asynchronous delivery notifications and verify signatures.
- **[Limits & quotas](/docs/api/limits)**: Review duration, resolution, concurrency, and rate limits per plan.
---
## Document: API reference
> Description: Endpoints, request fields, job states, and failure codes for the Automation API.
> Path: /docs/api/reference
The tscaps automation API is organized around RESTful JSON endpoints. Send your secret API key as a bearer token in the `Authorization` header with every request:
```http
Authorization: Bearer TSCAPS_API_KEY
Content-Type: application/json
```
Base URL:
```http
https://api.tscaps.io
```
---
## Endpoints
| Method | Path | Purpose |
| --- | --- | --- |
| `POST` | `/v1/automation-jobs` | [Create a job](#create-a-job) |
| `GET` | `/v1/automation-jobs/:id` | [Get job status](#get-job-status) |
| `POST` | `/v1/automation-jobs/:id/cancel` | [Cancel a job](#cancel-a-job) |
| `POST` | `/v1/automation-jobs/uploads` | [Create an upload URL](#create-an-upload-url) |
| `GET` | `/v1/automation-jobs` | [List jobs](#list-jobs) |
---
## Create a job
`POST /v1/automation-jobs`
Queues a new video processing job with caption styling and subtitle burn-in.
### Request parameters
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `videoUrl` | string | One source | Publicly accessible HTTPS URL of the video file to process. Specify either `videoUrl` or `uploadId`, never both. |
| `uploadId` | string | One source | ID of a file uploaded via `POST /v1/automation-jobs/uploads`. Specify either `videoUrl` or `uploadId`, never both. |
| `templateId` | string | Yes | ID of the caption template to apply. Use built-in templates (such as `mira` or `noor`) or custom templates saved to your account. View all available templates in your [Templates catalog](/app/developers/templates). |
| `format` | string | No | Output video container: `mp4` or `webm`. Defaults to `mp4`. |
| `quality` | string | No | Encoding bitrate quality: `low`, `medium`, or `high`. Defaults to `high`. Selecting higher quality does not change minute consumption. |
| `resolution` | object | No | Output dimensions in pixels as `{ "width": number, "height": number }`. Must not exceed your plan's frame budget. Omit to preserve the source resolution (automatically downscaled if over budget). |
| `language` | string | No | Language spoken in the video as a 2-letter ISO/BCP-47 code (e.g., `en`, `es`, `fr`). Automatically detected if omitted. |
| `webhookUrl` | string | No | Public HTTPS URL where an event notification will be sent when the job reaches a terminal state (`succeeded`, `failed`, or `cancelled`). |
### Request example
```json
{
"videoUrl": "https://example.com/reel.mp4",
"templateId": "mira",
"format": "mp4",
"quality": "high",
"resolution": { "width": 1080, "height": 1920 },
"language": "en",
"webhookUrl": "https://api.yourdomain.com/webhooks/tscaps"
}
```
### Code example
```bash
curl -X POST https://api.tscaps.io/v1/automation-jobs \
-H "Authorization: Bearer $TSCAPS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"videoUrl": "https://example.com/reel.mp4",
"templateId": "mira",
"quality": "high"
}'
```
```javascript
const response = await fetch("https://api.tscaps.io/v1/automation-jobs", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TSCAPS_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
videoUrl: "https://example.com/reel.mp4",
templateId: "mira",
quality: "high",
}),
});
const job = await response.json();
```
```python
import os
import requests
response = requests.post(
"https://api.tscaps.io/v1/automation-jobs",
headers={"Authorization": f"Bearer {os.environ['TSCAPS_API_KEY']}"},
json={
"videoUrl": "https://example.com/reel.mp4",
"templateId": "mira",
"quality": "high",
},
)
job = response.json()
```
### Response
Returns `202 Accepted` with the initial job record.
```json
{
"id": "9f2c48e1-5b7a-4a29-8874-9dfc8227b610",
"status": "queued",
"phase": "queued",
"templateId": "mira",
"quotedSeconds": null,
"format": "mp4",
"quality": "high",
"resolution": null,
"percent": null,
"downloadUrl": null,
"outputExpiresAt": null,
"failureReason": null,
"failureCode": null,
"settledAt": null,
"createdAt": "2026-09-12T10:11:38.000Z"
}
```
---
## Get job status
`GET /v1/automation-jobs/:id`
Retrieves the current status and rendering progress of a specific job.
### Path parameters
| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | The unique UUID of the automation job. |
### Code example
```bash
curl https://api.tscaps.io/v1/automation-jobs/9f2c48e1-5b7a-4a29-8874-9dfc8227b610 \
-H "Authorization: Bearer $TSCAPS_API_KEY"
```
```javascript
const response = await fetch("https://api.tscaps.io/v1/automation-jobs/9f2c48e1-5b7a-4a29-8874-9dfc8227b610", {
headers: { Authorization: `Bearer ${process.env.TSCAPS_API_KEY}` },
});
const job = await response.json();
```
```python
import os
import requests
response = requests.get(
"https://api.tscaps.io/v1/automation-jobs/9f2c48e1-5b7a-4a29-8874-9dfc8227b610",
headers={"Authorization": f"Bearer {os.environ['TSCAPS_API_KEY']}"},
)
job = response.json()
```
### Response
Returns `200 OK` with the complete job record.
```json
{
"id": "9f2c48e1-5b7a-4a29-8874-9dfc8227b610",
"status": "succeeded",
"phase": "succeeded",
"templateId": "mira",
"quotedSeconds": 74,
"format": "mp4",
"quality": "high",
"resolution": { "width": 1080, "height": 1920 },
"percent": null,
"downloadUrl": "https://storage.tscaps.io/outputs/9f2c48e1-...?token=...",
"outputExpiresAt": "2026-09-19T10:14:00.000Z",
"failureReason": null,
"failureCode": null,
"settledAt": "2026-09-12T10:12:52.000Z",
"createdAt": "2026-09-12T10:11:38.000Z"
}
```
### Job lifecycle properties
- **`status`**: High-level execution state. One of:
- `queued`: Waiting in queue for processing capacity.
- `running`: Actively processing.
- `succeeded`: Video rendered and available for download.
- `failed`: Job encountered an unrecoverable error.
- `cancelled`: Job was cancelled by user request.
- **`phase`**: Detailed operation currently in progress: `queued`, `ingesting`, `transcribing`, `rendering`, `succeeded`, `failed`, or `cancelled`.
- **`percent`**: Integer between 0 and 100 indicating rendering progress during the `rendering` phase. `null` in other phases.
- **`quotedSeconds`**: Measured duration of the input video in seconds (populated after ingest).
- **`downloadUrl`**: Presigned HTTPS download link to the final video (populated upon `succeeded`).
- **`outputExpiresAt`**: ISO timestamp when `downloadUrl` expires (rendered files remain available for 7 days).
---
## Cancel a job
`POST /v1/automation-jobs/:id/cancel`
Cancels a queued or currently executing job. If the job is active, rendering stops immediately. All minutes reserved for the job are refunded back to your account balance.
This endpoint is idempotent: repeating the cancel call on an already cancelled job returns `200 OK`.
### Path parameters
| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | The unique UUID of the job to cancel. |
### Code example
```bash
curl -X POST https://api.tscaps.io/v1/automation-jobs/9f2c48e1-5b7a-4a29-8874-9dfc8227b610/cancel \
-H "Authorization: Bearer $TSCAPS_API_KEY"
```
```javascript
const response = await fetch("https://api.tscaps.io/v1/automation-jobs/9f2c48e1-5b7a-4a29-8874-9dfc8227b610/cancel", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.TSCAPS_API_KEY}` },
});
const job = await response.json();
```
```python
import os
import requests
response = requests.post(
"https://api.tscaps.io/v1/automation-jobs/9f2c48e1-5b7a-4a29-8874-9dfc8227b610/cancel",
headers={"Authorization": f"Bearer {os.environ['TSCAPS_API_KEY']}"},
)
job = response.json()
```
### Response
Returns `200 OK` with the cancelled job record (`status: "cancelled"`).
---
## Create an upload URL
`POST /v1/automation-jobs/uploads`
Requests a signed URL to upload a video file directly to tscaps cloud storage when the video cannot be exposed over a public URL.
### Request parameters
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `fileName` | string | Yes | Original file name (e.g., `reel.mp4`). Used to determine media content type. |
| `sizeBytes` | integer | Yes | Exact file size in bytes. This value is cryptographically signed into the presigned URL. |
### Request example
```json
{
"fileName": "reel.mp4",
"sizeBytes": 14205000
}
```
### Code example
```bash
curl -X POST https://api.tscaps.io/v1/automation-jobs/uploads \
-H "Authorization: Bearer $TSCAPS_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "fileName": "reel.mp4", "sizeBytes": 14205000 }'
```
```javascript
const response = await fetch("https://api.tscaps.io/v1/automation-jobs/uploads", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TSCAPS_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ fileName: "reel.mp4", sizeBytes: 14205000 }),
});
const upload = await response.json();
```
```python
import os
import requests
response = requests.post(
"https://api.tscaps.io/v1/automation-jobs/uploads",
headers={"Authorization": f"Bearer {os.environ['TSCAPS_API_KEY']}"},
json={"fileName": "reel.mp4", "sizeBytes": 14205000},
)
upload = response.json()
```
### Response
Returns `201 Created` with upload instructions:
```json
{
"uploadId": "3b71d39a-7c22-48a3-92f1-47fae3223019",
"uploadUrl": "https://storage.tscaps.io/uploads/3b71d39a-...?signature=...",
"contentType": "video/mp4",
"sizeBytes": 14205000,
"expiresAt": "2026-09-12T10:26:38.000Z"
}
```
Upload the file binary to `uploadUrl` via HTTP `PUT` with the returned `Content-Length` and `Content-Type`. Once uploaded, pass `uploadId` to `POST /v1/automation-jobs`. Read the [Direct file uploads guide](/docs/api/uploads) for complete examples.
---
## List jobs
`GET /v1/automation-jobs`
Retrieves a paginated list of recent automation jobs for your account, ordered newest first.
### Query parameters
| Parameter | Type | Description |
| --- | --- | --- |
| `cursor` | string | Opaque pagination cursor. Pass `nextCursor` from the previous response to retrieve the next page. |
### Code example
```bash
curl "https://api.tscaps.io/v1/automation-jobs?cursor=eyJjcmVhdGVkQXQiOi..." \
-H "Authorization: Bearer $TSCAPS_API_KEY"
```
```javascript
const response = await fetch("https://api.tscaps.io/v1/automation-jobs", {
headers: { Authorization: `Bearer ${process.env.TSCAPS_API_KEY}` },
});
const { jobs, nextCursor } = await response.json();
```
```python
import os
import requests
response = requests.get(
"https://api.tscaps.io/v1/automation-jobs",
headers={"Authorization": f"Bearer {os.environ['TSCAPS_API_KEY']}"},
)
data = response.json()
jobs = data["jobs"]
next_cursor = data["nextCursor"]
```
### Response
Returns `200 OK`:
```json
{
"jobs": [
{
"id": "9f2c48e1-5b7a-4a29-8874-9dfc8227b610",
"status": "succeeded",
"phase": "succeeded",
"templateId": "mira",
"quotedSeconds": 74,
"format": "mp4",
"quality": "high",
"resolution": { "width": 1080, "height": 1920 },
"percent": null,
"downloadUrl": "https://...",
"outputExpiresAt": "2026-09-19T10:14:00.000Z",
"failureReason": null,
"failureCode": null,
"settledAt": "2026-09-12T10:12:52.000Z",
"createdAt": "2026-09-12T10:11:38.000Z"
}
],
"nextCursor": null
}
```
---
## Failure codes
When a job fails, the response includes:
- **`failureCode`**: A machine-readable identifier for program logic.
- **`failureReason`**: A human-readable description for debugging or logging.
Always branch on `failureCode` in your application and include a generic fallback for any future codes:
| Failure Code | Meaning | Retryable | Resolution |
| --- | --- | :---: | --- |
| `source-unreachable` | The video URL could not be reached or returned HTTP 4xx/5xx. | Yes | Check that the URL is public, unauthenticated, and online. |
| `source-unreadable` | The file was downloaded but could not be parsed as a valid video container. | No | Ensure the file is an encoded MP4, WebM, MOV, or supported container. |
| `source-too-long` | The video exceeds your plan's maximum duration limit (3 min Free, 5 min Starter, 10 min Pro). | No | Shorten the video or upgrade your subscription plan. |
| `output-larger-than-source` | The requested `resolution` is larger than the input video dimensions. | No | Tscaps does not upscale. Omit `resolution` or specify equal or smaller dimensions. |
| `automation-minutes-exhausted` | The account has insufficient automation minutes to process the video. | No | [Buy more minutes](/app/developers/minutes) or wait for monthly renewal. |
| `ingest-failed` | Audio extraction or preliminary video probing failed. | Yes | Check video codec compatibility or retry the request. |
| `transcription-failed` | Speech-to-text recognition encountered an error. | Yes | Retry the job. If speech is in a specific language, pass the `language` code explicitly. |
| `render-failed` | The rendering engine could not composite the subtitle frames. | Yes | Transient rendering error; retry the job. |
| `timed-out` | Processing exceeded the maximum execution deadline. | Yes | Retry the job. If the problem persists, reduce video duration. |
| `internal` | An unexpected server error occurred. | Yes | Transient error; safe to retry. |
> **Billing guarantee**
>
> Failed and cancelled jobs never consume automation minutes. Reserved minutes are refunded to your balance immediately.
---
## Document: Direct file uploads
> Description: Upload video files directly to cloud storage when they are not hosted at a public URL.
> Path: /docs/api/uploads
When your video files are stored locally, behind a private firewall, or uploaded directly by your end-users, you can upload them to tscaps using presigned direct uploads rather than hosting them on a public web server.
Direct uploads use a secure three-step flow:
1. **Request an upload URL** from the tscaps API.
2. **PUT the binary file** directly into tscaps object storage using the presigned URL.
3. **Queue the job** by passing the returned `uploadId` instead of a `videoUrl`.
---
## Step 1: Request an upload URL
Call `POST /v1/automation-jobs/uploads` with the name of the file and its exact size in bytes.
```bash
curl -X POST https://api.tscaps.io/v1/automation-jobs/uploads \
-H "Authorization: Bearer $TSCAPS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"fileName": "reel.mp4",
"sizeBytes": 14205000
}'
```
```javascript
const response = await fetch("https://api.tscaps.io/v1/automation-jobs/uploads", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TSCAPS_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
fileName: "reel.mp4",
sizeBytes: 14205000,
}),
});
const upload = await response.json();
```
```python
import os
import requests
response = requests.post(
"https://api.tscaps.io/v1/automation-jobs/uploads",
headers={"Authorization": f"Bearer {os.environ['TSCAPS_API_KEY']}"},
json={
"fileName": "reel.mp4",
"sizeBytes": 14205000,
},
)
upload = response.json()
```
### Response
```json
{
"uploadId": "3b71d39a-7c22-48a3-92f1-47fae3223019",
"uploadUrl": "https://storage.tscaps.io/uploads/3b71d39a-...?signature=...",
"contentType": "video/mp4",
"sizeBytes": 14205000,
"expiresAt": "2026-09-12T10:26:38.000Z"
}
```
The response provides:
- **`uploadId`**: The reference ID used in Step 3 to create the job.
- **`uploadUrl`**: The signed target URL for your HTTP `PUT` request.
- **`contentType`**: The derived MIME type (e.g. `video/mp4`).
- **`sizeBytes`**: The expected payload byte size.
- **`expiresAt`**: Timestamp when the upload URL expires (valid for 15 minutes).
---
## Step 2: Upload the video file
Send an HTTP `PUT` request directly to `uploadUrl` with the raw file bytes.
> **Important upload rules**
>
> - `Content-Length` must match `sizeBytes` exactly. The byte length is cryptographically signed; a different size will be rejected by storage with HTTP 403.
> - `Content-Type` should match the returned `contentType` (for example `video/mp4`).
> - **Do not send your API key:** `uploadUrl` is a presigned URL that writes directly to object storage. Do not include an `Authorization` header, to avoid leaking your secret API key to third-party storage logs.
> - **Send raw binary bytes:** Send the raw video file directly in the request body. Do not wrap the file in `multipart/form-data` (FormData), because storage would save the multipart boundary markers into the file and corrupt the video container.
```bash
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: video/mp4" \
-H "Content-Length: 14205000" \
--data-binary @reel.mp4
```
```javascript
// Browser or Node.js (with File, Blob, or ReadStream)
await fetch(upload.uploadUrl, {
method: "PUT",
headers: {
"Content-Type": upload.contentType,
},
body: fileBlob,
});
```
```python
with open("reel.mp4", "rb") as file:
response = requests.put(
upload["uploadUrl"],
headers={"Content-Type": upload["contentType"]},
data=file,
)
response.raise_for_status()
```
A successful upload responds with HTTP `200 OK`.
---
## Step 3: Queue the job
Once the binary upload finishes, start the job by sending `uploadId` in place of `videoUrl`:
```bash
curl -X POST https://api.tscaps.io/v1/automation-jobs \
-H "Authorization: Bearer $TSCAPS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"uploadId": "3b71d39a-7c22-48a3-92f1-47fae3223019",
"templateId": "mira"
}'
```
```javascript
const response = await fetch("https://api.tscaps.io/v1/automation-jobs", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TSCAPS_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
uploadId: upload.uploadId,
templateId: "mira",
}),
});
const job = await response.json();
console.log(`Job queued with upload: ${job.id}`);
```
```python
response = requests.post(
"https://api.tscaps.io/v1/automation-jobs",
headers={"Authorization": f"Bearer {os.environ['TSCAPS_API_KEY']}"},
json={
"uploadId": upload["uploadId"],
"templateId": "mira",
},
)
job = response.json()
print(f"Job queued with upload: {job['id']}")
```
---
## Storage and lifecycle rules
- **Upload URL expiration:** Presigned upload URLs expire after 15 minutes. Start the PUT request promptly after minting the URL.
- **Unclaimed uploads:** Uploaded files that are not turned into an automation job within 24 hours are automatically swept from storage.
- **Upload limits:** Minting upload URLs follows your plan's daily limit (10 per day on Free, 100 per day on Pro) to prevent storage abuse.
---
## Document: Webhooks
> Description: Receive signed real-time notifications when Automation API jobs finish without polling.
> Path: /docs/api/webhooks
Webhooks allow your application to receive real-time HTTP callbacks when video processing finishes. Instead of repeatedly polling `GET /v1/automation-jobs/:id`, configure a `webhookUrl` and tscaps will send a `POST` request the moment the job settles.
Webhooks trigger once per job upon reaching any terminal state:
- `succeeded`: The video finished rendering and the download link is ready.
- `failed`: Processing encountered an error (payload includes `failureCode` and `failureReason`).
- `cancelled`: The job was cancelled.
---
## Configuring webhooks
Provide a `webhookUrl` property when calling `POST /v1/automation-jobs`:
```json
{
"videoUrl": "https://example.com/reel.mp4",
"templateId": "mira",
"webhookUrl": "https://api.yourdomain.com/webhooks/tscaps"
}
```
The URL must be publicly accessible and use HTTPS. Webhook endpoints pointing to `localhost`, internal private IPs, or non-HTTPS URLs are rejected at job creation.
---
## Delivery headers
Every webhook request sent by tscaps includes the following HTTP headers defined by the [Standard Webhooks](https://www.standardwebhooks.com) specification:
| Header | Description |
| --- | --- |
| `webhook-id` | Unique delivery identifier for this event notification. |
| `webhook-timestamp` | Unix epoch timestamp (in seconds) when the delivery was dispatched. |
| `webhook-signature` | Cryptographic HMAC-SHA256 signature used to verify that the message originated from tscaps. |
---
## Verifying webhook signatures
Always verify webhook signatures before processing event payloads. Verifying ensures:
1. **Authenticity:** The request genuinely originated from tscaps and was signed with your account's secret.
2. **Integrity:** The payload was not tampered with in transit.
3. **Replay prevention:** Old intercepted requests cannot be replayed against your server.
Retrieve your account's webhook signing secret from your [Webhooks dashboard](/app/developers/webhooks).
> **Important:** Always verify the **raw request body**. Do not verify re-serialized JSON objects, as whitespace and key ordering differences will cause signature verification to fail.
```javascript
import express from "express";
import { Webhook } from "standardwebhooks";
const app = express();
const signingSecret = process.env.TSCAPS_WEBHOOK_SECRET;
// Preserve the raw body buffer for verification
app.post("/tscaps-webhook", express.raw({ type: "*/*" }), (req, res) => {
const webhook = new Webhook(signingSecret);
let event;
try {
// Verifies raw body buffer and delivery headers
event = webhook.verify(req.body, req.headers);
} catch (err) {
console.error("Webhook signature verification failed:", err.message);
return res.status(400).send("Invalid signature");
}
// Handle job outcome
if (event.status === "succeeded") {
console.log(`Video rendered: ${event.downloadUrl}`);
} else if (event.status === "failed") {
console.error(`Job failed: ${event.failureCode} - ${event.failureReason}`);
}
// Acknowledge receipt
res.sendStatus(200);
});
```
```python
import os
from fastapi import FastAPI, Header, HTTPException, Request
from standardwebhooks import Webhook
app = FastAPI()
signing_secret = os.environ["TSCAPS_WEBHOOK_SECRET"]
@app.post("/tscaps-webhook")
async def receive_webhook(request: Request):
# Read the raw unparsed body
raw_body = await request.body()
headers = dict(request.headers)
webhook = Webhook(signing_secret)
try:
event = webhook.verify(raw_body, headers)
except Exception as err:
raise HTTPException(status_code=400, detail="Invalid signature")
if event.get("status") == "succeeded":
print(f"Video rendered: {event.get('downloadUrl')}")
elif event.get("status") == "failed":
print(f"Job failed: {event.get('failureCode')} - {event.get('failureReason')}")
return {"received": True}
```
---
## Event payload
The webhook payload carries the exact same structure as the `GET /v1/automation-jobs/:id` status endpoint, allowing a single parser to handle both polling and webhook responses.
```json
{
"id": "9f2c48e1-5b7a-4a29-8874-9dfc8227b610",
"status": "succeeded",
"phase": "succeeded",
"templateId": "mira",
"quotedSeconds": 74,
"format": "mp4",
"quality": "high",
"resolution": { "width": 1080, "height": 1920 },
"percent": null,
"downloadUrl": "https://storage.tscaps.io/outputs/9f2c48e1-...?token=...",
"outputExpiresAt": "2026-09-19T10:14:00.000Z",
"failureReason": null,
"failureCode": null,
"settledAt": "2026-09-12T10:12:52.000Z",
"createdAt": "2026-09-12T10:11:38.000Z"
}
```
---
## Delivery and retry rules
- **Return a 2xx status:** Your server must respond with HTTP `200` (or any 2xx status code) within 10 seconds. Non-2xx responses and timeouts are treated as delivery failures.
- **Automatic retries:** If your endpoint fails or times out, tscaps retries delivery several times using exponential backoff over a 24-hour period.
- **Idempotency:** Because retried messages may deliver a payload you have already received, ensure your receiver is idempotent. Track processed job `id` values in your database to avoid duplicating post-processing actions.
- **No redirects:** Webhook requests do not follow HTTP redirects (301/302). Deliveries are made directly to the exact URL specified in `webhookUrl`.
---
## Document: Limits & Quotas
> Description: Compare Automation API duration, resolution, frame budgets, concurrency, and minute billing across plans.
> Path: /docs/api/limits
Your subscription tier determines the resource limits applied to your Automation API requests.
## Plan comparison
| Limit | Free | Starter | Pro |
| --- | ---: | ---: | ---: |
| Maximum video duration | 3 minutes | 5 minutes | 10 minutes |
| Maximum output resolution | 720p HD | 1080p Full HD | 1080p Full HD |
| Maximum frame budget | 921,600 pixels | 2,073,600 pixels | 2,073,600 pixels |
| Concurrent jobs | 1 | 2 | 3 |
| Daily job starts | 10 jobs / day | 100 jobs / day | 500 jobs / day |
| Monthly included minutes | 5 minutes | 30 minutes | 60 minutes |
Upgrading your plan immediately raises all duration, resolution, concurrency, and daily ceilings for your account.
---
## Video duration
Every video submitted to the API is measured after it is fetched or uploaded. A video exceeding your plan's maximum duration is refused and the job fails with `source-too-long`:
- **Free**: Up to 3 minutes (180 seconds).
- **Starter**: Up to 5 minutes (300 seconds).
- **Pro**: Up to 10 minutes (600 seconds).
No minutes are charged for a video rejected for duration. If your workflow requires longer durations, contact support to discuss custom enterprise limits.
---
## Output resolution & frame budget
Rather than restricting width or height independently, resolution limits are defined as a **total pixel budget per frame**. This allows you to render videos in any aspect ratio without artificial letterboxing or cropping:
| Frame dimensions | Aspect ratio | Total pixels | Free (720p) | Starter & Pro (1080p) |
| --- | --- | ---: | :---: | :---: |
| 1080 × 1920 | 9:16 Vertical (Reels / Shorts / TikTok) | 2,073,600 | ✗ | ✓ |
| 720 × 1280 | 9:16 Vertical | 921,600 | ✓ | ✓ |
| 1920 × 1080 | 16:9 Landscape | 2,073,600 | ✗ | ✓ |
| 1280 × 720 | 16:9 Landscape | 921,600 | ✓ | ✓ |
| 1080 × 1350 | 4:5 Feed Portrait | 1,458,000 | ✗ | ✓ |
| 720 × 900 | 4:5 Feed Portrait | 648,000 | ✓ | ✓ |
| 1080 × 1080 | 1:1 Square | 1,166,400 | ✗ | ✓ |
| 960 × 960 | 1:1 Square | 921,600 | ✓ | ✓ |
### Scaling rules
- **Automatic downscaling:** If you leave `resolution` omitted and the source video exceeds your plan's frame budget (for example, submitting a 4K video on a paid plan), tscaps scales the video down to fit within your budget (1920 × 1080) while preserving the original aspect ratio.
- **No upscaling:** Asking for a `resolution` larger than your input video will fail the job with `output-larger-than-source`. Tscaps does not artificially upscale lower-resolution videos.
---
## Concurrency and queueing
- **Free plans** can run **1 job at a time**.
- **Starter plans** can run up to **2 jobs concurrently**.
- **Pro plans** can run up to **3 jobs concurrently**.
If you submit new jobs while all concurrent slots are busy, the extra jobs remain in the `queued` status and begin processing automatically as earlier jobs finish.
---
## Automation minutes and billing
API jobs consume **automation minutes** based on the duration of the input video:
- **1 minute of input video consumes 1 automation minute.** A 45-second video consumes 45 seconds of automation minutes.
- **Quality does not change the cost.** Selecting `low`, `medium`, or `high` quality adjusts encoding bitrate but does not change the minutes charged.
- **Zero-cost failure guarantee:** Minutes are only charged for jobs that successfully deliver a finished video. If a job fails or is cancelled, all held minutes are immediately refunded to your balance.
- **Rollover packs:** Additional minute packs purchased via the dashboard never expire and are consumed only after your monthly plan minutes are exhausted.
You can [check your account limits](/app/developers) and [manage your minute balance](/app/developers/minutes) in the developer dashboard.
---