Gareth Wilson Gareth Wilson

Guide to Speechmatics Webhooks: Features and Best Practices

Published


Speechmatics webhooks, which its docs call notifications, tell you when a batch transcription job has finished. You submit an audio file, Speechmatics transcribes it in the background, and when the job is done it calls your URL, optionally with the transcript or the original media attached. That saves you from polling the jobs API and lets you hand the transcript straight to the next stage: storage, search, summaries or an LLM.

This guide covers how notifications are configured on each job, what a callback looks like, how to authenticate it with a header you set in auth_headers, how failed deliveries are retried, and the best practices for running it in production.

What are Speechmatics webhooks?

Speechmatics has no webhook settings page. Notifications belong to the batch job: you add a notification_config array to the job config when you submit it, and each entry names a URL, what to send, and any headers to send with it. A job can notify up to three URLs, and you can split the output across them, for example jobinfo to your application and the transcript to an object store.

There are no event types. Speechmatics appends two query parameters to your URL: id, the job ID, and status, the outcome of the job.

POST /webhooks/speechmatics?id=r6sr3jlzjj&status=success HTTP/1.1
Host: example.com
User-Agent: Speechmatics-API/2.0

Speechmatics doesn't sign notifications. It gives you two ways to check them: auth_headers, a list of literal Name: value strings that it adds to the request unchanged, and a published list of egress IP addresses that you can allowlist.

Speechmatics webhook features

FeatureDetails
ConfigurationPer job: notification_config in the batch job config
DestinationsUp to 3 URLs per job
TriggerThe job finishes, successfully or not
MethodPOST by default, or PUT via method
Outcomestatus query parameter: success, error, fetch_error or trim_error
Job referenceid query parameter
BodyWhatever contents you request: JSON, plain text, multipart or empty
Authenticationauth_headers: free-form Name: value strings, set per job
SignatureNone
Source IPsPublished per region: EU1, EU2, US1, US2 and AU1
RetriesAny non-2xx is retried until a timeout threshold the docs don't state
OrderingCallbacks run in parallel and may arrive in any order
Failure recorderrors array on GET /jobs/{id}

Notification outcomes

The status query parameter is the only thing that tells callbacks apart:

statusSent when
successThe job was processed successfully
errorSomething went wrong while processing the job. Only jobinfo is sent
fetch_errorSpeechmatics couldn't fetch the data file from the URL you gave it
trim_errorSpeechmatics couldn't process the audio file before transcription

These aren't the job status values that the jobs API returns (running, done and so on), so don't reuse the same enum for both.

What the body contains

The body is whatever you list in contents, and its format depends on how many items you ask for:

contents requestedContent typeBody
Exactly one of transcript, transcript.json-v2 or jobinfoapplication/jsonThe JSON item itself
Exactly one of transcript.txt or transcript.srttext/plainThe plain-text or SRT transcript
data, or two or more itemsmultipart/form-dataOne part per item
NothingNoneEmpty. Only id and status in the query string

transcript and transcript.json-v2 are the same JSON v2 transcript. jobinfo is the configuration the job was submitted with, which is also included in the JSON v2 transcript. data is the media file you submitted, so asking for it can make the callback very large.

A single item is sent as the raw request body, not wrapped in an envelope. That's deliberate, so that one callback can write straight to an object store such as Amazon S3, but it means your receiver can't assume a fixed JSON shape.

Inspect Speechmatics webhooks as they arrive. Point Speechmatics at a Hookdeck Inspect URL to inspect and replay real deliveries, with no account or setup required. It's a quick way to see how each contents choice changes the body.

Setting up Speechmatics webhooks

Pick the header your receiver will check and generate a long random value for it:

openssl rand -hex 32

Then add a notification_config entry to the job config. Each auth_headers entry is a single Name: value string:

{
  "type": "transcription",
  "transcription_config": { "language": "en" },
  "notification_config": [
    {
      "url": "https://example.com/webhooks/speechmatics",
      "contents": ["jobinfo"],
      "auth_headers": ["X-Speechmatics-Secret: <your random value>"]
    }
  ]
}
OptionRequiredNotes
urlYesWhere the notification is sent
contentsNotranscript, transcript.json-v2, transcript.txt, transcript.srt, data, jobinfo
auth_headersNoLiteral Name: value strings sent with the request
methodNoPOST (the default) or PUT

Authorization: Bearer <token> works as well as a custom header. Whichever you choose, the value is the credential your service accepts, not your Speechmatics API key, which should never leave your backend.

Because the configuration lives on the job, every job your code submits must carry it. A job submitted without auth_headers, from a script or a different service, sends notifications that your receiver will reject. Build the job config in one place so the header can't be forgotten.

If your network filters inbound traffic, allowlist the egress IP addresses that Speechmatics publishes for your region on its notifications page. Speechmatics presents this as its way to confirm that callbacks come from it. Treat it as an extra layer alongside the header check rather than a replacement.

For local development, use the Hookdeck CLI: hookdeck listen 3000 speechmatics --path /webhooks/speechmatics 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 the url in notification_config.

With Hookdeck. Create a Speechmatics source in Hookdeck and enter the API Key Header and API Key exactly as they appear in auth_headers. The value is the whole string after the colon, so for Authorization: Bearer abc123 the API Key is Bearer abc123, not abc123. The source accepts both POST and PUT, so it works whichever method the job sets. Use the source URL as the url in notification_config. Hookdeck accepts JSON, plain-text and multipart bodies, but its default ingestion limit is 10 MiB, so send transcripts and jobinfo through Hookdeck and leave data out of contents unless your media files are small.

Securing Speechmatics webhooks

Check the header before you read the body. Look it up case-insensitively, compare the whole value in constant time, and check lengths first because crypto.timingSafeEqual throws on buffers of different lengths. Accept both POST and PUT, and don't parse the body as JSON unless the content type says it is.

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 verifySpeechmaticsRequest(headers) {
  const secret = process.env.SPEECHMATICS_WEBHOOK_SECRET;
  if (!secret) return false; // fail closed when unconfigured
  const name = (process.env.SPEECHMATICS_WEBHOOK_HEADER || 'X-Speechmatics-Secret').toLowerCase();
  const received = headers[name];
  return typeof received === 'string' && safeEqual(received, secret);
}

function handleNotification(req, res) {
  if (!verifySpeechmaticsRequest(req.headers)) {
    return res.sendStatus(401);
  }

  const jobId = req.query.id;
  const status = req.query.status;
  const contentType = req.headers['content-type'] || '';

  let body = null;
  if (contentType.startsWith('application/json')) {
    try {
      body = JSON.parse(req.body.toString('utf8'));
    } catch {
      return res.sendStatus(400);
    }
  }

  res.sendStatus(200);

  // Queue the work: jobId, status, and either the parsed JSON or the raw
  // text or multipart buffer in req.body, depending on contentType.
}

const rawBody = express.raw({ type: '*/*', limit: '50mb' });
app.post('/webhooks/speechmatics', rawBody, handleNotification);
app.put('/webhooks/speechmatics', rawBody, handleNotification);

Set the body size limit to match what you request. A transcript is small, but data sends the original media back, and a limit that's too low turns every callback into a 413 that Speechmatics keeps retrying.

The same check in Python:

import hmac
import os


def verify_speechmatics_request(headers) -> bool:
    secret = os.environ.get("SPEECHMATICS_WEBHOOK_SECRET")
    if not secret:
        return False
    name = os.environ.get("SPEECHMATICS_WEBHOOK_HEADER", "X-Speechmatics-Secret")
    received = headers.get(name)  # framework header maps are case-insensitive
    if not received:
        return False
    return hmac.compare_digest(received.encode("utf-8"), secret.encode("utf-8"))

A static header authenticates the sender, but it doesn't protect the body, and nothing stops a captured request from being replayed, because nothing is signed or timestamped. Use HTTPS for the notification URL and keep the value out of logs. Since the header is set per job, rotating it means submitting new jobs with the new value while still accepting the old one until jobs already in progress have called back.

Make Speechmatics webhooks production-ready. Hookdeck Event Gateway checks your header on every notification, stores each one durably, and retries delivery to your service on a schedule you control.

Speechmatics webhook limitations and pain points

An unstated retry window

The Problem: You can't tell how long Speechmatics will keep trying if your receiver is down, so you can't tell when a notification is lost.

Why It Happens: The docs say Speechmatics retries any non-2xx response until it reaches "the set timeout threshold", without giving the threshold or the interval. The failure example on the same page shows a few attempts over about a minute before it gives up. When it does, the transcript is still available from the API and the failures are listed in an errors array on GET /jobs/{id}.

Workarounds:

  • Return a 2xx as soon as the notification is authenticated and stored.
  • Run a periodic check for recent jobs that finished without a callback, and fetch their results from the API.

How Hookdeck Can Help: Hookdeck answers Speechmatics as soon as the notification is stored, then delivers it to your service with automatic retries on your own schedule. Issues alert you when deliveries fail, and you can replay any stored notification without resubmitting the job.

Errors are retried too

The Problem: A receiver that rejects a callback still receives it again and again.

Why It Happens: Speechmatics treats every non-2xx response as a failed delivery, including 401 and 413. A wrong header value or a body size limit set too low produces a run of retries rather than a single clear failure.

Workarounds:

  • Raise the body size limit to fit the largest item you request, or stop requesting data if you don't need the media back.
  • Watch for repeated 401s after you change the header value, since every job submitted before the change still carries the old one.

How Hookdeck Can Help: Hookdeck's Speechmatics source compares the whole header value with the one you paste. A request without a matching header 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 headers that actually arrived, which makes a missing or mistyped auth_headers entry easy to spot.

No fixed payload shape

The Problem: The same endpoint can receive JSON, plain text, multipart form data or an empty body.

Why It Happens: The body is whatever contents the job requested, sent raw when there's one item and as multipart when there are several. There's no envelope and no event type field.

Workarounds:

  • Standardise on one contents setting per endpoint, for example ["jobinfo"] for your application and a separate notification for the transcript.
  • Branch on Content-Type before parsing, and take the outcome from the status query parameter rather than from the body.

How Hookdeck Can Help: Hookdeck stores every notification as it arrived, with its query string, headers and body, so you can see exactly what a job sent. Filters can route on the status query parameter, and transformations can reshape JSON bodies before they reach your service.

Callbacks arrive in any order

The Problem: Downstream steps that depend on several callbacks from one job run before all of them have arrived.

Why It Happens: A job can send several notifications, and Speechmatics runs them in parallel. They can complete in any order, and some may succeed while others fail.

Workarounds:

  • Key each callback by job ID and content, and only start the dependent step once everything you need is present.
  • Fall back to the API for any piece that hasn't arrived.

How Hookdeck Can Help: A max delivery rate on the destination smooths bursts when many jobs finish at once, and every stored callback can be replayed if a downstream step needs to run again.

Best practices

Request only what you need

Ask for jobinfo or a single transcript format in the callback, and fetch anything else from the API. Leave data out unless you want the media file posted back to you.

Keep the header in one place

Generate the job config from a single function or template so that every job carries auth_headers, and so that rotating the value is a one-line change.

Handle every outcome

Route on the status query parameter. error, fetch_error and trim_error are real outcomes: record them, check the job's errors from the API, and decide whether to resubmit.

Make handlers idempotent

Retries can deliver the same notification more than once. Record the job ID with the status, plus the content type if a job sends several notifications to one endpoint, and skip repeats. Hookdeck's deduplication can drop exact duplicates before they reach you. See our guide to webhook idempotency.

Acknowledge fast

Return a 2xx before writing large files or calling an LLM, and do that work in the background. See why to process webhooks asynchronously, and for AI pipelines, Hookdeck for AI agents and LLMs.

Conclusion

Speechmatics sends one unsigned notification per configured URL when a batch job finishes, with the job ID and outcome in the query string and whatever contents you asked for in the body. Authenticate it by comparing the whole header value you set in auth_headers, accept both POST and PUT, branch on Content-Type before parsing, and return a 2xx quickly, because every other response is retried until a timeout the docs don't state.

Hookdeck Event Gateway checks the header, stores every notification and retries delivery to your service, so a finished transcript isn't lost to an outage on your side.

Get started with Hookdeck for free and handle Speechmatics webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.