API Documentation
REST API to fetch YouTube video transcripts with asynchronous processing, prepaid credit billing and outbound webhooks signed with HMAC-SHA256.
Interactive spec: Swagger UI at /api/v1/docs · OpenAPI JSON
Getting Started
1. Create an API key
Sign in and open /account/api-keys to generate a
key. The sk_live_… token is shown only once — store it
immediately.
Scopes:
read— read-only (list, fetch, balance, deliveries).read-write— create/retry/delete transcripts and purchases.admin— create new keys, configure webhook.
2. Make sure you have credits
Each transcript costs credits (defined per package). Buy at
/account/billing or via
POST /api/v1/credits/purchase. Current balance:
curl https://<host>/api/v1/credits/balance \
-H "Authorization: Bearer sk_live_…"
3. Request a transcript
curl -X POST https://<host>/api/v1/transcripts \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"videos": [{ "url": "https://www.youtube.com/watch?v=…",
"requested_language": "en" }]
}'
A 201 response returns transcript_ids. Status follows
todo → doing → done (or error). Poll:
curl https://<host>/api/v1/transcripts/<id> \
-H "Authorization: Bearer sk_live_…"
Webhooks (HMAC signature + retries)
Instead of polling, configure an outbound webhook to be notified whenever a transcript changes state.
Configure
curl -X PATCH https://<host>/api/v1/account/webhook \
-H "Authorization: Bearer sk_live_…_admin" \
-H "Content-Type: application/json" \
-d '{ "webhook_url": "https://my-app.com/hooks/transcript" }'
The response includes webhook_secret — shown only once. Store it
to validate signatures. DELETE on the same endpoint removes the
configuration.
Verify the signature
Every POST includes the header
X-Webhook-Signature: sha256=<hex> computed over the raw body with
your webhook_secret.
// Node 18+ / Bun
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody: string, headerSig: string, secret: string) {
const expected = "sha256=" + createHmac("sha256", secret)
.update(rawBody).digest("hex");
const a = Buffer.from(headerSig);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}
Always use timingSafeEqual (or equivalent) — direct string
comparison leaks timing.
Retries
Failures (timeout or HTTP ≥ 400) enter a retry loop with exponential
backoff, up to webhook_max_attempts attempts (configured globally,
default 5). History at
GET /api/v1/account/webhook/deliveries.
Rate Limits
Limits are applied per API key. Headers on every response:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Limit for the current window |
X-RateLimit-Remaining | Remaining requests |
X-RateLimit-Reset | Epoch (s) when the window resets |
Retry-After | (On 429) seconds until you may retry |
429 response:
{ "error": { "code": "RATE_LIMITED", "message": "Too many requests" } }
Recommended strategy: exponential backoff honouring Retry-After.
Error codes
All errors follow the envelope:
{ "error": { "code": "INVALID_REQUEST", "message": "…", "details": {} } }
| HTTP | error.code | When |
|---|---|---|
| 400 | INVALID_REQUEST | Invalid body / query |
| 400 | INVALID_VIDEO_URL | Unrecognised video URL |
| 400 | INVALID_WEBHOOK_URL | Invalid webhook URL (https) |
| 401 | UNAUTHORIZED | Missing/invalid/revoked API key |
| 402 | INSUFFICIENT_CREDITS | Insufficient balance |
| 403 | FORBIDDEN | Key scope does not allow the action |
| 404 | TRANSCRIPT_NOT_FOUND | Resource missing or not yours |
| 409 | INVALID_STATUS_TRANSITION | e.g. retry on non-error transcript |
| 409 | TRANSCRIPT_LOCKED | Cannot delete while processing |
| 429 | RATE_LIMITED | See X-RateLimit-* headers |
| 500 | INTERNAL_ERROR | Internal error (report with x-request-id) |
Next steps
- Import the Postman Collection to test quickly.
- Browse the full spec in Swagger UI.
- For issues, include the
x-request-idresponse header.