Scriptivox logoScriptivox

    Get started

    OverviewQuickstartPricing

    API Reference

    TranscribeFile UploadGet ResultBalanceError CodesRate LimitsFormatsLanguages

    Guides

    Webhooks

    Use Cases

    Folder Watcher
    Scriptivox logoScriptivoxAPI Documentation

    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.

    json
    {
    "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.

    json
    {
    "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.

    json
    {
    "event": "transcription.failed",
    "transcription_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
    "status": "failed",
    "error": {
    "code": "PROCESSING_ERROR",
    "message": "Failed to process audio file"
    }
    }

    Events summary

    EventWhenFlows
    transcription.processingFile validated, job submitted to GPUBoth
    transcription.completedTranscription finished successfullyBoth
    transcription.failedDownload, validation, or processing failedBoth

    Verifying webhook signatures

    Every webhook request includes two headers for signature verification:

    HeaderDescription
    X-Scriptivox-SignatureHMAC-SHA256 hex digest of the signed payload
    X-Scriptivox-TimestampUnix timestamp (seconds, as a string) of when the webhook was sent

    Signing formula

    1. signing_secret = SHA256_hex(api_key) — hex digest of your API key as a UTF-8 string
    2. signed_payload = f"{timestamp}.{body}" — body is the exact raw JSON request body, byte-for-byte
    3. signature = HMAC_SHA256_hex(signing_secret, signed_payload) — sent as X-Scriptivox-Signature

    Reject any request whose X-Scriptivox-Timestamp is more than 5 minutes old to prevent replay attacks.

    import hmac
    import hashlib
    import time
    def 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 secret
    signing_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 Location header, the dispatcher re-POSTs the same body and signature/timestamp headers to the resolved URL exactly once. Only http(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-Timestamp older than 5 minutes to prevent replay attacks.
    • Be idempotent — Each transcription_id produces one processing event followed by one terminal completed or failed; 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.

    API Reference

    Full endpoint documentation

    Pricing

    Pay-as-you-go at $0.20/hour