# CaptionCraft API documentation ## Overview Captions that make every word count. One API, ready for your workflow. ## Your video. Captioned. CaptionCraft turns a video URL into a captioned MP4, an SRT subtitle file, and a JSON transcript with word-level timestamps. Add captions to your application, automation, or AI agent with a single request.
CAPTIONCRAFT · HIGHLIGHT
Make every word count.
Captioned MP4SRT subtitlesJSON transcript
API requests require a CaptionCraft API key and sufficient prepaid video seconds. ## How it works Host your video at a direct, publicly reachable HTTPS URL. MP4, MOV, and WebM are supported up to 500 MiB and 1080p in landscape or portrait. The video must contain speech and an audio track. Call [Create subtitles](/api-reference/create-subtitles) with your video URL and chosen preset. The API returns a job ID and a status URL immediately. Poll [Get job](/api-reference/get-job) every five seconds while your video is downloaded, transcribed, and rendered. When the status is `completed`, download the video, subtitles, and transcript. Results are retained for 24 hours; copy them to your own storage before they expire. ## Start building Submit your first video and retrieve the results. Explore request fields and response examples. Choose Classic, Highlight, or Karaoke. Connect with a scoped Bearer API key. ## Build with AI The **OpenAPI** button in the header opens the machine-readable JSON specification: endpoints, authentication, request fields, and response schemas. Download it to import the API into compatible tools. Use the [OpenAPI specification](/openapi.json) to describe the API to a connector or agent. The [AI integration guide](/ai-integrations) covers job polling, retries, and downloadable documentation. ## Quickstart Go from a video URL to a captioned video in three steps. You need a [CaptionCraft API key](/authentication), prepaid seconds, and a [supported video URL](/media-requirements). The shell examples use `curl` and `jq`. Use the endpoint provided with your API key. ```bash export CAPTIONCRAFT_API_URL="https://api.captioncraft.studio" # Set CAPTIONCRAFT_API_KEY securely in your environment. ``` Set `CAPTIONCRAFT_API_KEY` through your secret manager or shell environment. Replace the sample URL below with your own direct video URL: ```bash export VIDEO_URL="https://your-cdn.example/video.mp4" ``` Videos can be up to 600 seconds long. After inspecting the video, the API reserves its actual duration rounded up to a whole second. Your available balance must cover that duration before transcription can start. ```bash cURL jq -n --arg video_url "$VIDEO_URL" '{ video_url: $video_url, preset: "highlight", style: { highlight_color: "#7651E8" } }' > request.json curl --fail-with-body --silent --show-error \ "$CAPTIONCRAFT_API_URL/v1/subtitles" \ -H "Authorization: Bearer $CAPTIONCRAFT_API_KEY" \ -H "Content-Type: application/json" \ --data-binary @request.json > submission.json cat submission.json ``` ```javascript JavaScript // Node.js 20+. Set VIDEO_URL and API environment variables. const response = await fetch(`${process.env.CAPTIONCRAFT_API_URL}/v1/subtitles`, { method: "POST", headers: { Authorization: `Bearer ${process.env.CAPTIONCRAFT_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ video_url: process.env.VIDEO_URL, preset: "highlight", style: { highlight_color: "#7651E8" }, }), }); const job = await response.json(); if (!response.ok) throw new Error(JSON.stringify(job.error)); console.log(job); ``` ```python Python # Python 3; uses only the standard library. import json import os import urllib.request request = urllib.request.Request( f"{os.environ['CAPTIONCRAFT_API_URL']}/v1/subtitles", data=json.dumps({ "video_url": os.environ["VIDEO_URL"], "preset": "highlight", "style": {"highlight_color": "#7651E8"}, }).encode(), headers={ "Authorization": f"Bearer {os.environ['CAPTIONCRAFT_API_KEY']}", "Content-Type": "application/json", }, method="POST", ) with urllib.request.urlopen(request) as response: job = json.load(response) print(job) ``` A successful submission returns **202 Accepted**: ```json { "id": "", "status": "ingesting", "status_url": "/v1/jobs/" } ``` This confirms the job was accepted. It does not mean the video is ready. Each accepted submission creates a new job, even with identical input. Using the cURL submission above, poll until the job reaches a terminal state: ```bash STATUS_URL=$(jq -er '.status_url' submission.json) while true; do sleep 5 curl --fail-with-body --silent --show-error "$STATUS_URL" \ -H "Authorization: Bearer $CAPTIONCRAFT_API_KEY" > job.json || break STATUS=$(jq -r '.status' job.json) case "$STATUS" in completed) curl --fail --output captioned.mp4 "$(jq -er '.video.url' job.json)" curl --fail --output subtitles.srt "$(jq -er '.subtitles.srt_url' job.json)" curl --fail --output transcript.json "$(jq -er '.subtitles.transcript_url' job.json)" break ;; failed|canceled) cat job.json break ;; *) printf '%s\n' "Job status: $STATUS" ;; esac done ``` Download URLs do not need the API key. Save all three files within 24 hours of completion. If a poll returns `429`, wait at least the `Retry-After` interval and retry that poll. ## Next steps Explore presets, positioning, and colors. Add progress, cancellation, and retry handling. ## Authentication Use a CaptionCraft API key to authenticate server-side requests. ## Your API key Authenticate requests with your CaptionCraft API key. Keys begin with `cc_`. Store the key in a secret manager or a server environment variable named `CAPTIONCRAFT_API_KEY`. ## Send a Bearer token ```http Authorization: Bearer ``` Every endpoint except [List presets](/api-reference/list-presets), `/health`, and `/openapi.json` requires authentication. Use the endpoint provided with your API key. ```bash export CAPTIONCRAFT_API_URL="https://api.captioncraft.studio" # Set CAPTIONCRAFT_API_KEY securely in your environment. ``` ```bash curl --fail-with-body "$CAPTIONCRAFT_API_URL/v1/usage" \ -H "Authorization: Bearer $CAPTIONCRAFT_API_KEY" ``` ## Key scopes | Scope | Operations | | --- | --- | | `subtitles:write` | Create and cancel jobs | | `jobs:read` | Read job status and result URLs | | `usage:read` | Read prepaid balances and concurrency | Keys are associated with an account. Job access is scoped to that account. A key may be restricted by scope, expire, or be revoked. Keep keys on your server. Do not put them in frontend code, public repositories, URLs, or a `NEXT_PUBLIC_` variable. Media download URLs also grant access to the files; treat them as private. ## Authentication errors - `401 UNAUTHORIZED`: the key is missing, malformed, expired, or revoked. - `403 FORBIDDEN`: the key lacks the required scope or the account is disabled. See [Errors](/errors) for the response format and retry guidance. ## Create subtitles Create a captioned video with automatic duration inspection. Requires `subtitles:write`. Submit a direct HTTPS video URL and poll the returned `status_url` every five seconds. Each accepted submission creates a new job, even with identical input. Choose any of the 16 presets and optionally override typography, layout, background, outline, shadow, and applicable highlight colors through `style`. Omitted fields inherit the preset, including fields within nested objects. [Caption styles](/caption-styles) explains all controls and preset-specific behavior. Creating a job requires a positive prepaid balance. After inspecting the video, the API reserves its actual duration rounded up before transcription. If credit is insufficient, the job fails with `INSUFFICIENT_CREDITS`. Successful jobs consume the reservation; failed and canceled jobs release it. Videos can be up to 600 seconds long. See [Quickstart](/quickstart) for a full submission and download example, and [Media requirements](/media-requirements) for accepted sources. ## Get job Retrieve progress, usage, and links to completed output files. Requires `jobs:read`. Only jobs belonging to your account are visible. Poll every five seconds until `completed`, `failed`, or `canceled`. Results include a captioned MP4, SRT subtitles, and a JSON transcript. Download them before `expires_at`. URLs are `null` before completion or after file expiration. An HTTP `200` response may contain a failed job. Check the `status` and `error` fields. ## Cancel job Cancel an unfinished job and release its reserved credits. Requires `subtitles:write`. No request body is required. Queued and processing jobs become `canceled`. Jobs already completed, failed, or canceled keep their existing state. Cancellation does not refund a completed job. ## List presets Discover the available caption presets. This endpoint is public and does not require an API key. Use a returned `id` as the `preset` in a create request. All 16 caption presets are available. Each entry returns its `defaults`, `supported_style_fields`, and `style_notes` so clients can discover which controls affect that preset. Nested defaults use the same shape as the request's `style`. `animation` describes the preset's fixed animation and is read-only. See [Caption styles](/caption-styles) for style overrides and language settings. ## Get usage Check prepaid balances and available job capacity. Requires `usage:read`. All values are scoped to your account and shared across its API keys. Read [Usage and credits](/usage-and-credits) for reservation and billing examples, or [Rate limits](/rate-limits) for request and concurrency limits. ## Usage and credits Prepaid seconds, temporary reservations, and completion-based billing. ## How credits work CaptionCraft accounts hold prepaid video seconds. A successful job is billed for its actual video duration, rounded up to the next whole second. ## Buy credits Organization admins can open **Settings → Billing** in the API console and buy a custom amount of credits through Stripe Managed Payments. Credits are priced in USD, with a **$20 minimum** and a $10,000 limit per checkout. Rendering costs **$0.06 per minute**, equivalent to 10 video seconds per cent. A $20 purchase adds **20,000 video seconds** (333 minutes and 20 seconds). Applicable tax is added at checkout and does not add credits. Stripe may offer payment in your local currency; the credit quantity stays the same. New credits expire **12 calendar months after they are granted**. Existing credits keep their original no-expiry terms. The credit history shows each grant’s expiration date in UTC. This is a one-time purchase, with no subscription or automatic recharge. Credits belong to the account selected when checkout starts. Your balance updates after Stripe confirms payment, even if you close the checkout tab. Delayed payment methods remain pending until payment succeeds. Purchase history shows payment status and a Stripe receipt when available. Test payments are labeled in the console. Credits with the earliest expiration are reserved first, before existing credits without expiry. Buying more credits does not extend earlier grants. Expired credits cannot fund new jobs. A job may finish using credits reserved before expiration; released credits return to the available balance only if they have not expired. ## How rendering uses credits Creating a job requires a positive available balance. No credits are reserved until the video duration is known. Videos can be up to 600 seconds long. Before transcription starts, the API atomically reserves `ceil(actual duration)` from your available balance. If there are not enough seconds, the job fails with `INSUFFICIENT_CREDITS` without a charge. Concurrent jobs share the same balance and cannot reserve the same credits. On success, the remaining reservation becomes consumed credit. Failure or cancellation releases the reservation without billing. ## Example For a **23.4-second** video: | Event | Available seconds | Reserved seconds | Consumed seconds | | --- | --- | --- | --- | | Starting balance | 120 | 0 | 0 | | Submission | 120 | 0 | 0 | | Duration inspected | 96 | 24 | 0 | | Completed | 96 | 0 | 24 | If the job fails before the credits expire, all 24 reserved seconds return to the available balance. Submitting the same input again creates another job, which is billed separately if it succeeds. ## Read your balance [Get usage](/api-reference/get-usage) returns: - `available_seconds`: seconds available for new reservations. - `reserved_seconds`: seconds held by active jobs. - `consumed_seconds`: seconds billed for completed jobs. - `active_jobs`: the account's queued and processing jobs. - `concurrency_limit`: the maximum number of active jobs allowed for the account. A submission returns `402 INSUFFICIENT_CREDITS` if no prepaid seconds are available. An accepted job can also fail with `INSUFFICIENT_CREDITS` after inspection if the available balance cannot cover the video's rounded-up duration. Add credits in **Settings → Billing** and submit a new job. ## Caption styles All 16 caption presets, with typography, layout, background, outline and shadow controls. ## Choose a preset | Preset | Appearance | | --- | --- | | `classic` | White Inter captions with a dark outline. The default preset. | | `highlight` | Uppercase Rubik captions highlighting the active word. | | `smash` | Outlined Paytone One captions that pop in one word at a time. | | `box` | Inter captions with a purple box behind the active word. | | `push` | Uppercase Rubik captions with a green highlight and scale animation. | | `elegant` | Uppercase Inter captions that float into place with a soft shadow. | | `ogre` | Green Caveat Brush captions popping into the center of the video. | | `outline` | Warm Rubik captions with a thick orange outline. | | `paper-ink` | Black Rubik captions on rounded white lines at the top. | | `action` | Italic Bebas Neue captions with an outline, shadow and active-word color. | | `prompter` | Centered Zilla Slab captions with a white outline and active-word opacity. | | `storytelling` | Gold Instrument Serif captions with a red outline and offset shadow. | | `karaoke` | Rounded Coiny lettering with a progressive purple fill. | | `glow` | Rubik captions with a pink glow and fade-in animation. | | `thin&bold` | Uppercase Inter captions with animated thin and bold weights. | | `background` | Urbanist captions on a rounded black background. | Fetch the catalog from [List presets](/api-reference/list-presets), which is public and requires no API key. Each preset includes `defaults`, `supported_style_fields`, `style_notes`, and its read-only `animation` name. ## Customize the style All style fields are optional. Omitted fields inherit the preset, including individual fields inside `background`, `outline`, `shadow`, and `offset`. Sending `shadow: { "color": "#FF0000" }` changes only the color; it does not enable a disabled shadow. Use `enabled: true` to enable it. The API starts from the selected preset's full default style, adapts it to the video's dimensions, and applies only the properties supplied in `style`. Omitting `style`, sending `{}`, or sending empty nested objects produces the same preset defaults. For example, `"preset": "glow"` with `"style": { "text_color": "#FFE600" }` changes only the text color and retains Glow's font, shadow, layout and animation. ```json { "video_url": "https://your-cdn.example/video.mp4", "preset": "box", "style": { "position": "bottom", "font_family": "Rubik", "text_color": "#FFFFFF", "highlight_color": "#7651E8", "line_count": 2, "scale": 0.7, "background": { "type": "box-rounded", "color": "#111111", "opacity": 0.65 }, "outline": { "color": "#000000", "thickness": 2 }, "shadow": { "enabled": true, "color": "#000000", "blur": 8, "distance": 0 } } } ``` | Field | Accepted values | Behavior | | --- | --- | --- | | `position` | `top`, `center`, `bottom` | Vertical placement. | | `text_alignment` | `left`, `center`, `right` | Alignment of lines within that block. | | `text_color` | Six-digit hex color | Text fill. | | `highlight_color` | Six-digit hex color | Preset-specific active-word effect; see below. | | `font_family` | Bundled font name below | Font family. | | `font_weight` | `400`, `700` | Regular or bold. Thin&Bold animates its own weights. | | `font_style` | `normal`, `italic` | Font style; browser synthesis is used when the font has no italic face. | | `text_case` | `none`, `uppercase`, `lowercase`, `capitalize` | Visual casing. `capitalize` uppercases each word's first character. Downloads preserve the source words. | | `text_width` | `small`, `medium`, `large` | Text density: small uses larger type and fewer words per line. Font size is derived from this value and the font family. | | `line_count` | `0`, `1`, `2`, `3`, `100` | Maximum lines per cue. `0` means one word at a time. `100` allows up to 100 lines; use cautiously on small videos. | | `scale` | Number, 0.2–1 | Base caption scale, multiplied by video width / 1080. Defaults to the preset's scale for portrait/square and 0.55 for landscape. | | `offset` | Object with optional `left`, `top`, `right`, `bottom`, each −1920–1920 | Pixel offsets in output-video coordinates. Horizontal movement is left minus right; vertical movement is top minus bottom. | | `background` | Object with optional `type`, `color`, `opacity` | Type: `none`, `line`, `line-rounded`, `box`, `box-rounded`. Color: hex. Opacity: 0–1. Set a type to enable. | | `outline` | Object with optional `color`, `thickness` | Color: hex. Thickness: 0–20 caption-canvas pixels; 0 disables the outline. | | `shadow` | Object with optional `enabled`, `color`, `blur`, `distance`, `angle` | Enabled: boolean. Color: hex. Blur: 0–40. Distance: 0–20. Angle: −180–180 degrees. | Bundled fonts: `Inter`, `Urbanist`, `Rubik`, `Bangers`, `Paytone One`, `Slackey`, `Permanent Marker`, `Caveat Brush`, `Zilla Slab`, `Young Serif`, `Instrument Serif`, `Bebas Neue`, and `Coiny`. Fonts without a native bold face use browser synthesis. Unknown fields (including nested ones), shorthand colors, alpha hex values, named colors, and out-of-range numbers return `400 INVALID_REQUEST`. Background transparency uses `background.opacity`. ## Properties that depend on the preset The `style` object is shared across presets. Discover applicable fields through each preset's `supported_style_fields`, and read `style_notes` for special behavior. This lets clients display the appropriate controls without hardcoding preset rules or accepting arbitrary renderer properties. | Preset | Special behavior | | --- | --- | | `highlight`, `push`, `action` | `highlight_color` changes the active word's text. | | `box` | `highlight_color` changes the animated box behind the active word. `background` separately controls the background behind the whole caption or its lines. | | `karaoke` | `highlight_color` changes the progressive fill. Text and outline default to the preset's white; set `text_color` explicitly to change the unspoken text. | | `smash` | Defaults to `line_count: 0`. Set 1, 2, or 3 to group words into lines. | | `thin&bold` | The animation controls weights 200 and 700; `font_weight` has no effect. | | `prompter` | Highlights with opacity, so `highlight_color` has no effect. | For compatibility, `highlight_color` remains accepted for every preset; it has no visual effect outside the five color-highlight presets. Other common controls remain available even when their effect is disabled by default. For example, any preset can enable a background or a shadow. The preset selects its animation. Arbitrary animation settings, custom font URLs, derived shadow coordinates, and watermark controls are not accepted. API renders have no watermark. ## Sizing and defaults Each preset defines its font, text-width, outline, shadow, background, scale and animation defaults. Portrait and square videos use the preset's text width and base scale. Landscape videos initialize with `text_width: "large"` and base scale `0.55`. Explicit `text_width` and `scale` overrides take precedence over those orientation defaults. The final scale is the base scale multiplied by video width / 1080, including for videos wider than 1080px. Font size is always derived from the resolved `font_family` and `text_width`. Use `text_width` and `scale` to adjust caption sizing. Horizontal caption placement follows the preset; `text_alignment` controls only the alignment of lines within that block. Direct font-size and horizontal-placement overrides (`style.font_size` and `style.horizontal_position`, corresponding to renderer `fontSize` and `alignX`) are not supported and return `400 INVALID_REQUEST`. Renderer-only properties such as `fontSizeOverride` and `alignX` are also rejected. Catalog defaults describe supported controls before landscape adaptation or resolution scaling. Top and bottom placement add an inset to the preset's offset on the selected edge: 20% of video height for portrait/square and 14% for landscape. An explicit `offset.top` or `offset.bottom` replaces that edge's complete resolved offset, including an explicit zero. Catalog defaults omit those two dimensions because they depend on the video. For shadows, angle 0 points down and angle 90 points right. Shadow position is derived automatically when distance or angle changes. ## Set the spoken language Omit `language` for automatic detection, or pass a lowercase two- or three-letter language code, such as `en` or `fra`, supported by the transcription service. The API transcribes speech in the source language; it does not translate. ## Job lifecycle Track progress, handle retries, and know when your files are ready. ## Job states A job normally moves through these states: ```text ingesting → transcribing → queued → rendering → completed ``` | State | Meaning | | --- | --- | | `ingesting` | Downloading and inspecting the source video. | | `transcribing` | Transcribing speech and preparing timed captions. | | `queued` | Captions are ready; waiting for renderer capacity. | | `rendering` | Rendering the prepared captions and uploading the video. | | `completed` | Processing succeeded. Download the result files. | | `failed` | Processing stopped with an error. Reserved credits are released. | | `canceled` | The job was canceled. Reserved credits are released. | `completed`, `failed`, and `canceled` are terminal states. Stop polling when you receive one of them. The `progress` field is a percentage from 0 to 100, not an estimate of remaining time. ## Poll for results Call [Get job](/api-reference/get-job) every five seconds. The response includes `Retry-After: 5`. If a request returns `429`, honor `Retry-After` before trying again. Successful HTTP status does not imply successful processing: a `200` job response may contain `status: "failed"`. Inspect both the HTTP status and the job's `status`. This release uses polling. Webhook callbacks are not currently supported. ## Submit another job Each accepted `POST /v1/subtitles` request creates a new job, even when its input is identical to an earlier request. Each successful job is billed separately. Save the returned job ID and poll its status instead of submitting again. If a submission times out or its response is lost, check your recent jobs in the console before retrying: the original job may already exist. To retry a failed or canceled job, fix the underlying issue and submit another request. ## Cancel a job Call [Cancel job](/api-reference/cancel-job) for a queued or processing job. It changes to `canceled` and releases the reservation. If processing has already reached a terminal state, cancellation returns that existing state. Completed jobs remain billed. ## Retention and timeouts Result files expire 24 hours after completion. Check `expires_at` and save all outputs to your own storage. Before expiry, fetch the job again to obtain current download URLs. After expiry the job can remain `completed`, but file URLs are `null`; fetching it again does not restore expired files. Source ingestion, transcription and caption preparation have a ten-minute deadline. Prepared jobs waiting for renderer capacity time out after 30 minutes. Assigned renders have a one-hour ceiling. A processing failure or timeout releases the job's reserved credit; it does not automatically submit a replacement. ## Media requirements Prepare a source video the API can download and caption. ## Video limits | Property | Limit | | --- | --- | | Source formats | MP4, MOV, WebM | | Maximum file size | 500 MiB (524,288,000 bytes) | | Maximum duration | 600 seconds | | Maximum dimensions | Long edge ≤ 1920 pixels and short edge ≤ 1080 pixels | | Audio | An audio track containing speech is required | | Output | MP4 video, SRT subtitles, JSON transcript | Both 1920 × 1080 landscape and 1080 × 1920 portrait videos are supported. A square video must fit within 1080 × 1080. Odd pixel dimensions may be rounded down by one pixel during encoding. The API inspects the video automatically. Videos longer than 600 seconds fail validation; the API does not trim them. Credits are reserved for the actual duration rounded up to a whole second before transcription starts. ## Direct HTTPS URLs The API downloads `video_url` from the processing server. The URL must: - Use HTTPS on the standard port, with a public hostname. - Return the video directly with HTTP `200` and no redirect. - Be reachable without cookies or additional authorization headers. - Contain no embedded username/password or URL fragment. - Remain valid while the job is waiting and downloading. Signed object-storage URLs are supported when they meet these requirements. Account for queue time when choosing their expiration. URLs for local networks, localhost, private addresses, and custom ports are rejected. A YouTube page, a cloud-drive sharing page, or a login page is not a direct media URL. ## Uploads There is no public upload endpoint in this release. Upload your media to your own object storage, then provide its direct HTTPS URL. ## Transcript format The downloaded JSON transcript contains the full text and individual words: ```json { "text": "Make every word count.", "timestamp_unit": "seconds", "words": [ { "text": "Make", "start": 0.12, "end": 0.4 }, { "text": "every", "start": 0.42, "end": 0.75 }, { "text": "word", "start": 0.78, "end": 1.08 }, { "text": "count.", "start": 1.1, "end": 1.5 } ] } ``` Times are measured in **seconds**, not milliseconds, from the start of the source video. The example timestamps above are illustrative. ## Rate limits Keep polling predictable and stay within your account's capacity. ## Request rate Authenticated API operations share an account-level token bucket: | Setting | Value | | --- | --- | | Refill rate | 120 requests per minute | | Burst capacity | 30 requests | | Recommended polling interval | 5 seconds per job | All API keys belonging to the account share the same allowance. This is a refill rate, not permission to send 120 simultaneous requests. The public presets endpoint does not use the authenticated request bucket. ## Concurrent jobs Each account has a separate active-job limit. Read `concurrency_limit` and `active_jobs` from [Get usage](/api-reference/get-usage). Queued jobs count toward this limit. A create request may return `429` with either: - `RATE_LIMITED`: too many API requests. - `CONCURRENCY_LIMIT`: too many active jobs on the account. ## Back off and retry Both return a `Retry-After` header, currently `5` seconds. Wait at least that long before retrying. For a concurrency limit, also wait for an active job to complete, fail, or be canceled. Use bounded retries with increasing delays and jitter for polling and requests rejected with `429`. Each accepted create request starts a new job. If a submission times out, loses its response, or returns `500`, check your recent jobs in the console before retrying to avoid creating and paying for duplicate jobs. ## Errors Distinguish request failures from processing failures. ## Request errors An unsuccessful API request returns an appropriate HTTP status and a JSON error object: ```json { "error": { "code": "INSUFFICIENT_CREDITS", "message": "No prepaid seconds available. Add credits before submitting a video.", "request_id": "" } } ``` Record `error.request_id` or the `X-Request-Id` response header when reporting a problem. Do not include your API key in logs or support messages. | HTTP | Code | Action | | --- | --- | --- | | 400 | `INVALID_REQUEST` | Correct invalid or unknown JSON fields. | | 400 | `INVALID_JSON` | Send valid JSON. | | 401 | `UNAUTHORIZED` | Check the Bearer key and whether it expired or was revoked. | | 402 | `INSUFFICIENT_CREDITS` | Add prepaid credit before submitting a video. | | 403 | `FORBIDDEN` | Check key scopes and account access. | | 404 | `NOT_FOUND` | Check the endpoint, job ID, and account that owns the job. | | 413 | `PAYLOAD_TOO_LARGE` | Keep the request's JSON body within 16 KiB. | | 415 | `INVALID_CONTENT_TYPE` | Set `Content-Type: application/json`. | | 429 | `RATE_LIMITED` | Wait for `Retry-After` before retrying. | | 429 | `CONCURRENCY_LIMIT` | Wait for capacity on the account. | | 500 | `INTERNAL_ERROR` | Retry polling with backoff. For submissions, check recent jobs before retrying. | | 503 | `SERVICE_UNAVAILABLE` | Processing is not configured; contact the operator. | ## Processing errors A job may be accepted and later fail. [Get job](/api-reference/get-job) still returns HTTP `200`; inspect `status` and `error`: ```json { "status": "failed", "error": { "code": "UNSUPPORTED_VIDEO", "message": "Unsupported video, missing audio, or exceeded duration/resolution limits." }, "usage": { "reserved_seconds": 0, "billed_seconds": 0 } } ``` This is an excerpt of a job response. Common processing codes include: | Code | Meaning | | --- | --- | | `INVALID_SOURCE` | The source could not be downloaded safely. Check URL expiry, redirects, permissions, and file size. | | `UNSUPPORTED_VIDEO` | The video is unsupported, lacks audio, or exceeds the duration or resolution limit. | | `INSUFFICIENT_CREDITS` | The available balance could not cover the inspected duration rounded up. Add credits and submit a new job. | | `NO_SPEECH` | No speech was detected. | | `TRANSCRIPTION_FAILED` | Transcription did not complete. | | `RENDER_FAILED` | Rendering or output preparation did not complete. | | `QUEUE_TIMEOUT` | Processing capacity was not available before the queue deadline. | | `WORKER_TIMEOUT` | Processing timed out. | | `ACCOUNT_DISABLED` | Account access was disabled before processing. | Failed jobs release reserved credits. Fix the underlying issue, then submit a new request if you want another attempt. ## AI integrations Connect CaptionCraft to tools, workflows, and agents. ## Machine-readable documentation Download the public endpoint contract. Find the guides and endpoint references. Read the [full documentation text](/llms-full.txt) for tools that ingest a single document. Use **Copy page** on an individual page for Markdown content. ## Model a long-running tool A caption job is asynchronous. Expose at least two operations in your connector: 1. **Create captions**: call `POST /v1/subtitles` with the video URL and caption options, and return the job ID. 2. **Get result**: call `GET /v1/jobs/{id}` to return status, progress, and completed output URLs. You can also expose cancellation, preset discovery, and usage checks. Keep the API key in the connector's server-side secret store. ## Integration rules - Check the prepaid balance and the video's expected duration before submission. - Require a direct HTTPS media URL; an agent cannot pass a local file path. - Save the job ID as soon as submission succeeds. - Poll no more often than every five seconds per job and honor `Retry-After`. - Stop on `completed`, `failed`, or `canceled`. - Treat processing errors as tool results the caller can act on. - Save output files before their 24-hour expiration. - Each accepted submission creates a new job. Check recent jobs before retrying a submission whose response was lost. ## Platform connectors The REST API and OpenAPI specification can be used as the basis for a platform connector. Platform-specific manifests, authorization requirements, and submission approval still need to be implemented and verified for the target platform. This documentation does not imply a published or approved Muse connector. ## OpenAPI specification ```json { "openapi": "3.1.0", "info": { "title": "CaptionCraft API", "version": "1.0.0", "description": "Create captioned MP4 videos from a video URL. Jobs are asynchronous. Poll every 5 seconds. Media is retained for 24 hours; request a fresh job response to retrieve result URLs. All word timestamps use seconds." }, "servers": [ { "url": "https://api.captioncraft.studio" } ], "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer", "bearerFormat": "cc_ API key" } }, "schemas": { "SubtitleRequest": { "type": "object", "properties": { "video_url": { "type": "string", "maxLength": 4096, "format": "uri", "description": "Direct HTTPS MP4, MOV, or WebM URL. No redirects or extra auth headers. Maximum 500 MiB; must remain valid until downloaded.", "examples": [ "https://your-cdn.example/video.mp4" ] }, "preset": { "default": "classic", "type": "string", "enum": [ "classic", "highlight", "smash", "box", "push", "elegant", "ogre", "outline", "paper-ink", "action", "prompter", "storytelling", "karaoke", "glow", "thin&bold", "background" ], "description": "Caption preset. Omitted style properties use this preset's defaults." }, "language": { "type": "string", "pattern": "^[a-z]{2,3}$", "description": "Optional lowercase two- or three-letter language code supported by transcription. Omit for automatic language detection. Does not translate.", "examples": [ "en" ] }, "style": { "default": {}, "type": "object", "properties": { "position": { "description": "Vertical caption placement; omitted values use the preset's default.", "type": "string", "enum": [ "top", "center", "bottom" ] }, "text_color": { "description": "Text color as a six-digit hexadecimal color.", "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" }, "highlight_color": { "description": "Active-word color for highlight, push, action and karaoke; active-word box color for box. Ignored by other presets for compatibility.", "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" }, "font_family": { "description": "A bundled font; custom font URLs are not accepted.", "type": "string", "enum": [ "Inter", "Urbanist", "Rubik", "Bangers", "Paytone One", "Slackey", "Permanent Marker", "Caveat Brush", "Zilla Slab", "Young Serif", "Instrument Serif", "Bebas Neue", "Coiny" ] }, "font_weight": { "description": "Regular (400) or bold (700). thin&bold animates its own weight.", "anyOf": [ { "type": "number", "const": 400 }, { "type": "number", "const": 700 } ] }, "font_style": { "type": "string", "enum": [ "normal", "italic" ] }, "text_alignment": { "description": "Alignment of lines within the caption block.", "type": "string", "enum": [ "left", "center", "right" ] }, "text_case": { "description": "Visual casing only; downloaded transcripts preserve the original words.", "type": "string", "enum": [ "none", "uppercase", "lowercase", "capitalize" ] }, "text_width": { "description": "Text density: small uses larger type and fewer words per line. Font size is derived from font_family and text_width. Defaults to the preset's width for portrait/square, large for landscape.", "type": "string", "enum": [ "small", "medium", "large" ] }, "line_count": { "description": "Maximum lines per cue. 0 displays one word at a time; 100 allows up to 100 lines. Smash defaults to 0.", "anyOf": [ { "type": "number", "const": 0 }, { "type": "number", "const": 1 }, { "type": "number", "const": 2 }, { "type": "number", "const": 3 }, { "type": "number", "const": 100 } ] }, "scale": { "description": "Caption scale multiplied by video width / 1080. Defaults to the preset's scale for portrait/square, 0.55 for landscape.", "type": "number", "minimum": 0.2, "maximum": 1 }, "offset": { "description": "Pixel offsets in output-video coordinates. Left minus right moves horizontally; top minus bottom vertically. Unspecified top/bottom inherit the preset plus an inset on the selected edge: 20% of height for portrait/square, 14% for landscape. Explicit values replace the resolved offset, including zero.", "type": "object", "properties": { "left": { "type": "number", "minimum": -1920, "maximum": 1920 }, "top": { "type": "number", "minimum": -1920, "maximum": 1920 }, "right": { "type": "number", "minimum": -1920, "maximum": 1920 }, "bottom": { "type": "number", "minimum": -1920, "maximum": 1920 } }, "additionalProperties": false }, "background": { "description": "Caption background. Set type to enable it; color and opacity alone preserve the preset's background type.", "type": "object", "properties": { "type": { "type": "string", "enum": [ "none", "line", "line-rounded", "box", "box-rounded" ] }, "color": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" }, "opacity": { "type": "number", "minimum": 0, "maximum": 1 } }, "additionalProperties": false }, "outline": { "description": "Text outline on the caption canvas. Thickness 0 disables it.", "type": "object", "properties": { "color": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" }, "thickness": { "type": "number", "minimum": 0, "maximum": 20 } }, "additionalProperties": false }, "shadow": { "description": "Text shadow. Set enabled to show it. Distance and angle determine its position: 0 degrees points down, 90 points right.", "type": "object", "properties": { "enabled": { "type": "boolean" }, "color": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" }, "blur": { "type": "number", "minimum": 0, "maximum": 40 }, "distance": { "type": "number", "minimum": 0, "maximum": 20 }, "angle": { "type": "number", "minimum": -180, "maximum": 180 } }, "additionalProperties": false } }, "additionalProperties": false, "description": "Optional caption style overrides. Omitted fields (including nested fields) inherit the selected preset. Font size is derived from font_family and text_width; horizontal caption placement follows the preset. Direct font_size and horizontal_position overrides are not accepted. GET /v1/presets lists defaults, supported controls and preset-specific behavior." } }, "required": [ "video_url" ], "additionalProperties": false, "examples": [ { "video_url": "https://your-cdn.example/video.mp4", "preset": "highlight", "style": { "highlight_color": "#7651E8" } } ] }, "SubtitleStyle": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "position": { "description": "Vertical caption placement.", "type": "string", "enum": [ "top", "center", "bottom" ] }, "text_color": { "description": "Text fill as a six-digit hex color.", "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" }, "highlight_color": { "description": "Active-word color for highlight, push, action and karaoke; active-word box color for box. Ignored by other presets for compatibility.", "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" }, "font_family": { "description": "A bundled font; custom font URLs are not accepted.", "type": "string", "enum": [ "Inter", "Urbanist", "Rubik", "Bangers", "Paytone One", "Slackey", "Permanent Marker", "Caveat Brush", "Zilla Slab", "Young Serif", "Instrument Serif", "Bebas Neue", "Coiny" ] }, "font_weight": { "description": "Regular (400) or bold (700). thin&bold animates its own weight.", "anyOf": [ { "type": "number", "const": 400 }, { "type": "number", "const": 700 } ] }, "font_style": { "type": "string", "enum": [ "normal", "italic" ] }, "text_alignment": { "description": "Alignment of lines within the caption block.", "type": "string", "enum": [ "left", "center", "right" ] }, "text_case": { "description": "Visual casing only; downloaded transcripts preserve the original words.", "type": "string", "enum": [ "none", "uppercase", "lowercase", "capitalize" ] }, "text_width": { "description": "Text density: small uses larger type and fewer words per line. Font size is derived from font_family and text_width. Defaults to the preset's width for portrait/square, large for landscape.", "type": "string", "enum": [ "small", "medium", "large" ] }, "line_count": { "description": "Maximum lines per cue. 0 displays one word at a time; 100 allows up to 100 lines. Smash defaults to 0.", "anyOf": [ { "type": "number", "const": 0 }, { "type": "number", "const": 1 }, { "type": "number", "const": 2 }, { "type": "number", "const": 3 }, { "type": "number", "const": 100 } ] }, "scale": { "description": "Caption scale multiplied by video width / 1080. Defaults to the preset's scale for portrait/square, 0.55 for landscape.", "type": "number", "minimum": 0.2, "maximum": 1 }, "offset": { "description": "Pixel offsets in output-video coordinates. Left minus right moves horizontally; top minus bottom vertically. Unspecified top/bottom inherit the preset plus an inset on the selected edge: 20% of height for portrait/square, 14% for landscape. Explicit values replace the resolved offset, including zero.", "type": "object", "properties": { "left": { "type": "number", "minimum": -1920, "maximum": 1920 }, "top": { "type": "number", "minimum": -1920, "maximum": 1920 }, "right": { "type": "number", "minimum": -1920, "maximum": 1920 }, "bottom": { "type": "number", "minimum": -1920, "maximum": 1920 } }, "additionalProperties": false }, "background": { "description": "Caption background. Set type to enable it; color and opacity alone preserve the preset's background type.", "type": "object", "properties": { "type": { "type": "string", "enum": [ "none", "line", "line-rounded", "box", "box-rounded" ] }, "color": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" }, "opacity": { "type": "number", "minimum": 0, "maximum": 1 } }, "additionalProperties": false }, "outline": { "description": "Text outline on the caption canvas. Thickness 0 disables it.", "type": "object", "properties": { "color": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" }, "thickness": { "type": "number", "minimum": 0, "maximum": 20 } }, "additionalProperties": false }, "shadow": { "description": "Text shadow. Set enabled to show it. Distance and angle determine its position: 0 degrees points down, 90 points right.", "type": "object", "properties": { "enabled": { "type": "boolean" }, "color": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" }, "blur": { "type": "number", "minimum": 0, "maximum": 40 }, "distance": { "type": "number", "minimum": 0, "maximum": 20 }, "angle": { "type": "number", "minimum": -180, "maximum": 180 } }, "additionalProperties": false } }, "additionalProperties": false }, "Preset": { "type": "object", "required": [ "id", "name", "description", "animation", "defaults", "supported_style_fields", "style_notes" ], "properties": { "id": { "type": "string", "enum": [ "classic", "highlight", "smash", "box", "push", "elegant", "ogre", "outline", "paper-ink", "action", "prompter", "storytelling", "karaoke", "glow", "thin&bold", "background" ] }, "name": { "type": "string" }, "description": { "type": "string" }, "animation": { "type": "string", "description": "Animation selected by the preset; read-only." }, "defaults": { "$ref": "#/components/schemas/SubtitleStyle" }, "supported_style_fields": { "type": "array", "items": { "type": "string", "enum": [ "position", "text_color", "highlight_color", "font_family", "font_weight", "font_style", "text_alignment", "text_case", "text_width", "line_count", "scale", "offset", "background", "outline", "shadow" ] }, "description": "Style fields that affect this preset. Nested groups are partially overridable." }, "style_notes": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Preset-specific behavior keyed by style field path." } } }, "Error": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "properties": { "code": { "type": "string" }, "message": { "type": "string" }, "request_id": { "type": "string" } }, "required": [ "code", "message", "request_id" ] } } }, "Job": { "type": "object", "required": [ "id", "status", "progress", "usage" ], "properties": { "id": { "type": "string" }, "status": { "enum": [ "queued", "ingesting", "transcribing", "rendering", "completed", "failed", "canceled" ] }, "progress": { "type": "number", "minimum": 0, "maximum": 100, "description": "Progress from 0 to 100; not an estimate of remaining time." }, "created_at": { "type": "string", "format": "date-time" }, "expires_at": { "type": [ "string", "null" ], "format": "date-time", "description": "Result retention deadline, 24 hours after the terminal transition." }, "video": { "type": [ "object", "null" ], "properties": { "url": { "type": "string", "format": "uri" }, "content_type": { "const": "video/mp4" } }, "description": "Video download details. Null before completion or after expiration." }, "subtitles": { "type": "object", "properties": { "srt_url": { "type": [ "string", "null" ] }, "transcript_url": { "type": [ "string", "null" ] } }, "description": "Download URLs are null until completion and after expiration." }, "usage": { "type": "object", "properties": { "reserved_seconds": { "type": "integer" }, "billed_seconds": { "type": "integer" } }, "description": "Current reservation and final charge, in whole seconds." }, "error": { "type": [ "object", "null" ], "properties": { "code": { "type": "string" }, "message": { "type": "string" } } }, "metadata": { "type": [ "object", "null" ], "description": "Source dimensions and duration, available after inspection.", "properties": { "width": { "type": "integer" }, "height": { "type": "integer" }, "duration_seconds": { "type": "number" } } } } } } }, "paths": { "/v1/presets": { "get": { "operationId": "listPresets", "summary": "List available caption styles", "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "presets": { "type": "array", "items": { "$ref": "#/components/schemas/Preset" } } } }, "example": { "presets": [ { "id": "classic", "name": "Classic", "description": "White Inter captions with a dark outline.", "animation": "classic", "defaults": { "position": "bottom", "text_color": "#ffffff", "font_family": "Inter", "font_weight": 700, "font_style": "normal", "text_alignment": "center", "text_case": "none", "text_width": "small", "line_count": 2, "scale": 0.75, "offset": { "left": 0, "right": 0 }, "background": { "type": "none", "color": "#000000", "opacity": 1 }, "outline": { "color": "#000000", "thickness": 8 }, "shadow": { "enabled": false, "color": "#000000", "blur": 0, "distance": 8, "angle": 0 } }, "supported_style_fields": [ "position", "text_color", "font_family", "font_weight", "font_style", "text_alignment", "text_case", "text_width", "line_count", "scale", "offset", "background", "outline", "shadow" ], "style_notes": {} }, { "id": "highlight", "name": "Highlight", "description": "Uppercase Rubik captions highlighting the current word.", "animation": "colorHighlight", "defaults": { "position": "bottom", "text_color": "#ffffff", "highlight_color": "#ffff00", "font_family": "Rubik", "font_weight": 700, "font_style": "normal", "text_alignment": "center", "text_case": "uppercase", "text_width": "medium", "line_count": 2, "scale": 0.7, "offset": { "left": 0, "right": 0 }, "background": { "type": "none", "color": "#000000", "opacity": 1 }, "outline": { "color": "#000000", "thickness": 0 }, "shadow": { "enabled": true, "color": "#3d3d3d", "blur": 7, "distance": 0, "angle": 0 } }, "supported_style_fields": [ "position", "text_color", "highlight_color", "font_family", "font_weight", "font_style", "text_alignment", "text_case", "text_width", "line_count", "scale", "offset", "background", "outline", "shadow" ], "style_notes": { "highlight_color": "Color of the active word." } }, { "id": "smash", "name": "Smash", "description": "Outlined Paytone One captions that pop in one word at a time.", "animation": "popIn", "defaults": { "position": "bottom", "text_color": "#ffffff", "font_family": "Paytone One", "font_weight": 700, "font_style": "normal", "text_alignment": "center", "text_case": "uppercase", "text_width": "medium", "line_count": 0, "scale": 1, "offset": { "left": 0, "right": 0 }, "background": { "type": "none", "color": "#000000", "opacity": 1 }, "outline": { "color": "#000000", "thickness": 19 }, "shadow": { "enabled": false, "color": "#000000", "blur": 0, "distance": 8, "angle": 0 } }, "supported_style_fields": [ "position", "text_color", "font_family", "font_weight", "font_style", "text_alignment", "text_case", "text_width", "line_count", "scale", "offset", "background", "outline", "shadow" ], "style_notes": { "line_count": "Defaults to 0 (one word per cue). Set 1, 2 or 3 to group words into lines." } }, { "id": "box", "name": "Box", "description": "Inter captions with a colored box behind the active word.", "animation": "boxHighlight", "defaults": { "position": "bottom", "text_color": "#ffffff", "highlight_color": "#7447E3", "font_family": "Inter", "font_weight": 700, "font_style": "normal", "text_alignment": "center", "text_case": "none", "text_width": "small", "line_count": 2, "scale": 0.62, "offset": { "left": 0, "right": 0 }, "background": { "type": "none", "color": "#000000", "opacity": 1 }, "outline": { "color": "#000000", "thickness": 0 }, "shadow": { "enabled": true, "color": "#828282", "blur": 8, "distance": 0, "angle": 0 } }, "supported_style_fields": [ "position", "text_color", "highlight_color", "font_family", "font_weight", "font_style", "text_alignment", "text_case", "text_width", "line_count", "scale", "offset", "background", "outline", "shadow" ], "style_notes": { "highlight_color": "Color of the box behind the active word; independent of background.color." } }, { "id": "push", "name": "Push", "description": "Uppercase Rubik captions with a green highlight and a scale animation.", "animation": "colorHighlightScale", "defaults": { "position": "bottom", "text_color": "#ffffff", "highlight_color": "#00ff4f", "font_family": "Rubik", "font_weight": 700, "font_style": "normal", "text_alignment": "center", "text_case": "uppercase", "text_width": "small", "line_count": 2, "scale": 0.72, "offset": { "left": 0, "right": 0 }, "background": { "type": "none", "color": "#000000", "opacity": 1 }, "outline": { "color": "#000000", "thickness": 18 }, "shadow": { "enabled": false, "color": "#000000", "blur": 0, "distance": 8, "angle": 0 } }, "supported_style_fields": [ "position", "text_color", "highlight_color", "font_family", "font_weight", "font_style", "text_alignment", "text_case", "text_width", "line_count", "scale", "offset", "background", "outline", "shadow" ], "style_notes": { "highlight_color": "Color of the active word during the scale animation." } }, { "id": "elegant", "name": "Elegant", "description": "Uppercase Inter captions that float into place with a soft shadow.", "animation": "floatInTop", "defaults": { "position": "bottom", "text_color": "#ffffff", "font_family": "Inter", "font_weight": 700, "font_style": "normal", "text_alignment": "center", "text_case": "uppercase", "text_width": "medium", "line_count": 2, "scale": 0.65, "offset": { "left": 0, "right": 0 }, "background": { "type": "none", "color": "#000000", "opacity": 1 }, "outline": { "color": "#000000", "thickness": 0 }, "shadow": { "enabled": true, "color": "#000000", "blur": 7, "distance": 0, "angle": 0 } }, "supported_style_fields": [ "position", "text_color", "font_family", "font_weight", "font_style", "text_alignment", "text_case", "text_width", "line_count", "scale", "offset", "background", "outline", "shadow" ], "style_notes": {} }, { "id": "ogre", "name": "Ogre", "description": "Green Caveat Brush captions that pop into the center of the video.", "animation": "popIn", "defaults": { "position": "center", "text_color": "#1c9311", "font_family": "Caveat Brush", "font_weight": 700, "font_style": "normal", "text_alignment": "center", "text_case": "uppercase", "text_width": "small", "line_count": 1, "scale": 0.6, "offset": { "left": 0, "right": 0 }, "background": { "type": "none", "color": "#ffffff", "opacity": 1 }, "outline": { "color": "#000000", "thickness": 0 }, "shadow": { "enabled": false, "color": "#000000", "blur": 0, "distance": 8, "angle": 0 } }, "supported_style_fields": [ "position", "text_color", "font_family", "font_weight", "font_style", "text_alignment", "text_case", "text_width", "line_count", "scale", "offset", "background", "outline", "shadow" ], "style_notes": {} }, { "id": "outline", "name": "Outline", "description": "Warm Rubik captions with a thick orange outline and changing opacity.", "animation": "fillOpacity", "defaults": { "position": "bottom", "text_color": "#ffe4bb", "font_family": "Rubik", "font_weight": 700, "font_style": "normal", "text_alignment": "center", "text_case": "none", "text_width": "large", "line_count": 1, "scale": 0.72, "offset": { "left": 0, "right": 0 }, "background": { "type": "none", "color": "#000000", "opacity": 1 }, "outline": { "color": "#cb8003", "thickness": 20 }, "shadow": { "enabled": false, "color": "#000000", "blur": 0, "distance": 8, "angle": 0 } }, "supported_style_fields": [ "position", "text_color", "font_family", "font_weight", "font_style", "text_alignment", "text_case", "text_width", "line_count", "scale", "offset", "background", "outline", "shadow" ], "style_notes": {} }, { "id": "paper-ink", "name": "PaperInk", "description": "Black Rubik captions on rounded white lines at the top.", "animation": "fillOpacity", "defaults": { "position": "top", "text_color": "#000000", "font_family": "Rubik", "font_weight": 700, "font_style": "normal", "text_alignment": "center", "text_case": "none", "text_width": "large", "line_count": 1, "scale": 0.72, "offset": { "left": 0, "right": 0 }, "background": { "type": "line-rounded", "color": "#ffffff", "opacity": 1 }, "outline": { "color": "#cb8003", "thickness": 0 }, "shadow": { "enabled": false, "color": "#000000", "blur": 0, "distance": 8, "angle": 0 } }, "supported_style_fields": [ "position", "text_color", "font_family", "font_weight", "font_style", "text_alignment", "text_case", "text_width", "line_count", "scale", "offset", "background", "outline", "shadow" ], "style_notes": {} }, { "id": "action", "name": "Action", "description": "Italic Bebas Neue captions with an outline, shadow and active-word color.", "animation": "colorHighlightAppear", "defaults": { "position": "bottom", "text_color": "#ffffff", "highlight_color": "#FFFF00", "font_family": "Bebas Neue", "font_weight": 700, "font_style": "italic", "text_alignment": "center", "text_case": "none", "text_width": "medium", "line_count": 2, "scale": 0.7, "offset": { "left": 0, "right": 0 }, "background": { "type": "none", "color": "#000000", "opacity": 1 }, "outline": { "color": "#000000", "thickness": 12 }, "shadow": { "enabled": true, "color": "#000000", "blur": 0, "distance": 12, "angle": 0 } }, "supported_style_fields": [ "position", "text_color", "highlight_color", "font_family", "font_weight", "font_style", "text_alignment", "text_case", "text_width", "line_count", "scale", "offset", "background", "outline", "shadow" ], "style_notes": { "highlight_color": "Color of the active word as it appears." } }, { "id": "prompter", "name": "Prompter", "description": "Centered Zilla Slab captions with a white outline and active-word opacity.", "animation": "highlight", "defaults": { "position": "center", "text_color": "#000000", "font_family": "Zilla Slab", "font_weight": 700, "font_style": "normal", "text_alignment": "center", "text_case": "uppercase", "text_width": "medium", "line_count": 1, "scale": 0.66, "offset": { "left": 0, "right": 0 }, "background": { "type": "none", "color": "#ffffff", "opacity": 1 }, "outline": { "color": "#ffffff", "thickness": 17 }, "shadow": { "enabled": false, "color": "#ff00c7", "blur": 10, "distance": 0, "angle": 0 } }, "supported_style_fields": [ "position", "text_color", "font_family", "font_weight", "font_style", "text_alignment", "text_case", "text_width", "line_count", "scale", "offset", "background", "outline", "shadow" ], "style_notes": { "highlight_color": "This preset highlights by opacity, so highlight_color has no effect." } }, { "id": "storytelling", "name": "Storytelling", "description": "Gold Instrument Serif captions with a red outline and offset shadow.", "animation": "slideInLeft", "defaults": { "position": "bottom", "text_color": "#ffbf00", "font_family": "Instrument Serif", "font_weight": 700, "font_style": "normal", "text_alignment": "center", "text_case": "none", "text_width": "medium", "line_count": 2, "scale": 0.68, "offset": { "left": 0, "right": 0 }, "background": { "type": "none", "color": "#000000", "opacity": 1 }, "outline": { "color": "#ff0000", "thickness": 13 }, "shadow": { "enabled": true, "color": "#ff0000", "blur": 0, "distance": 14, "angle": 51 } }, "supported_style_fields": [ "position", "text_color", "font_family", "font_weight", "font_style", "text_alignment", "text_case", "text_width", "line_count", "scale", "offset", "background", "outline", "shadow" ], "style_notes": {} }, { "id": "karaoke", "name": "Karaoke", "description": "Rounded Coiny lettering with a progressive karaoke fill.", "animation": "twoToneFill", "defaults": { "position": "bottom", "text_color": "#ffffff", "highlight_color": "#634afc", "font_family": "Coiny", "font_weight": 700, "font_style": "normal", "text_alignment": "center", "text_case": "uppercase", "text_width": "small", "line_count": 2, "scale": 0.6, "offset": { "left": 0, "right": 0 }, "background": { "type": "none", "color": "#000000", "opacity": 1 }, "outline": { "color": "#ffffff", "thickness": 18 }, "shadow": { "enabled": false, "color": "#3d3d3d", "blur": 7, "distance": 0, "angle": 0 } }, "supported_style_fields": [ "position", "text_color", "highlight_color", "font_family", "font_weight", "font_style", "text_alignment", "text_case", "text_width", "line_count", "scale", "offset", "background", "outline", "shadow" ], "style_notes": { "highlight_color": "Progressive fill color of spoken words." } }, { "id": "glow", "name": "Glow", "description": "Rubik captions with a pink glow and a fade-in animation.", "animation": "fadeIn", "defaults": { "position": "bottom", "text_color": "#ffffff", "font_family": "Rubik", "font_weight": 700, "font_style": "normal", "text_alignment": "center", "text_case": "none", "text_width": "small", "line_count": 2, "scale": 0.65, "offset": { "left": 0, "right": 0 }, "background": { "type": "none", "color": "#000000", "opacity": 1 }, "outline": { "color": "#000000", "thickness": 0 }, "shadow": { "enabled": true, "color": "#ff00de", "blur": 12, "distance": 0, "angle": 0 } }, "supported_style_fields": [ "position", "text_color", "font_family", "font_weight", "font_style", "text_alignment", "text_case", "text_width", "line_count", "scale", "offset", "background", "outline", "shadow" ], "style_notes": {} }, { "id": "thin&bold", "name": "Thin&Bold", "description": "Uppercase Inter captions with animated thin and bold weights.", "animation": "bolder", "defaults": { "position": "bottom", "text_color": "#ffffff", "font_family": "Inter", "font_weight": 700, "font_style": "normal", "text_alignment": "center", "text_case": "uppercase", "text_width": "medium", "line_count": 2, "scale": 0.7, "offset": { "left": 0, "right": 0 }, "background": { "type": "none", "color": "#000000", "opacity": 1 }, "outline": { "color": "#000000", "thickness": 0 }, "shadow": { "enabled": true, "color": "#000000", "blur": 5, "distance": 0, "angle": 0 } }, "supported_style_fields": [ "position", "text_color", "font_family", "font_style", "text_alignment", "text_case", "text_width", "line_count", "scale", "offset", "background", "outline", "shadow" ], "style_notes": { "font_weight": "The animation uses weights 200 and 700; font_weight does not change them." } }, { "id": "background", "name": "Background", "description": "Urbanist captions on a rounded black background.", "animation": "classic", "defaults": { "position": "bottom", "text_color": "#ffffff", "font_family": "Urbanist", "font_weight": 700, "font_style": "normal", "text_alignment": "center", "text_case": "none", "text_width": "large", "line_count": 2, "scale": 0.65, "offset": { "left": 0, "right": 0 }, "background": { "type": "box-rounded", "color": "#000000", "opacity": 1 }, "outline": { "color": "#000000", "thickness": 0 }, "shadow": { "enabled": false, "color": "#000000", "blur": 0, "distance": 8, "angle": 0 } }, "supported_style_fields": [ "position", "text_color", "font_family", "font_weight", "font_style", "text_alignment", "text_case", "text_width", "line_count", "scale", "offset", "background", "outline", "shadow" ], "style_notes": {} } ] } } } } } } }, "/v1/subtitles": { "post": { "operationId": "createSubtitles", "summary": "Create a captioned video", "description": "Each accepted submission creates a new job, including repeated input. Requires a positive prepaid balance. After inspecting the video, reserves ceil(actual duration) before transcription. Insufficient credit at inspection fails the job with INSUFFICIENT_CREDITS. Charges ceil(actual duration) exactly once on success; failures and cancellations release the reservation. Direct HTTPS MP4/MOV/WebM URLs only; redirects are not followed. Maximum 600 seconds, 500 MiB and 1080p in either orientation.", "security": [ { "apiKey": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SubtitleRequest" } } } }, "responses": { "202": { "description": "Accepted; poll status_url every five seconds", "content": { "application/json": { "schema": { "type": "object", "required": [ "id", "status", "status_url" ], "properties": { "id": { "type": "string" }, "status": { "type": "string" }, "status_url": { "type": "string", "format": "uri" } } }, "example": { "id": "example_job_id", "status": "ingesting", "status_url": "https://api.captioncraft.studio/v1/jobs/example_job_id" } } }, "headers": { "Location": { "description": "Status URL for the accepted job.", "schema": { "type": "string", "format": "uri" } }, "Retry-After": { "description": "Recommended polling interval in seconds.", "schema": { "type": "integer", "example": 5 } } } }, "400": { "description": "Invalid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Missing or invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Insufficient prepaid seconds", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Missing key scope or disabled account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "413": { "description": "JSON body exceeds 16 KiB", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "415": { "description": "Use Content-Type: application/json", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Rate or concurrency limit exceeded; honor Retry-After", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }, "headers": { "Retry-After": { "description": "Minimum retry delay in seconds.", "schema": { "type": "integer", "example": 5 } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "Processing is not configured", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/jobs/{id}": { "get": { "operationId": "getSubtitleJob", "summary": "Get progress and result downloads", "security": [ { "apiKey": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Job ID returned by a create request. The job must belong to your account." } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Job" }, "example": { "id": "example_job_id", "status": "ingesting", "progress": 0, "created_at": "2026-09-21T10:00:00Z", "expires_at": null, "video": null, "subtitles": { "srt_url": null, "transcript_url": null }, "metadata": null, "usage": { "reserved_seconds": 0, "billed_seconds": 0 }, "error": null } } } }, "401": { "description": "Missing or invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Missing key scope or disabled account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Job or endpoint not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Rate or concurrency limit exceeded; honor Retry-After", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }, "headers": { "Retry-After": { "description": "Minimum retry delay in seconds.", "schema": { "type": "integer", "example": 5 } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/jobs/{id}/cancel": { "post": { "operationId": "cancelSubtitleJob", "summary": "Cancel an unfinished job and release credits", "security": [ { "apiKey": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Job ID returned by a create request. The job must belong to your account." } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "string" }, "status": { "type": "string" } } }, "example": { "id": "example_job_id", "status": "canceled" } } } }, "401": { "description": "Missing or invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Missing key scope or disabled account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Job or endpoint not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Rate or concurrency limit exceeded; honor Retry-After", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }, "headers": { "Retry-After": { "description": "Minimum retry delay in seconds.", "schema": { "type": "integer", "example": 5 } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/usage": { "get": { "operationId": "getUsage", "summary": "Read prepaid usage in seconds", "security": [ { "apiKey": [] } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { "available_seconds": { "type": "integer", "minimum": 0 }, "reserved_seconds": { "type": "integer", "minimum": 0 }, "consumed_seconds": { "type": "integer", "minimum": 0 }, "active_jobs": { "type": "integer", "minimum": 0 }, "concurrency_limit": { "type": "integer", "minimum": 0 } } }, "example": { "available_seconds": 300, "reserved_seconds": 60, "consumed_seconds": 24, "active_jobs": 1, "concurrency_limit": 2 } } } }, "401": { "description": "Missing or invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Missing key scope or disabled account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Rate or concurrency limit exceeded; honor Retry-After", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }, "headers": { "Retry-After": { "description": "Minimum retry delay in seconds.", "schema": { "type": "integer", "example": 5 } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } } } ```