Horizon

Test Svix webhooks locally

Receive Svix-signed webhook events on localhost with a Horizon tunnel, and verify them with the svix npm package in a Next.js route.

Receive Svix webhooks on your laptop while you build, with a URL Svix can reach.

Svix delivers webhooks for many companies. You get a signing secret from the app portal of the service that sends you events. The steps below work for any sender that uses Svix.

Before you begin

  • Node.js 18 or later
  • A Horizon account and the CLI (see Getting started)
  • A reserved subdomain for -s. Reserve one on the Subdomains page.
  • Access to the Svix app portal of the service that sends you webhooks
  • A Next.js App Router app

Start your app

Install the official svix package.

npm install svix

Svix signs the message ID, the timestamp and the raw body with HMAC-SHA256 (a keyed hash). It sends the result in the svix-id, svix-timestamp and svix-signature headers. The Webhook class checks all three and throws when they don't match. Read the raw body with request.text(), because a parsed and re-serialized body breaks the signature.

app/api/webhooks/svix/route.ts
import { Webhook } from "svix";

export async function POST(request: Request) {
  const secret = process.env.SVIX_WEBHOOK_SECRET;
  if (!secret) {
    return new Response("Missing SVIX_WEBHOOK_SECRET", { status: 500 });
  }

  const body = await request.text();
  const headers = {
    "svix-id": request.headers.get("svix-id") ?? "",
    "svix-timestamp": request.headers.get("svix-timestamp") ?? "",
    "svix-signature": request.headers.get("svix-signature") ?? "",
  };

  try {
    new Webhook(secret).verify(body, headers);
  } catch {
    return new Response("Invalid signature", { status: 400 });
  }

  const event = JSON.parse(body);
  console.log(`Received Svix event: ${event.type}`);

  return new Response("ok", { status: 200 });
}

Svix rejects messages with a timestamp more than five minutes from the current time. The library applies that check for you.

Store the signing secret in an environment variable. It starts with whsec_.

.env.local
SVIX_WEBHOOK_SECRET=whsec_replace-with-your-secret

Start the app on port 3000.

npm run dev

Start a tunnel

Use -s with a subdomain you reserved. Without it, the subdomain is random and changes every run, so your Svix endpoint would point at a dead URL after a restart. Reserve it first on the Subdomains page. Reserved subdomains are a paid feature, see Pricing.

hrzn tunnel http://localhost:3000 -s my-app
Output
HORIZON: Tunnel connected
  URL          https://my-app.hrzn.run (reserved)
  Forwarding   http://localhost:3000
  Request log  https://hrzn.run/dashboard/tunnels/my-app

Your public URL is https://my-app.hrzn.run. Keep this terminal open.

Add the endpoint in the Svix app portal

An endpoint is a URL you control plus the event types you want to receive.

  1. Open the app portal of the service that sends you webhooks.
  2. Add an endpoint.
  3. Enter https://my-app.hrzn.run/api/webhooks/svix as the URL.
  4. Choose the event types you want. If you choose none, the endpoint receives all events.
  5. Save the endpoint, then copy its signing secret into SVIX_WEBHOOK_SECRET.

Restart npm run dev after you edit .env.local.

Send a test event

Svix gives every endpoint a Testing tab for example events.

  1. Open your endpoint in the app portal.
  2. Select the Testing tab.
  3. Send an example event for one of your event types.
  4. Select the message to see its payload and every delivery attempt.

Resend a message

Svix keeps past messages, so you can send one again without a new event.

  1. Find the message in the app portal.
  2. Open the options menu next to one of its attempts.
  3. Select resend.

To resend everything that failed, open the endpoint's details page and select Options, then Recover Failed Messages. Pick a time window.

Check it works

Send an example event from the Testing tab. Your Horizon terminal prints one line for it:

Output
  POST    200  /api/webhooks/svix

Your app terminal prints the event type:

Output
Received Svix event: <event type>

The message in the app portal shows the attempt as succeeded.

Troubleshooting

The handler returns 400

  • Check that SVIX_WEBHOOK_SECRET is the signing secret of this endpoint, including the whsec_ prefix. Each endpoint has its own secret.
  • Verify the raw body from request.text(). Don't run JSON.parse and JSON.stringify first.
  • Restart npm run dev after you edit .env.local.
  • Check your computer's clock. Svix rejects timestamps more than five minutes away.

The URL changed after a restart

You started the tunnel without -s, so Horizon gave you a new random subdomain. Restart with -s my-app and the URL stays the same. -s needs a subdomain you reserved, see Pricing.

Nothing reaches your app

  • Check that the Horizon terminal is still running. If its last line is Connection lost. Reconnecting…, wait for Reconnected.
  • Check that the endpoint URL ends with /api/webhooks/svix.

Next steps

On this page