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. Upload jobs settle on the source length before rendering; for highlights that is the video you submit (after any trim), not the clips.

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

MethodPathPurposeCredits
GET/v1/accountBalances, pricing rules and limits for the key's ownerFree
GET/v1/catalogVoices (with previews and the premium flag), caption presets, chat themes, highlight types, stock backgrounds and musicFree
POST/v1/uploadsDeclare a video or audio file (filename, content_type, size_bytes); returns a signed PUT URLFree
POST/v1/uploads/{id}/completeVerify the PUT; the id becomes source_upload_id for upload-based jobsFree
POST/v1/jobsQueue a render: stories, fake_text, subtitles, splitscreen or highlights (same schemas as the app)Reserved on accept, settled on length (source length for upload jobs)
GET/v1/jobsList 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/webhooksList endpointsFree
POST/v1/webhooksRegister an https endpoint for job.completed / job.failed; returns the signing secret onceFree
POST/v1/webhooks/{id}Send a test deliveryFree
DELETE/v1/webhooks/{id}Remove an endpointFree

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 with POST /v1/uploads, PUT the bytes to the returned URL with the same Content-Type, then POST /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; renders made on paid credits do not.

What the API does not do

The API renders and hands back a download URL. It has no endpoints for publishing, scheduling or connecting social accounts, and neither do the CLI or the MCP server. Those live in the web app: a video rendered through the API appears in your StoryTok library, where you can publish or schedule it to connected TikTok, Instagram or YouTube accounts if it is completed and unwatermarked, at 1 paid credit per video per account. API usage itself only ever spends render credits.

Need something the API does not expose yet? Email [email protected] with what you are building.