Skip to content

Webhooks ​

Receive signed audio processing events and track or retry their delivery.

Authentication and availability ​

Every endpoint on this page requires a server-side API key with {"webhooks":"access"}, equivalent to webhooks:write. This scope is required for reads too; there is no separate webhooks:read scope. Subscriptions and delivery history belong to the account, not an individual key.

Requests return 503 webhooks_unavailable when webhooks are disabled. TTS emits completion events. The STT event types listed below apply only to the older transcription pipeline. Current /v1/stt jobs require polling; they do not emit webhook events.

Create a subscription ​

POSThttps://api.voicelab.uz/v1/webhooks

Create an account-owned webhook and return its signing secret once.

bash
curl --fail-with-body 'https://api.voicelab.uz/v1/webhooks' \
  -H "Authorization: Bearer $VOICELAB_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://your-app.example/webhooks/voicelab","event_types":["tts.generation.completed"]}'

Replace the example URL with your HTTPS receiver. The URL must use a public DNS hostname and port 443, with no credentials or fragment. IP literals, localhost, private network destinations and redirects are not supported. URLs are limited to 2,048 characters. JSON request bodies are limited to 16 KiB; unknown fields are rejected.

event_types must contain one to four distinct supported values:

Eventdata fields
tts.generation.completedobject: "tts_generation", id, status: "completed", duration_ms, credits_used
stt.transcription.queuedobject: "transcription", id, status: "queued", duration_ms
stt.transcription.completedobject: "transcription", id, status: "completed", language, duration_ms
stt.transcription.failedobject: "transcription", id, status: "failed", error_code

Each account can have up to 20 active subscriptions. 201 Created returns:

json
{
  "webhook": {
    "id": "wh_EXAMPLE",
    "url": "https://your-app.example/webhooks/voicelab",
    "event_types": ["tts.generation.completed"],
    "secret_suffix": "abcd",
    "enabled": true,
    "revision": 1,
    "created_at": "2026-10-01T08:00:00Z",
    "updated_at": "2026-10-01T08:00:00Z",
    "disabled_at": null
  },
  "secret": "whsec_EXAMPLEabcd",
  "request_id": "req_EXAMPLE"
}

Store secret securely. It is returned only on creation and rotation, never in normal reads. It is a signing secret, not an API key. Creation has no idempotency-key support. If the response is lost, check the subscription list before creating another one.

List subscriptions ​

GEThttps://api.voicelab.uz/v1/webhooks

List account-owned webhook subscriptions.

Accepts page from 1–100000 and page_size from 1–100, defaulting to 1 and 50. Unknown or repeated query parameters are rejected. Returns 200 with webhooks, pagination, and request_id. Pagination contains total, total_pages, page, page_size, has_next, has_previous, next_page, and previous_page; unavailable next/previous pages are null.

Get a subscription ​

GEThttps://api.voicelab.uz/v1/webhooks/{id}

Read a webhook's configuration and current revision.

Returns 200 with webhook and request_id, without secret. No query parameters are accepted.

Update a subscription ​

PATCHhttps://api.voicelab.uz/v1/webhooks/{id}

Change a webhook URL, event types or enabled state.

Send at least one of url, event_types, or enabled. Omitted fields remain unchanged. event_types replaces the complete list and cannot be empty or null. Include the last observed expected_revision to prevent overwriting concurrent changes:

json
{"enabled":false,"expected_revision":1}

Returns 200 with webhook and request_id. A stale revision returns 409 webhook_revision_conflict; fetch the latest configuration before retrying. Disabling cancels pending/retrying deliveries. A delivery already in flight may still reach your receiver. Re-enabling does not replay canceled deliveries.

Delete a subscription ​

DELETEhttps://api.voicelab.uz/v1/webhooks/{id}

Disable a subscription and cancel pending/retrying deliveries.

Send a JSON object, preferably {"expected_revision":2} using the current revision, or {} to omit the revision check. An empty body is invalid. Success is 204 No Content. This operation disables the subscription; it does not erase its metadata or delivery history. In-flight delivery can still finish.

Rotate the signing secret ​

POSThttps://api.voicelab.uz/v1/webhooks/{id}/secret/rotate

Issue a new signing secret for future deliveries.

No body or query parameters are needed. Returns 200 with webhook, the new secret, and request_id. Deliveries retain the URL and secret captured when they were enqueued. Keep the previous secret available in your receiver until old pending/retrying deliveries are resolved. URL changes also apply only to newly queued deliveries.

Send a test event ​

POSThttps://api.voicelab.uz/v1/webhooks/{id}/test

Queue a test callback for an enabled subscription.

No body or query parameters are needed. Returns 202 with delivery and request_id. This confirms queueing, not successful delivery. The callback type is webhook.test, with data: {"object":"webhook_test"}. Do not include this test type in the subscription's event_types list.

List deliveries ​

GEThttps://api.voicelab.uz/v1/webhooks/{id}/deliveries

Inspect delivery status and response metadata.

Accepts the same page and page_size parameters as subscriptions. Returns 200 with deliveries, pagination, and request_id. Each delivery contains id, event_id, event_type, status, attempt_count, response_status, optional error_code, created_at, updated_at, delivered_at, and next_attempt_at. Pending response/timestamp values may be null. States are pending, delivering, retrying, succeeded, failed, or canceled. Receiver response bodies and event payloads are not exposed in this list.

Retry a delivery ​

POSThttps://api.voicelab.uz/v1/webhooks/{id}/deliveries/{delivery_id}/retry

Queue another attempt for a failed delivery on an enabled subscription.

No body or query parameters are needed. Returns 202 with delivery and request_id. Only failed deliveries on enabled subscriptions can be retried; otherwise the response is 409 webhook_delivery_not_retryable. Retrying keeps the same event/delivery IDs, captured URL/secret, and attempt count.

Verify callbacks ​

Callbacks are JSON POST requests. They contain resource metadata only, never transcript text, synthesis text, audio bytes, API keys, or signed audio URLs. After verification, retrieve results through the authenticated resource endpoint.

json
{
  "id": "whevt_EXAMPLE",
  "type": "tts.generation.completed",
  "created_at": "2026-10-01T08:00:00Z",
  "api_version": "2026-08-22",
  "data": {"object":"tts_generation","id":"gen_EXAMPLE","status":"completed","duration_ms":1180,"credits_used":24}
}

Headers include X-VoiceLab-Event, X-VoiceLab-Event-ID, X-VoiceLab-Delivery-ID, X-VoiceLab-Timestamp and X-VoiceLab-Signature. The timestamp is Unix seconds. The signature is v1= followed by the lowercase hex HMAC-SHA256 of timestamp + "." + rawBody, keyed by the literal signing secret, including its whsec_ prefix. Do not decode that secret or reserialize the body before verification.

This Node.js verifier accepts a five-minute timestamp window as a receiver policy. Keep your server clock synchronized. Pass the raw request bytes before any JSON body parser, and header values from the names above.

js
import { createHmac, timingSafeEqual } from "node:crypto";

function verifyWebhook(rawBody, timestamp, signature, secrets, now = Date.now()) {
  if (!Buffer.isBuffer(rawBody) || typeof timestamp !== "string" ||
      !/^\d+$/.test(timestamp) || typeof signature !== "string" ||
      !/^v1=[0-9a-f]{64}$/.test(signature)) return false;
  const seconds = Number(timestamp);
  if (!Number.isSafeInteger(seconds) ||
      Math.abs(Math.floor(now / 1000) - seconds) > 300) return false;
  const supplied = Buffer.from(signature.slice(3), "hex");
  return secrets.some(secret => {
    const expected = createHmac("sha256", secret)
      .update(timestamp + ".").update(rawBody).digest();
    return timingSafeEqual(expected, supplied);
  });
}

Reject invalid signatures before parsing or processing the payload. After verification, deduplicate using the signed body's event id, durably enqueue your work, and return a 2xx response promptly. The receiver timeout is ten seconds. Delivery is at least once; handle duplicates and do not depend on order.

Network failures, 408, 409, 425, 429, and 5xx responses are retried, up to eight attempts. Backoff is 5 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 3 hours, then 12 hours. Other non-2xx responses fail without automatic retry. Redirects are not followed. Delivery timestamps/signatures are renewed per attempt; event IDs remain stable.

Errors ​

HTTPCodeRecovery
400invalid_json, invalid_filter, invalid_paginationCorrect the body or query
404webhook_not_foundCheck account ownership and resource IDs
409webhook_limit_reachedDisable an unused subscription
409webhook_revision_conflictFetch the latest revision
409webhook_delivery_not_retryableInspect delivery and subscription states
422validation_errorCheck the URL, event list and editable fields
503webhooks_unavailableCheck feature availability; use resource polling while unavailable

Authentication errors follow the common API rules.