Webhooks
Receive real-time notifications when transcriptions complete or fail instead of polling.
Overview
When you provide a webhook_url in your transcription request, we'll POST progress and result events to that URL as the job moves through the pipeline. Webhooks are signed with HMAC-SHA256 so you can verify they came from Scriptivox.
Webhooks fire for both upload-based and URL-based transcription flows. The webhook_url field is optional — if omitted, no webhooks are sent and you should poll GET /v1/transcribe/{id} instead.
You can also set a default webhook URL in your account settings that is used for any transcription that does not specify one in the request body.
Webhooks are best-effort
Webhook delivery is fire-and-forget with no retries. If your endpoint is down or returns a non-2xx response, the event is dropped — the transcription itself still completes normally. Always pair webhooks with polling GET /v1/transcribe/{id} as a fallback so you never miss a completion.
Setting up webhooks
Pass a webhook_url when starting a transcription:
requests.post("https://api.scriptivox.com/v1/transcribe",headers={"Authorization": "sk_live_YOUR_KEY"},json={"upload_id": "abc-123","webhook_url": "https://your-server.com/webhook"})
Webhook events
Three events fire per job: transcription.processing once the file has been validated and dispatched to a GPU, then exactly one of transcription.completed or transcription.failed when the job terminates. The created and downloading states are visible in GET /v1/transcribe/{id} polling but do not emit webhooks.
transcription.processing
Sent when the audio file has been downloaded and validated, and the transcription job has been submitted for processing. Includes the detected duration and reserved cost.
{"event": "transcription.processing","transcription_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901","status": "processing","duration_seconds": 120,"cost_cents": 0.5}
Both URL-based and upload-based transcriptions receive this event after the file has been validated and the job is submitted.
transcription.completed
Sent when a transcription finishes successfully. Includes the full result.
{"event": "transcription.completed","transcription_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901","status": "completed","duration_seconds": 120,"cost_cents": 0.5,"result": {"full_transcript": "Hello, thanks for joining...","language": "en","duration_seconds": 120,"speakers": ["SPEAKER 1", "SPEAKER 2"],"utterances": [{"start": 0.5,"end": 3.2,"text": "Hello, thanks for joining the call today.","speaker": "SPEAKER 1","confidence": 0.95,"words": [...]}]}}
transcription.failed
Sent any time a transcription ends in the failed state. Reserved balance is automatically released so a failed job costs $0.
This event covers every failure surface: URL download/validation errors, insufficient balance, internal queue/server errors, GPU processing failures, and cleanup-timeout failures (when a job sits stuck in created, downloading, or processing past its threshold). You will receive at most one transcription.failed event per transcription, so a single handler can cover all failure modes — branch on error.code (see API reference: error codes) for code-specific behavior.
error.message is a sanitized, customer-safe string; we don't forward raw GPU/library stack traces. For PROCESSING_ERROR cases where the message is generic, retrying is usually the right next step.
{"event": "transcription.failed","transcription_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901","status": "failed","error": {"code": "PROCESSING_ERROR","message": "Failed to process audio file"}}
Events summary
| Event | When | Flows |
|---|---|---|
| transcription.processing | File validated, job submitted to GPU | Both |
| transcription.completed | Transcription finished successfully | Both |
| transcription.failed | Download, validation, or processing failed | Both |
Verifying webhook signatures
Every webhook request includes two headers for signature verification:
| Header | Description |
|---|---|
X-Scriptivox-Signature | HMAC-SHA256 hex digest of the signed payload |
X-Scriptivox-Timestamp | Unix timestamp (seconds, as a string) of when the webhook was sent |
Signing formula
signing_secret = SHA256_hex(api_key)— hex digest of your API key as a UTF-8 stringsigned_payload = f"{timestamp}.{body}"— body is the exact raw JSON request body, byte-for-bytesignature = HMAC_SHA256_hex(signing_secret, signed_payload)— sent asX-Scriptivox-Signature
Reject any request whose X-Scriptivox-Timestamp is more than 5 minutes old to prevent replay attacks.
import hmacimport hashlibimport timedef get_signing_secret(api_key: str) -> str:"""Your signing secret is SHA256 of your API key."""return hashlib.sha256(api_key.encode()).hexdigest()def verify_webhook(body: bytes, signature: str, timestamp: str, api_key: str) -> bool:# Check timestamp is recent (within 5 minutes)now = int(time.time())if abs(now - int(timestamp)) > 300:return False# Compute expected signature using SHA256(api_key) as secretsigning_secret = get_signing_secret(api_key)message = f"{timestamp}.{body.decode()}"expected = hmac.new(signing_secret.encode(),message.encode(),hashlib.sha256).hexdigest()return hmac.compare_digest(expected, signature)# In your webhook handler:# signature = request.headers["X-Scriptivox-Signature"]# timestamp = request.headers["X-Scriptivox-Timestamp"]# is_valid = verify_webhook(request.body, signature, timestamp, API_KEY)
Delivery behavior
Webhooks are delivered on a best-effort, fire-and-forget basis:
- No retries. Connection failures, timeouts, and non-2xx responses are all final — the event is dropped.
- One redirect followed. If your endpoint returns a 3xx response with a
Locationheader, the dispatcher re-POSTs the same body and signature/timestamp headers to the resolved URL exactly once. Onlyhttp(s)destinations are followed; the Location is re-parsed and rejected if it points anywhere else (open-redirect protection). - User-Agent on every request is
scriptivox-webhook/1. - Transcriptions complete normally regardless of webhook delivery status — webhook health does not affect billing or job state.
- We monitor our own delivery pipeline. A heartbeat fires on every successful 2xx response from a customer URL; if no webhook delivery succeeds for several hours, we get alerted internally. This catches systemic delivery problems on our side fast — but it does not retry your specific dropped events, which is why polling is still required.
For reliable delivery, treat webhooks as a latency optimization and poll GET /v1/transcribe/{id} as a fallback. Live API uptime is published at status.scriptivox.com.
Best practices
- Return 2xx quickly — Acknowledge the webhook fast and do any heavy work asynchronously. Slow responses risk timing out, and there are no retries.
- Always verify signatures — Check the HMAC-SHA256 signature against the raw body before trusting any payload.
- Check timestamps — Reject webhooks with
X-Scriptivox-Timestampolder than 5 minutes to prevent replay attacks. - Be idempotent — Each
transcription_idproduces oneprocessingevent followed by one terminalcompletedorfailed; deduplicate by(transcription_id, event)in case a redirected delivery results in a repeat POST. - Use HTTPS — Always use HTTPS for production webhook URLs. HTTP is allowed for local development only.
- Poll as a fallback — Since webhooks are never retried, also poll
GET /v1/transcribe/{id}so a single dropped delivery does not strand a job.