Developers

API reference

Send audio, get transcripts back. A small REST API with the same jobs, exports and pricing as the app. Machine-readable spec: /v1/openapi.json.

Authentication

Create a key in the app under Settings, API keys. Keys look like cs_live_... and are shown once. Send one with every request:

Authorization: Bearer cs_live_xxxxxxxxxxxxxxxx

All endpoints live under https://your-domain/v1. A key acts for the workspace it was created in; usage is charged to that workspace's balance. Read-only keys can list and download but cannot create or delete jobs.

Errors and limits

Errors use normal HTTP status codes and one JSON shape:

{ "error": { "code": "insufficient_balance", "message": "Add credit to transcribe this file.", "details": { "needed_cents": 412 } } }
StatusMeaning
400Invalid input; details.field names the field
401Missing or revoked key
402Not enough credit, or the monthly cap would be exceeded
404No such job in this workspace
409Wrong state, for example the transcript is not ready yet
415Unsupported file type
429Rate limited: 120 requests per minute per key. Honor Retry-After

POST/v1/jobs

Creates a job and returns a one-time upload URL. Include duration_sec if you know it for an accurate estimate. Send an Idempotency-Key header to make retries safe.

curl https://your-domain/v1/jobs \
  -H "Authorization: Bearer $CS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "board-meeting.m4a",
    "size_bytes": 48211022,
    "mime": "audio/mp4",
    "duration_sec": 3121,
    "speakers_expected": 4,
    "vocabulary": ["Okonkwo", "Q3 roadmap"],
    "callback_url": "https://example.com/hooks/transcripts"
  }'
{
  "id": "job_8f2c...",
  "status": "awaiting_upload",
  "est_cost_cents": 521,
  "upload_url": "https://your-domain/u/up_3b9e...",
  "upload_method": "PUT",
  "upload_expires_in": 3600
}

Other fields: project_id, language (default auto), style (clean or verbatim), notify_emails, notify_mode. To import from a public link instead of uploading, send url; the job goes straight to queued.

PUTupload_url

PUT the raw file bytes to the URL you got back. No auth header is needed; the URL itself is the credential and expires after an hour.

curl -X PUT --data-binary @board-meeting.m4a \
  -H "Content-Type: audio/mp4" "$UPLOAD_URL"

GET/v1/jobs/{id}

Status moves awaiting_upload → queued → transcribing → ready (or failed). progress is 0 to 100. Prefer webhooks to polling; if you poll, every 10 to 30 seconds is plenty.

GET/v1/jobs/{id}/transcript

Returns the structured transcript once the job is ready:

{
  "job_id": "job_8f2c...",
  "version": 1,
  "doc": {
    "speakers": [{ "id": "S1", "name": "Speaker 1", "color": "#4f5bd5" }],
    "turns": [{
      "id": "t1", "speaker": "S1", "start": 0.42, "end": 6.1,
      "tokens": [{ "w": "Thanks" }, { "w": "everyone" }, { "w": "Okonkwo", "u": 1, "alts": ["Okonko", "O'Konkwo"] }]
    }]
  },
  "insights": { "summary": "...", "chapters": [{ "start": 0, "title": "Opening" }], "actions": ["..."], "keywords": ["..."] }
}

Tokens with "u": 1 are low-confidence words; alts holds the likely alternatives. You can save edits back with PUT /v1/jobs/{id}/transcript and { "doc": ..., "base_version": 1 }; a stale base_version returns 409 version_conflict.

GET/v1/jobs/{id}/export

Downloads a file. format is one of docx, txt, srt, vtt, json, html. Add timestamps=0 or speakers=0 to leave those out.

curl -o meeting.docx -H "Authorization: Bearer $CS_KEY" \
  "https://your-domain/v1/jobs/job_8f2c.../export?format=docx"

Other endpoints

GET/v1/jobsList jobs. Filters: status, project_id, q; paginate with before and limit (returns has_more and next_before)
PATCH/v1/jobs/{id}Rename, or move to another project
POST/v1/jobs/{id}/cancelCancel a job that has not finished. Not charged
POST/v1/jobs/{id}/retryRetry a failed job
DELETE/v1/jobs/{id}Delete the job, its audio and every transcript version
GET/v1/jobs/{id}/audioStream the original audio; supports Range
GET/v1/estimate?duration_sec=Price a file before creating a job

Webhooks

Two ways to hear about finished jobs: a per-job callback_url, or workspace webhooks configured under Settings, Webhooks (with delivery logs and retries). Events are job.completed and job.failed:

{
  "id": "evt_41d0...",
  "type": "job.completed",
  "created": "2026-10-22T18:04:11.000Z",
  "data": { "id": "job_8f2c...", "name": "board-meeting.m4a", "status": "ready", "duration_sec": 3121, "cost_cents": 521 }
}

Respond with any 2xx within 10 seconds. Failed deliveries are retried with backoff for about a day.

Verify signatures

Every delivery carries a Scribberty-Signature header: t=<unix seconds>,v1=<hex>, where the hex is an HMAC-SHA256 of t + "." + raw body keyed with your webhook secret. Reject anything older than five minutes.

import crypto from 'node:crypto';

export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}