> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vook.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Get notified when a transcription job finishes instead of polling.

A webhook lets Vook tell your server when a job is done, so you do not need to
poll. You pass a `callback_url` when you create the job, and Vook sends one
signed request to that endpoint when the job reaches `completed` or `failed`.

## Subscribe a job

Add `callback_url` to the body of `POST /api/v1/transcription-jobs`, whether
you [transcribe from a file](/upload) or [from a URL](/transcribe-url).

```json theme={null}
{
  "url": "https://example.com/recordings/meeting.mp3",
  "language": "en",
  "diarize": false,
  "callback_url": "https://example.com/vook/webhook"
}
```

The endpoint must be public https on a standard port, up to 2048 characters.
The job echoes it back as `callback_url` when you read the job, and returns
`null` there for jobs created without one.

## When it fires

Vook notifies the endpoint **once per job**, when the job reaches a terminal
status (`completed` or `failed`). The event is `transcription_job.resolved`.

## What your endpoint receives

Vook sends a `POST` request with a JSON body:

```json theme={null}
{
  "event": "transcription_job.resolved",
  "delivered_at": "2024-01-15T10:35:00.000Z",
  "data": {
    "id": "123e4567-e89b-12d3-a456-426614174000",
    "status": "completed",
    "name": "Team weekly sync",
    "language": "en",
    "diarize": false,
    "transcription_id": "123e4567-e89b-12d3-a456-426614174000",
    "callback_url": "https://example.com/vook/webhook",
    "error_code": null,
    "created_at": "2024-01-15T10:30:00.000Z",
    "updated_at": "2024-01-15T10:35:00.000Z"
  }
}
```

| Field | Description |
| - | - |
| `event` | Always `transcription_job.resolved`. |
| `delivered_at` | When Vook sent the request, as an ISO 8601 timestamp. |
| `data` | The job, in the same shape `GET /api/v1/transcription-jobs/{id}` returns. Check `data.status` for `completed` or `failed`. |

The body does not include the transcript itself. When `data.status` is
`completed`, fetch it with
`GET /api/v1/transcription-jobs/{id}/transcript` using your API key.

The request carries these headers:

| Header | Description |
| - | - |
| `Content-Type` | `application/json`. |
| `X-Vook-Signature` | Signature of the request, as `v1=<hex>`. May hold several comma-separated values. |
| `X-Vook-Timestamp` | When the request was signed, in Unix seconds. |

## Verify the signature

Check the signature on every request before you trust its body. It proves the
request comes from Vook and that the body was not changed on the way.

Each API key has its own **webhook signing secret**, shown once when you create
the key (see [Authentication](/authentication#mint-a-key)). Deliveries for a job
are signed with the secret of the key that created the job.

To verify a request:

1. Read the **raw request body** as bytes, before any JSON parsing. Parsing and
   re-serializing changes the bytes and breaks the signature.
2. Build the signed string: the `X-Vook-Timestamp` value, a period (`.`), then
   the raw body.
3. Compute an HMAC-SHA256 of that string, using your signing secret as the key,
   and encode the result as lowercase hex.
4. Split `X-Vook-Signature` on commas. Each part has the form `v1=<hex>`. Accept
   the request if **any** `v1` value matches your result. The header can carry
   more than one value while Vook rotates its signing keys.
5. Compare the timestamp to your clock and reject requests older than a
   tolerance you choose, such as five minutes. The timestamp is part of the
   signed string, so this blocks replays of a captured request.

```python Python theme={null}
import hashlib
import hmac
import time

def verify_vook_webhook(raw_body: bytes, headers, secret: str, tolerance_s: int = 300) -> bool:
    timestamp = headers["X-Vook-Timestamp"]
    if abs(time.time() - int(timestamp)) > tolerance_s:
        return False

    signed = timestamp.encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()

    for part in headers["X-Vook-Signature"].split(","):
        scheme, _, value = part.strip().partition("=")
        if scheme == "v1" and hmac.compare_digest(value, expected):
            return True
    return False
```

Use a constant-time comparison such as `hmac.compare_digest`, as above. Answer
`400` or `401` when verification fails, and do not act on the body.

<Tip>
  `POST /api/v1/webhooks/test` sends a sample signed with the same secret, so you
  can check your verification code before you send any audio. See
  [Test your endpoint](#test-your-endpoint).
</Tip>

## Respond and retries

Answer with any `2xx` status within **10 seconds** to acknowledge the delivery.
Vook does not follow redirects, so a `3xx` answer counts as not delivered.

If the delivery does not succeed, Vook tries again. There are **three attempts
in total**: when the job finishes, 10 minutes later, and one hour after that.
After the third attempt, no further requests are sent for that job.

Polling stays the source of truth. If your endpoint missed every attempt, read
the job with `GET /api/v1/transcription-jobs/{id}` to get its final status.

## Test your endpoint

Before you send any audio, check that your endpoint receives and accepts a
request. `POST /api/v1/webhooks/test` runs the same `callback_url` checks as job
creation, delivers a signed sample `transcription_job.resolved` payload, and
returns the outcome. It starts no work and costs nothing.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST "https://www.api.vook.ai/api/v1/webhooks/test" \
    -H "Authorization: Bearer $VOOK_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"callback_url": "https://example.com/vook/webhook"}'
  ```

  ```python Python theme={null}
  import os, requests

  BASE = "https://www.api.vook.ai/api/v1"
  headers = {"Authorization": f"Bearer {os.environ['VOOK_API_KEY']}"}

  resp = requests.post(
      f"{BASE}/webhooks/test",
      headers=headers,
      json={"callback_url": "https://example.com/vook/webhook"},
  )
  resp.raise_for_status()
  print(resp.json())
  ```
</CodeGroup>

```json theme={null}
{
  "delivered": true,
  "status_code": 200,
  "duration_ms": 143,
  "error": null
}
```

| Field | Type | Description |
| - | - | - |
| `delivered` | boolean | `true` when your endpoint answered with a `2xx` status. |
| `status_code` | number or `null` | HTTP status your endpoint answered, or `null` if the request could not reach it. |
| `duration_ms` | number | Round-trip time in milliseconds. |
| `error` | string or `null` | Why the request could not reach your endpoint, or `null` when it answered. |

A `callback_url` that is not public https on a standard port returns `400` with
the error code `invalid_callback_url`.

## Next steps

<CardGroup cols={2}>
  <Card title="Transcribe from a file" icon="cloud-arrow-up" href="/upload">
    Upload a file and create a job with a `callback_url`.
  </Card>

  <Card title="Transcribe from a URL" icon="link" href="/transcribe-url">
    Create a job from a hosted file in one call.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.