# 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. ---