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_auth_header_value), 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_name and webhook_auth_header_value?
  • How do I handle a transcription with status completed or error?
  • 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 from SONIOX_WEBHOOK_HEADER (default Authorization) and look it up case-insensitively.
  • Compare the whole header value with SONIOX_WEBHOOK_SECRET, exactly as you passed it as webhook_auth_header_value. If you sent Bearer <my-secret-token>, the secret is that full string including Bearer . Don't strip or parse the prefix.
  • Return 401 for a missing header or a mismatch, 400 for a body that isn't a JSON object, 200 once 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.

statusMeaning
completedThe transcription finished; fetch the transcript by id
errorThe 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 10, 2026

View on GitHub →