# Guide to Rev.ai Webhooks: Features and Best Practices

Rev.ai webhooks tell you when an asynchronous transcription job is done. You submit an audio or video file, Rev.ai transcribes it in the background, and when the job finishes or fails it calls your URL so you can fetch the transcript and move on: summarise it, index it, or show it to a user. Rev AI, from Rev, calls these callbacks webhooks.

This guide covers how Rev.ai's per-job webhooks are configured, what the callback contains, how to authenticate it with the `Authorization` header you set on the job, the retry behaviour, and the best practices for production.

## What are Rev.ai webhooks?

Rev.ai has no webhook settings page. Instead, each asynchronous job can carry a `notification_config` object with a callback `url` and, optionally, `auth_headers`. When the job either completes or fails, Rev.ai sends a `POST` to that URL with the job's details in the body.

There are no event types. Every callback is about one job, and `job.status` tells you whether it finished (`transcribed`) or not (`failed`). `job.id` ties the callback back to the job you submitted.

Rev.ai doesn't sign callbacks. Its authentication option is `auth_headers`, which has two constraints: `Authorization` is the only header allowed, and its value must take the form `<scheme> <token>`, for example `Bearer <token>`. Rev.ai sends that header with the callback, and your receiver compares it.

An older `callback_url` parameter sends the same callback without any auth headers. It's deprecated, can't be combined with `notification_config`, and should be avoided.

## Rev.ai webhook features

| Feature | Details |
| --- | --- |
| Configuration | Per job: `notification_config` on `POST /speechtotext/v1/jobs` |
| Trigger | The job completes or fails |
| Method | `POST` with a JSON body |
| Payload | `{ "job": { ... } }`: a job summary |
| Event types | None. `job.status` is `transcribed` or `failed` |
| Authentication | `auth_headers` with `Authorization: <scheme> <token>` only |
| Signature | None |
| Success | A `200` response |
| Retries | Every 30 minutes until a `200` or 24 hours have passed |
| Deprecated option | `callback_url`, with no auth headers |
| Source IPs | Not published |

## The callback payload

A successful job produces a callback like this:

```json
{
  "job": {
    "id": "Umx5c6F7pH7r",
    "status": "transcribed",
    "created_on": "2018-05-05T23:23:22.29Z",
    "duration_seconds": 356.24
  }
}

```

Rev.ai's own example of this body has a trailing comma after `duration_seconds`, so don't copy it into a test fixture as-is. A failed job looks like this:

```json
{
  "job": {
    "id": "Umx5c6F7pH7r",
    "status": "failed",
    "created_on": "2018-05-05T23:23:22.29Z",
    "failure": "download_failure",
    "failure_detail": "Failed to download media file. Please check your url and file type"
  }
}

```

The callback doesn't include the transcript. On `transcribed`, fetch it with `GET https://api.rev.ai/speechtotext/v1/jobs/{id}/transcript` using your access token. On `failed`, `failure` gives a machine-readable reason and `failure_detail` a human-readable one.

> **See Rev.ai webhook payloads in action.** Inspect and replay sample Rev.ai webhook payloads in [Hookdeck Inspect](https://inspect.hookdeck.com/webhooks/rev-ai), with no account or setup required.

## Setting up Rev.ai webhooks

Choose the `Authorization` value your receiver will accept. It needs a scheme, so generate something like `Bearer` followed by a long random token, and store the whole string in your app's environment:

```bash
REV_AI_AUTHORIZATION="Bearer $(openssl rand -hex 32)"

```

Then pass it in `notification_config` on every job you submit:

```bash
curl -X POST https://api.rev.ai/speechtotext/v1/jobs \
  -H "Authorization: Bearer $REV_AI_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"source_config\": { \"url\": \"https://www.rev.ai/FTC_Sample_1.mp3\" },
    \"notification_config\": {
      \"url\": \"https://example.com/webhooks/rev-ai\",
      \"auth_headers\": { \"Authorization\": \"$REV_AI_AUTHORIZATION\" }
    },
    \"metadata\": \"order-1234\"
  }"

```

Don't confuse the two `Authorization` values. The one on the request authenticates you to Rev.ai with your access token. The one inside `auth_headers` is the credential Rev.ai sends to your callback URL, and should never be your Rev.ai access token. `notification_config` isn't echoed back in the submission response, so keep track of the URL and credential in your own code.

For local development, use the [Hookdeck CLI](/docs/cli): `hookdeck listen 3000 rev-ai --path /webhooks/rev-ai` gives you a public HTTPS URL that forwards to your local server, plus a web UI for inspecting and replaying deliveries, with no account required. Use the generated URL as `notification_config.url`.

**With Hookdeck.** Create a Rev.ai source in Hookdeck and paste the full `Authorization` value from `auth_headers` into **Authorization Header**, including the `Bearer ` prefix. Hookdeck compares the whole header, so `abc123` won't match an incoming `Bearer abc123`. Use the source URL as `notification_config.url`.

## Securing Rev.ai webhooks

Compare the incoming `Authorization` header with the full value you set, scheme included, in constant time. Check lengths first, because `crypto.timingSafeEqual` throws on buffers of different lengths. Return `200` only when you've accepted the callback, since that's what stops Rev.ai's retries.

```javascript
const crypto = require('crypto');
const express = require('express');

const app = express();

function safeEqual(a, b) {
  const x = Buffer.from(String(a), 'utf8');
  const y = Buffer.from(String(b), 'utf8');
  return x.length === y.length && crypto.timingSafeEqual(x, y);
}

function verifyRevAiRequest(authorization) {
  const expected = process.env.REV_AI_AUTHORIZATION; // e.g. "Bearer 3f9c..."
  if (!expected || typeof authorization !== 'string') return false;
  return safeEqual(authorization, expected); // whole value, scheme included
}

app.post('/webhooks/rev-ai', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyRevAiRequest(req.headers.authorization)) {
    return res.sendStatus(401);
  }

  let body;
  try {
    body = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  const job = body.job || {};
  res.sendStatus(200); // a 200 stops the retries

  if (job.status === 'transcribed') {
    // queue a job to GET /jobs/{job.id}/transcript
  } else if (job.status === 'failed') {
    // record job.failure and job.failure_detail
  }
});

```

The same check in Python:

```python
import hmac
import os

def verify_rev_ai_request(authorization) -> bool:
    expected = os.environ.get("REV_AI_AUTHORIZATION")
    if not expected or not authorization:
        return False
    return hmac.compare_digest(authorization.encode("utf-8"), expected.encode("utf-8"))

```

A static `Authorization` value authenticates the sender, but it doesn't protect the body, and a captured request can be replayed because nothing is signed or timestamped. Use an HTTPS callback URL and keep the value out of logs. Because the credential is set per job, rotation means submitting new jobs with a new value while accepting the old one until jobs already in flight have called back.

> **Make Rev.ai webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the `Authorization` header on every callback, stores each one durably, and keeps retrying delivery to your service well past Rev.ai's 24-hour window.

## Rev.ai webhook limitations and pain points

### A 24-hour retry window

**The Problem:** If your receiver is down or returning errors for more than a day, the callback is lost, and the only way to find out a job finished is to poll for it.

**Why It Happens:** Rev.ai retries every 30 minutes until it gets a `200` or 24 hours have passed. Anything other than a `200` counts as a failure.

**Workarounds:**

* Return `200` as soon as the callback is authenticated and stored.
* Run a periodic job that lists recent jobs and fetches any you haven't seen a callback for.

**How Hookdeck Can Help:** Hookdeck answers Rev.ai with a `200` as soon as the callback is stored, then delivers to your service with [automatic retries](/docs/retries) on a schedule you control. [Issues](/docs/issues) alert you when deliveries fail, and you can replay any stored callback without resubmitting the job.

### One header, one format

**The Problem:** Authentication fails even though the token is right.

**Why It Happens:** Rev.ai only allows the `Authorization` header in `auth_headers`, and its value must include a scheme. A receiver that stores only the token, or strips the `Bearer ` prefix from one side and not the other, never matches.

**Workarounds:**

* Store the whole value, scheme included, and compare it as-is.
* Keep the scheme and token in one environment variable so the two can't drift.

**How Hookdeck Can Help:** Hookdeck's Rev.ai source compares the whole `Authorization` header with the value you paste. A request that doesn't match still gets a 200 response, but Hookdeck marks it as failed verification and creates no event, so it never reaches your destination. The stored request shows the exact header that arrived, which makes a prefix mismatch easy to spot.

### No signature, and a deprecated unauthenticated option

**The Problem:** Anyone who knows the callback URL and credential can post a fake result, and jobs submitted with `callback_url` carry no credential at all.

**Why It Happens:** Rev.ai's only callback authentication is the static `Authorization` header, and the older `callback_url` predates it.

**Workarounds:**

* Always use `notification_config` with `auth_headers`.
* Before acting on a callback, fetch the job by `job.id` and confirm its status through the API.

**How Hookdeck Can Help:** Every callback is stored with its headers and body, so you can audit which jobs called back, and with what, before your handler acts.

## Best practices

### Use the callback as a trigger, not the data

The body is a job summary. Fetch the transcript, or other job details, from the API using `job.id`.

### Tag jobs with your own identifiers

Pass `metadata` when you submit the job, and store the returned job ID against your own record, so a callback can be matched to the right upload or user.

### Handle both outcomes

Route on `job.status`. Treat `failed` as a real event: record `failure` and `failure_detail`, and decide whether to resubmit with a corrected media URL.

### Make handlers idempotent

Retries can deliver the same callback more than once. Record `job.id` with its status and skip repeats. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Acknowledge fast

Return `200` before downloading transcripts or calling an LLM, and do that work in the background. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks), and for AI pipelines, [Hookdeck for AI agents and LLMs](/docs/use-cases/ai-agents).

## Conclusion

Rev.ai sends one unsigned `POST` per asynchronous job when it completes or fails, configured per job through `notification_config`. Authenticate it by comparing the whole `Authorization` value, scheme included, in constant time, route on `job.status`, fetch the transcript by `job.id`, and return `200` quickly because anything else is retried for only 24 hours.

[Hookdeck Event Gateway](/event-gateway) checks the `Authorization` header, stores every callback and retries delivery to your service, so a finished transcript is never lost to an outage on your side.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Rev.ai webhooks reliably in minutes.