Test Pusher Channels webhooks locally
Receive Pusher Channels webhooks on localhost with a Horizon tunnel, and verify the X-Pusher-Signature header with the Pusher Node library.
Receive Pusher Channels events such as channel_occupied on your laptop while you build, with a public HTTPS URL Pusher can reach.
Horizon has no Pusher integration. Pusher posts webhooks to a public URL, and Horizon provides that URL.
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. - A Pusher Channels app
- A Next.js app that uses the App Router and runs on port 3000
Start your app
Pusher sends two headers with each webhook. X-Pusher-Key names the app key. X-Pusher-Signature is the HMAC SHA256 hex digest of the POST body, signed with that key's secret. The body is JSON, with time_ms and an events array.
Install the pusher package. Its webhook helper checks the key and the signature.
npm install pusherThe helper wants the lower-case headers and the raw body.
import Pusher from "pusher";
const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID ?? "",
key: process.env.PUSHER_KEY ?? "",
secret: process.env.PUSHER_SECRET ?? "",
cluster: process.env.PUSHER_CLUSTER ?? "",
});
export async function POST(request: Request) {
const rawBody = await request.text();
const webhook = pusher.webhook({
headers: Object.fromEntries(request.headers),
rawBody,
});
if (!webhook.isValid()) {
return new Response("Invalid signature", { status: 401 });
}
for (const event of webhook.getEvents()) {
console.log(`Received Pusher event: ${event.name} on ${event.channel}`);
}
return new Response("ok", { status: 200 });
}Pusher expects a 2xx response. For any other code, it retries with exponential backoff for 5 minutes.
Find the app ID, key, secret and cluster in your app in the Pusher dashboard. Add them to .env.local:
PUSHER_APP_ID=replace-with-your-app-id
PUSHER_KEY=replace-with-your-key
PUSHER_SECRET=replace-with-your-secret
PUSHER_CLUSTER=replace-with-your-clusterStart the app.
npm run devStart a tunnel
In a second terminal, open a tunnel on a subdomain you reserved.
hrzn tunnel http://localhost:3000 -s my-appHORIZON: Tunnel connected
URL https://my-app.hrzn.run (reserved)
Forwarding http://localhost:3000
Request log https://hrzn.run/dashboard/tunnels/my-appYour public URL is https://my-app.hrzn.run. Keep this terminal open.
Use -s. Without it, the subdomain is random and changes every run, so your Pusher webhook would point at a dead URL after a restart. Reserved subdomains are a paid feature, see Pricing.
Add the webhook in Pusher
Webhooks are set per app.
- Sign in to the Pusher dashboard and open your Channels app.
- Select Webhooks, then Add webhook.
- Set Webhook URL to
https://my-app.hrzn.run/api/webhooks/pusher. - Set Event Type to
Channel existence. - Save the webhook.
Trigger an event
Pusher has no resend button. Cause a real event instead. A channel_occupied event fires when a channel gets its first subscriber. Subscribe from any client, for example with pusher-js:
import Pusher from "pusher-js";
const client = new Pusher("your-key", { cluster: "your-cluster" });
client.subscribe("my-channel");Check it works
The Horizon terminal prints one line for the request:
POST 200 /api/webhooks/pusherYour app terminal prints:
Received Pusher event: channel_occupied on my-channelPusher sends channel_vacated up to three seconds after the last client leaves.
Troubleshooting
The route returns 401
The Horizon line shows [401]. The key or signature check failed.
- Check that
PUSHER_KEYandPUSHER_SECRETbelong to the app that owns the webhook. The helper comparesX-Pusher-Keywith your key. - Restart
npm run devafter you edit.env.local. - Read the body with
request.text(). Pusher signs the exact bytes. - The helper also checks that the content type is JSON.
Nothing reaches your app
Check that the Webhook URL ends with /api/webhooks/pusher and that the event type matches what you trigger. Check that the Horizon terminal is still running.
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.
Next steps
- Read Pusher's webhooks reference for every event type.
- See Pricing to reserve a subdomain so your URL never changes.
Test Plivo SMS webhooks locally
Receive Plivo incoming SMS webhooks on localhost with a Horizon tunnel, and verify the X-Plivo-Signature-V2 header with the Plivo Node SDK.
Test SendGrid webhooks locally
Receive SendGrid Event Webhook events on localhost with a Horizon tunnel, and verify the signed event signature in a Next.js route handler.