Soniox

Handling webhooks with Node SDK

Use webhooks to receive transcription results with the Soniox Node SDK

SDK provides you a helper method to handle Webhooks from the Soniox API and transform them into a typed object.

Configure webhook delivery

If webhook is enabled during transcription creation, Soniox will send a POST request to your webhook URL with the transcription result.

await client.stt.transcribe({
  model: 'stt-async-v4',
  audio_url: 'https://soniox.com/media/examples/coffee_shop.mp3',
  webhook_url: 'https://your-server.com/webhooks/soniox',
  webhook_auth_header_name: 'X-Webhook-Secret',
  webhook_auth_header_value: process.env.SONIOX_API_WEBHOOK_SECRET,
});

You can also append metadata as query parameters:

await client.stt.transcribe({
  model: 'stt-async-v4',
  audio_url: 'https://soniox.com/media/examples/coffee_shop.mp3',
  webhook_url: 'https://your-server.com/webhooks/soniox',
  webhook_query: { 
    request_id: 'abc-123' 
  },
});

Learn more about testing webhooks locally.

Handling webhooks

The SDK provides both framework-agnostic and framework-specific handlers that parse the request body, verify authentication, and return a typed WebhookHandlerResultWithFetch.

All handlers return:

  • ok — whether the webhook was handled successfully
  • status — HTTP status code to return to Soniox
  • event — the parsed WebhookEvent (when ok=true)
  • error — error message (when ok=false)
  • fetchTranscript() — lazily fetch the full transcript (when event.status === 'completed')
  • fetchTranscription() — lazily fetch the transcription object
import express from 'express';

const app = express();
app.use(express.json());

app.post('/webhooks/soniox', async (req, res) => {
  const result = client.webhooks.handleExpress(req);

  if (result.ok && result.event.status === 'completed') {
    const transcript = await result.fetchTranscript();
    console.log(transcript?.text);
  }

  res.status(result.status).json({ received: true });
});
import Fastify from 'fastify';

const app = Fastify();

app.post('/webhooks/soniox', async (request, reply) => {
  const result = client.webhooks.handleFastify(request);

  if (result.ok && result.event.status === 'completed') {
    const transcript = await result.fetchTranscript();
    console.log(transcript?.text);
  }

  return reply.status(result.status).send({ received: true });
});
import { Hono } from 'hono';

const app = new Hono();

app.post('/webhooks/soniox', async (c) => {
  const result = await client.webhooks.handleHono(c);

  if (result.ok && result.event.status === 'completed') {
    const transcript = await result.fetchTranscript();
    console.log(transcript?.text);
  }

  return c.json({ received: true }, result.status);
});

handleHono is async because it reads the request body from the Hono context.

import { Controller, Post, Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';

@Controller('webhooks')
export class WebhooksController {
  @Post('soniox')
  async handleSoniox(@Req() req: Request, @Res() res: Response) {
    const result = client.webhooks.handleNestJS(req);

    if (result.ok && result.event.status === 'completed') {
      const transcript = await result.fetchTranscript();
      console.log(transcript?.text);
    }

    res.status(result.status).json({ received: true });
  }
}

Use handleRequest with any framework that provides a standard Fetch API Request object:

export default {
  async fetch(request: Request) {
    if (new URL(request.url).pathname === '/webhooks/soniox') {
      const result = await client.webhooks.handleRequest(request);

      if (result.ok && result.event.status === 'completed') {
        const transcript = await result.fetchTranscript();
        console.log(transcript?.text);
      }

      return Response.json({ received: true }, { status: result.status });
    }

    return new Response('Not found', { status: 404 });
  },
};

handleRequest is async because it reads the request body from the Request object.

The handle method is a framework-agnostic handler. You provide the method, headers, and parsed body directly:

const result = client.webhooks.handle({
  method: req.method,
  headers: req.headers,
  body: req.body,
});

if (result.ok && result.event.status === 'completed') {
  const transcript = await result.fetchTranscript();
  console.log(transcript?.text);
}

if (result.ok && result.event.status === 'error') {
  const transcription = await result.fetchTranscription();
  console.log(transcription?.error_message);
}

See HandleWebhookOptions for all available options.

Webhook auth helpers

By default, webhook handlers read auth from SONIOX_API_WEBHOOK_HEADER and SONIOX_API_WEBHOOK_SECRET. You can override auth explicitly:

const result = client.webhooks.handleExpress(req, {
  name: 'X-Webhook-Secret',
  value: process.env.SONIOX_API_WEBHOOK_SECRET,
});

Learn more info about Environment Variables.

But you can also verify the auth manually:

const auth = client.webhooks.getAuthFromEnv();
if (!auth) {
  throw new Error('Missing webhook auth');
}


const isValid = client.webhooks.verifyAuth(req.headers, auth);

Webhook event helpers

const event = client.webhooks.parseEvent(req.body);
const isEvent = client.webhooks.isEvent(req.body);

Testing webhooks locally

Since Soniox needs to reach your server over the internet, you'll need a tunnel to expose your local development server. You can use Cloudflare Tunnel or ngrok.

Cloudflare Tunnel provides a quick way to expose your local server — no account required.

Install cloudflared and start a tunnel pointing to your local server:

# macOS
brew install cloudflared

# Start a tunnel to your local server on port 3000
cloudflared tunnel --url http://localhost:3000

The command will output a public URL like https://random-name.trycloudflare.com.

ngrok creates a secure tunnel to your local server and provides a stable public URL.

Install ngrok, authenticate, and start a tunnel:

# macOS
brew install ngrok

# Authenticate (one-time setup)
ngrok config add-authtoken <your-token>

# Start a tunnel to your local server on port 3000
ngrok http 3000

The command will output a public URL like https://abcd-1234.ngrok-free.app.

Once you have your public tunnel URL, use it as the webhook_url when creating a transcription:

import express from 'express';
import { SonioxNodeClient } from '@soniox/node';

const client = new SonioxNodeClient();

const app = express();
app.use(express.json());

// Handle incoming webhook events
app.post('/webhooks/soniox', async (req, res) => {
  const result = client.webhooks.handleExpress(req);

  // You will receive the webhook event when the transcription is completed
  if (result.ok && result.event.status === 'completed') {
    const transcript = await result.fetchTranscript(); // Lazy fetch the transcript
    console.log(transcript?.text);
  }

  res.status(result.status).json({ received: true });
});

app.listen(3000, () => console.log('Listening on port 3000'));

// Start a transcription with the tunnel URL as webhook
await client.stt.transcribe({
  model: 'stt-async-v4',
  audio_url: 'https://soniox.com/media/examples/coffee_shop.mp3',
  webhook_url: 'https://<your-tunnel-url>/webhooks/soniox',
});