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.
Receive incoming Plivo text messages on your laptop while you build, with a public HTTPS URL Plivo can reach.
Horizon has no Plivo integration. Plivo calls 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 Plivo account with an SMS-capable phone number
- A Next.js app that uses the App Router and runs on port 3000
Start your app
Install the plivo package. It includes the signature helper.
npm install plivoPlivo sends an incoming SMS as a POST with a form-encoded body (application/x-www-form-urlencoded). The body has From, To and Text. Plivo signs the request in the X-Plivo-Signature-V2 header. It also sends X-Plivo-Signature-V2-Nonce. The signature is an HMAC-SHA256 of the URL plus the nonce, keyed with your Auth Token. It doesn't cover the body.
validateSignature needs the URL Plivo called. Set it in an environment variable, so it is the Horizon URL and not localhost.
import { validateSignature } from "plivo";
export async function POST(request: Request) {
const authToken = process.env.PLIVO_AUTH_TOKEN;
const webhookUrl = process.env.PLIVO_WEBHOOK_URL;
if (!authToken || !webhookUrl) {
return new Response("Missing PLIVO_AUTH_TOKEN or PLIVO_WEBHOOK_URL", { status: 500 });
}
const signature = request.headers.get("x-plivo-signature-v2") ?? "";
const nonce = request.headers.get("x-plivo-signature-v2-nonce") ?? "";
if (!validateSignature(webhookUrl, nonce, signature, authToken)) {
return new Response("Invalid signature", { status: 403 });
}
const form = new URLSearchParams(await request.text());
console.log(`Message from ${form.get("From")} to ${form.get("To")}: ${form.get("Text")}`);
return new Response("Message received", { status: 200 });
}Find your Auth Token in the Plivo console. Add both variables to .env.local:
PLIVO_AUTH_TOKEN=replace-with-your-auth-token
PLIVO_WEBHOOK_URL=https://my-app.hrzn.run/api/webhooks/plivoStart 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 Plivo Message URL would point at a dead URL after a restart. Reserved subdomains are a paid feature, see Pricing.
Set the Message URL in Plivo
Plivo calls the Message URL of the application assigned to your number.
- In the Plivo console, go to Messaging, then Applications.
- Select Add New Application.
- Enter a name, for example
Receive SMS. - Set Message URL to
https://my-app.hrzn.run/api/webhooks/plivoand the method toPOST. - Select Create Application.
- Open the Numbers page and select your phone number.
- Set Application Type to
XML Application. - In the Plivo Application dropdown, select the application you created.
- Select Update Number.
Send a test message
Plivo has no resend button for incoming messages. Text your Plivo number from your phone.
Check it works
The Horizon terminal prints one line for the request:
POST 200 /api/webhooks/plivoYour app terminal prints Message from <your number> to <your Plivo number>: <your text>.
Troubleshooting
The route returns 403
The signature check failed. Plivo signs the URL it called, so PLIVO_WEBHOOK_URL must match the Message URL in the console: protocol, subdomain and path.
- Check that
PLIVO_WEBHOOK_URLuseshttps://my-app.hrzn.run, nothttp://localhost:3000. - Check that
PLIVO_AUTH_TOKENbelongs to the account or subaccount that owns the application.X-Plivo-Signature-V2uses that token.X-Plivo-Signature-Ma-V2always uses the main account token. - Restart
npm run devafter you edit.env.local.
Text is empty
Plivo sends a form-encoded body, not JSON. Read it with URLSearchParams or request.formData(), not request.json().
The Message URL stopped working 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 Plivo's guide to validating requests.
- See Pricing to reserve a subdomain so your URL never changes.
Test Microsoft Teams outgoing webhooks locally
Receive Microsoft Teams outgoing webhook requests on localhost with a Horizon tunnel, and verify the HMAC authorization header.
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.