# Speechmatics Webhooks

Speechmatics calls the URL in a batch job's `notification_config` when the job finishes. The
request is **not signed**: you authenticate it with a header you add per job through
`auth_headers`. The job ID and outcome arrive as query parameters, and the body is whatever
`contents` you requested (JSON, plain text, or multipart), or empty.

## When to Use This Skill

* How do I receive Speechmatics webhooks (batch job notifications)?
* How do I verify a Speechmatics webhook? (There's no signature: check your `auth_headers` value)
* How do I use `notification_config` and `auth_headers`?
* How do I handle the `success`, `error`, `fetch_error` and `trim_error` statuses?
* Why is my Speechmatics callback endpoint returning 401 or 413 and being retried?

## Verification (core)

Add a secret header to the job, e.g. `"auth_headers": ["X-Speechmatics-Secret: <secret>"]`
(`Authorization: Bearer <token>` works too). Speechmatics sends it verbatim. Compare the
**whole** header value in constant time, without parsing off a `Bearer ` prefix:

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

function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest(); // equal length: never throws
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifySpeechmaticsRequest(headers, config = process.env) {
  const name = (config.SPEECHMATICS_WEBHOOK_HEADER || 'Authorization').toLowerCase();
  const expected = config.SPEECHMATICS_WEBHOOK_SECRET; // the exact value, e.g. "Bearer abc123"
  if (!expected) return false;                         // never accept when unconfigured
  const received = headers[name];                      // Node lowercases header names
  return typeof received === 'string' && safeEqual(received, expected);
}

```

Return `401` when this fails. **A static shared credential authenticates the sender, not the
body, and gives no replay protection** (no signature, no timestamp), so serve the endpoint over
HTTPS and rotate the credential if it leaks. The vendor's own origin check is an
[IP allowlist](https://github.com/hookdeck/webhook-skills/blob/main/skills/speechmatics-webhooks/references/setup.md#optional-allowlist-the-egress-ips), an optional extra.

The body isn't signed, so don't assume JSON: it can be JSON, plain text, multipart, or empty.
Authenticate before reading it (Express: `express.raw({ type: '*/*' })` after the check).

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/speechmatics-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/speechmatics-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/speechmatics-webhooks/examples/fastapi/).

## Event Types

There is no event-type field. Speechmatics appends `?id=<job id>&status=<status>` to your URL
(for example `POST /webhooks/speechmatics?id=r6sr3jlzjj&status=success`):

| `status` | Sent When |
| --- | --- |
| `success` | The job was processed successfully |
| `error` | Something went wrong processing the job (outputs that aren't available are left out) |
| `fetch_error` | Speechmatics failed to fetch the data file from a URL |
| `trim_error` | Speechmatics failed to process the audio file before transcription |

These are not the same as the job status values (`running`, `done`, ...). The user agent is
`Speechmatics-API/2.0`.

## Body by `contents`

| Requested `contents` | Body |
| --- | --- |
| one of `transcript`, `transcript.json-v2`, `jobinfo` | `application/json` |
| one of `transcript.srt`, `transcript.txt` | `text/plain` |
| two or more items | `multipart/form-data` |
| `data` alone | `multipart/form-data` per the notifications page; a raw body such as `application/octet-stream` per the API reference, so branch on `Content-Type` |
| `[]` (empty list) | empty: just the job ID and status in the query |
| field left out | the API reference says the transcript, as a `data_file` attachment; the notifications page says no body, so always set `contents` |

## Delivery Behavior

The method is `POST` unless the job sets `"method": "put"`, so accept both. Return a 2xx once
received: Speechmatics keeps retrying any other response (including 401 and 413) until its
timeout, so make sure your body size limit fits the media you request. Callbacks run in
parallel and may arrive in any order.

## Environment Variables

```bash
SPEECHMATICS_WEBHOOK_HEADER=Authorization                    # the header name you put in auth_headers
SPEECHMATICS_WEBHOOK_SECRET="Bearer replace-with-a-long-random-token"  # the exact whole value

```

## Local Development

Speechmatics can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

```bash
npx hookdeck-cli listen 3000 speechmatics --path /webhooks/speechmatics

```

Use the printed URL as the `url` in `notification_config`.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/speechmatics-webhooks/references/overview.md) - How notifications work, statuses, body formats, retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/speechmatics-webhooks/references/setup.md) - Configure `notification_config` and `auth_headers` on a job
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/speechmatics-webhooks/references/verification.md) - Header check, limits of a static credential, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/speechmatics-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests