Agent skill

Speechmatics Webhooks Skill

Receive and authenticate Speechmatics batch job notifications (webhooks). Use when setting up a Speechmatics notification_config callback, checking the auth_headers value, debugging 401s on deliveries, or handling the success, error, fetch_error and trim_error job statuses.

Install this skill

npx skills add hookdeck/webhook-skills --skill 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:

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, 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/, examples/nextjs/, 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):

statusSent When
successThe job was processed successfully
errorSomething went wrong processing the job (outputs that aren't available are left out)
fetch_errorSpeechmatics failed to fetch the data file from a URL
trim_errorSpeechmatics 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 contentsBody
one of transcript, transcript.json-v2, jobinfoapplication/json
one of transcript.srt, transcript.txttext/plain
two or more itemsmultipart/form-data
data alonemultipart/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 outthe 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

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):

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

Use the printed URL as the url in notification_config.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 5, 2026

View on GitHub →