Developer API
Render short-form video from your own code
The same pipeline behind storytok.ai, over one JSON API: narrated story videos, texting-story videos, auto-captioned uploads, split-screens and AI highlight clips. Keys spend the same credits as the app, so there is no separate plan — 1 credit per rendered minute, 2 per minute on premium voices, credits never expire.
Create a key under Settings → Developer API. Keys start with stk_live_ and carry the full permissions of your account: keep them server-side.
The basics
Base URL https://storytok.ai/api/v1
Auth header Authorization: Bearer stk_live_…
Content type application/json
Errors { "success": false, "error": { "code": "…", "message": "…" } }Quickstart: a narrated story in three calls
# 1. See what you can reference (voices, caption presets, backgrounds, music)
curl https://storytok.ai/api/v1/catalog -H "Authorization: Bearer $STORYTOK_API_KEY"
# 2. Queue the render (credits are reserved now, settled against the real length)
curl -X POST https://storytok.ai/api/v1/jobs \
-H "Authorization: Bearer $STORYTOK_API_KEY" -H "Content-Type: application/json" \
-d '{
"jobType": "stories",
"jobData": {
"title": "My landlord charged me for a room that did not exist",
"content": "My landlord tried to charge me $600 for a bedroom I had never rented, so I made him prove it existed. …",
"voice": "Joanna",
"voice_speed": 1.2,
"background": "Minecraft 4.mp4",
"subtitle_style": { "preset": "hormozi", "font_size": 48, "text_color": "#ffffff",
"border_color": "#000000", "border_width": 6, "position": "center" },
"enable_intro": false
}
}'
# 3. Poll until completed, then download (URL valid for one hour)
curl https://storytok.ai/api/v1/jobs/<job_id> -H "Authorization: Bearer $STORYTOK_API_KEY"Every endpoint
| Method | Path | Purpose | Credits |
|---|---|---|---|
| GET | /v1/account | Balances, pricing rules and limits for the key's owner | Free |
| GET | /v1/catalog | Voices (with previews and the premium flag), caption presets, chat themes, highlight types, stock backgrounds and music | Free |
| POST | /v1/uploads | Declare a video or audio file (filename, content_type, size_bytes); returns a signed PUT URL | Free |
| POST | /v1/uploads/{id}/complete | Verify the PUT; the id becomes source_upload_id for upload-based jobs | Free |
| POST | /v1/jobs | Queue a render: stories, fake_text, subtitles, splitscreen or highlights (same schemas as the app) | Reserved on accept, settled on length |
| GET | /v1/jobs | List your jobs, newest first (filter by status / job_type) | Free |
| GET | /v1/jobs/{id} | Status, progress, charge, and a signed download URL once completed (per-clip URLs for highlights) | Free |
| GET | /v1/webhooks | List endpoints | Free |
| POST | /v1/webhooks | Register an https endpoint for job.completed / job.failed; returns the signing secret once | Free |
| POST | /v1/webhooks/{id} | Send a test delivery | Free |
| DELETE | /v1/webhooks/{id} | Remove an endpoint | Free |
Job types
- stories — title, content (≤12,000 chars), voice, optional voice_speed (0.8–1.5), background, subtitle_style, music, enable_intro. Settles on the narrated length.
- fake_text — theme, contact_name, messages [{side, text, delay_ms}] (≤60), voices {left, right}, narration "both" | "none", sfx, background, music. Settles on the rendered length.
- subtitles / splitscreen / highlights — need a
source_upload_id: declare the file withPOST /v1/uploads, PUT the bytes to the returned URL with the same Content-Type, thenPOST /v1/uploads/{id}/complete. Split-screen accepts layout "classic" (with a background) or "streamer" (with a facecam rectangle). Highlights takes clip_count 1–20, highlight_type, clip_style, and returns clips ranked by a 0–100 score with a suggested hook.
Webhooks
Deliveries are JSON POSTs with a StoryTok-Signature: t=<unix>,v1=<hex> header, where v1 is HMAC-SHA256 of `${t}.${rawBody}` with your endpoint secret. Reject anything older than five minutes. One retry on 5xx or network failure; polling GET /v1/jobs/{id} is always a valid fallback.
import { createHmac, timingSafeEqual } from "node:crypto"
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")))
if (!parts.t || !parts.v1) return false
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false
const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex")
return expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}
// payload
{ "event": "job.completed", "created_at": "…",
"data": { "job_id": "…", "job_type": "stories", "status": "completed",
"title": "…", "credits_charged": 2, "duration_seconds": 97.4 } }Limits
- 3 active renders per account at a time (queued, starting or processing).
- Up to 5 active API keys and 5 webhook endpoints per account.
- Uploads up to 500 MB; stories up to 12,000 characters; texting stories up to 60 messages; highlights up to 20 clips from one source.
- Failed renders refund their reservation automatically. Trial-minute renders carry a watermark; any paid pack removes it.
Need something the API does not expose yet? Email juan@storytok.ai with what you are building.