Agent skill
Soniox Webhooks Skill
Receive and authenticate Soniox webhooks. Use when setting up a Soniox async transcription webhook endpoint, checking the webhook_auth_header_name / webhook_auth_header_value header, debugging 401s, or handling transcription completed and error statuses.
Install this skill
npx skills add hookdeck/webhook-skills --skill soniox-webhooks
Soniox sends a webhook when an async speech-to-text transcription finishes. Requests are not signed. You choose a secret header when you create each transcription (webhook_auth_header_name and webhook_), Soniox sends that header on the webhook request, and your endpoint authenticates the request by comparing it with the value you stored.
Soniox webhooks are not signed. Send webhook_auth_header_name and webhook_auth_header_value with every async transcription, and enter the same name and value here.
When to Use This Skill
- How do I receive Soniox webhooks?
- How do I authenticate Soniox async transcription webhooks?
- Does Soniox sign webhook payloads? (No: use the custom header)
- How do I set
webhook_auth_header_nameandwebhook_?auth_ header_ value - How do I handle a transcription with status
completedorerror? - Why does my Soniox webhook endpoint return 401?
Verification (core)
const crypto = require('crypto');
// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
const ha = crypto.createHash('sha256').update(String(a)).digest();
const hb = crypto.createHash('sha256').update(String(b)).digest();
return crypto.timingSafeEqual(ha, hb);
}
function verifySonioxRequest(headers, {
name = process.env.SONIOX_WEBHOOK_HEADER || 'Authorization',
secret = process.env.SONIOX_WEBHOOK_SECRET,
} = {}) {
if (!secret) return false; // never accept when unconfigured
const received = headers[name.toLowerCase()]; // Node lowercases header names
return Boolean(received) && safeEqual(received, secret); // whole value, no Bearer parsing
}
- The header name is yours: whatever you pass as
webhook_auth_header_name. Read it fromSONIOX_WEBHOOK_HEADER(defaultAuthorization) and look it up case-insensitively. - Compare the whole header value with
SONIOX_WEBHOOK_SECRET, exactly as you passed it aswebhook_. If you sentauth_ header_ value Bearer <my-secret-token>, the secret is that full string includingBearer. Don't strip or parse the prefix. - Return
401for a missing header or a mismatch,400for a body that isn't a JSON object,200once handled. - Read the body raw (
express.raw({ type: '*/*' })) and authenticate before parsing it. - A static shared credential authenticates the sender, not the body. There is no signature and no timestamp, so there is no replay protection. Use HTTPS only, and rotate the credential (use a new header value on new transcriptions and update
SONIOX_WEBHOOK_SECRET) if it leaks.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Payload and Statuses
The body is a small JSON object with no event name: { "id": "...", "status": "completed" }. id is the transcription ID; fetch the transcript with the Soniox API using that ID.
status | Meaning |
|---|---|
completed | The transcription finished; fetch the transcript by id |
error | The transcription failed; look up the transcription by id for details |
Environment Variables
SONIOX_WEBHOOK_HEADER=Authorization # same as webhook_auth_header_name
SONIOX_WEBHOOK_SECRET=Bearer your-secret-token # same as webhook_auth_header_value, whole value
Local Development
Soniox can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 soniox --path /webhooks/soniox
Use the printed URL as webhook_url when you create a transcription.
Resources
- references/overview.md - What Soniox webhooks are, statuses and payload shape
- references/setup.md - Create a transcription with the webhook and header fields
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests