ProvenPin Install

Twilio webhook signature validation in Express with TwiML replies: working versions (tested 2026-10-15)

Validate X-Twilio-Signature on inbound SMS webhooks in Express with twilio.validateRequest, including query strings and tampered params, and reply with MessagingResponse TwiML, tested offline with supertest.

passed5 passed · 0 failed2026-10-15 20:10 UTC
Status
passed
Last passed
2026-10-15 20:10 UTC
Tests
5 / 5
Language
TypeScript
Runtime
node v22.23.3
pinned versions5
twilio@6.2.0express@5.3.0@types/node@26.6.5tsx@4.23.15supertest@7.3.1
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38799498972

The code

Exactly the files that passed. Your agent can pull them with get_recipe("twilio-webhook-signature-express") (install), or see them on GitHub.

package.json
{
  "name": "twilio-webhook-signature-express",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "twilio": "6.2.0",
    "express": "5.3.0"
  },
  "devDependencies": {
    "@types/node": "26.6.5",
    "tsx": "4.23.15",
    "supertest": "7.3.1"
  }
}
src/app.ts
import express from "express";
import twilio from "twilio";

const { MessagingResponse } = twilio.twiml;

/**
 * Inbound SMS webhook. Twilio signs the full public URL plus the sorted POST params with your
 * auth token (HMAC-SHA1, X-Twilio-Signature). Behind a proxy, rebuild the public URL yourself.
 */
export function makeApp(authToken: string, publicBaseUrl: string) {
  const app = express();
  app.use(express.urlencoded({ extended: false }));
  app.post("/sms", (req, res) => {
    const signature = req.header("x-twilio-signature") ?? "";
    const url = publicBaseUrl + req.originalUrl;
    if (!twilio.validateRequest(authToken, signature, url, req.body)) {
      res.status(403).type("text/plain").send("invalid signature");
      return;
    }
    const twiml = new MessagingResponse();
    const text = String(req.body.Body ?? "").trim().toUpperCase();
    twiml.message(text === "STATUS" ? "Class starts Nov 1st" : `Got: ${req.body.Body}`);
    res.type("text/xml").send(twiml.toString());
  });
  return app;
}
test/app.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import request from "supertest";
import twilio from "twilio";
import { makeApp } from "../src/app.ts";

const TOKEN = "12345abcdef12345abcdef12345abcde";
const BASE = "https://example.ngrok.app";
const params = { From: "+19075550100", To: "+19075550199", Body: "status", MessageSid: "SM123" };

const sign = (url: string, p: Record<string, string>) => twilio.getExpectedTwilioSignature(TOKEN, url, p);

test("signature helper matches validateRequest", () => {
  const sig = sign(`${BASE}/sms`, params);
  assert.equal(twilio.validateRequest(TOKEN, sig, `${BASE}/sms`, params), true);
  assert.equal(twilio.validateRequest(TOKEN, sig, `${BASE}/other`, params), false);
});

test("valid webhook gets TwiML reply", async () => {
  const res = await request(makeApp(TOKEN, BASE))
    .post("/sms")
    .type("form")
    .set("X-Twilio-Signature", sign(`${BASE}/sms`, params))
    .send(params);
  assert.equal(res.status, 200);
  assert.match(res.headers["content-type"], /text\/xml/);
  assert.equal(res.text, '<?xml version="1.0" encoding="UTF-8"?><Response><Message>Class starts Nov 1st</Message></Response>');
});

test("query string is part of the signed URL", async () => {
  const sig = sign(`${BASE}/sms?tenant=aia`, { ...params, Body: "hi" });
  const ok = await request(makeApp(TOKEN, BASE)).post("/sms?tenant=aia").type("form").set("X-Twilio-Signature", sig).send({ ...params, Body: "hi" });
  assert.equal(ok.status, 200);
  assert.match(ok.text, /<Message>Got: hi<\/Message>/);
});

test("tampered params are rejected", async () => {
  const res = await request(makeApp(TOKEN, BASE))
    .post("/sms")
    .type("form")
    .set("X-Twilio-Signature", sign(`${BASE}/sms`, params))
    .send({ ...params, Body: "something else" });
  assert.equal(res.status, 403);
});

test("wrong token or missing header is rejected", async () => {
  const wrong = twilio.getExpectedTwilioSignature("x".repeat(32), `${BASE}/sms`, params);
  const app = makeApp(TOKEN, BASE);
  assert.equal((await request(app).post("/sms").type("form").set("X-Twilio-Signature", wrong).send(params)).status, 403);
  assert.equal((await request(app).post("/sms").type("form").send(params)).status, 403);
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.