webhook-sig
v0.1.0
Published
Verify and generate webhook signatures locally for Stripe, GitHub, Shopify, Slack, Twilio, and Svix. No network, no telemetry — pure node:crypto.
Downloads
24
Maintainers
Readme
webhook-sig
Verify and generate webhook signatures from your terminal. Stripe, GitHub, Shopify, Slack, Twilio, and Svix — one small CLI, the correct HMAC scheme for each provider.
- 🔒 100% local. Pure
node:crypto. No network calls, no telemetry. Your signing secret never leaves your machine. - 📦 Zero dependencies. One file of crypto, one file of CLI. Audit it in five minutes.
- 🧪 CI-friendly. Reads payloads from stdin or a file, exits
0/1on verify, speaks--json. - ✅ Correct. Tested against the official documented test vectors (GitHub
Hello, World!, the Svix example, Slack's base string) and round-tripped generate → verify.
npx webhook-sig verify --provider github \
--secret "$GH_WEBHOOK_SECRET" \
--signature "$X_HUB_SIGNATURE_256" \
--file captured-payload.json
# ✓ VALID — GitHub signature matches.Why this exists
If you've ever built a webhook handler, you've hit this: the signature doesn't match and you have no idea why. Is it the secret? The wrong base string? A re-serialized body? Each provider does it differently — Stripe signs timestamp.body, Slack signs v0:timestamp:body, Twilio signs the URL plus alpha-sorted form params with SHA-1, Svix base64-decodes the secret first.
webhook-sig lets you:
- Verify a captured webhook against your secret, so you can prove whether the signature itself is right (vs. your framework mangling the body).
- Generate a correctly-signed header for a test payload, so you can
curlyour own handler in development and in CI without waiting for a real event.
It mirrors EventDock's free in-browser signature tools as a scriptable, terminal-native CLI.
Install
No install needed:
npx webhook-sig --helpOr install globally:
npm install -g webhook-sig
webhook-sig --helpRequires Node.js 18+.
Usage
webhook-sig verify --provider <name> --secret <secret> --signature <header> [payload] [options]
webhook-sig generate --provider <name> --secret <secret> [payload] [options]The payload is the raw request body — the exact bytes the provider sent. Provide it with --body "<string>", --file <path>, or pipe it on stdin. (For Twilio, see below.)
⚠️ The #1 reason signatures fail: verifying against a re-serialized body. Most frameworks parse JSON before your handler runs, and re-stringifying changes whitespace and key order — which changes the HMAC. Always capture and verify the raw bytes.
Stripe — Stripe-Signature: t=…,v1=…
Signs "{timestamp}.{body}" with HMAC-SHA256, hex. Header carries the timestamp; verification enforces a 5-minute replay window by default.
# Verify
webhook-sig verify --provider stripe \
--secret whsec_xxx \
--signature "t=1614000000,v1=5257a8..." \
--file payload.json
# add --tolerance 0 to skip the replay-window check for an old captured event
# Generate a test header (timestamp defaults to now)
echo -n '{"id":"evt_1","object":"event"}' | \
webhook-sig generate --provider stripe --secret whsec_test --header
# Stripe-Signature: t=1718000000,v1=...GitHub — X-Hub-Signature-256: sha256=…
HMAC-SHA256 of the raw body, hex, prefixed sha256=.
webhook-sig verify --provider github \
--secret "$GH_WEBHOOK_SECRET" \
--signature "sha256=757107ea..." \
--file payload.json
webhook-sig generate --provider github --secret hunter2 --body '{"action":"opened"}'
# sha256=...Shopify — X-Shopify-Hmac-Sha256: <base64>
HMAC-SHA256 of the raw body, base64-encoded, keyed with your app's API secret.
webhook-sig verify --provider shopify \
--secret "$SHOPIFY_API_SECRET" \
--signature "9Xx...=" \
--file order.jsonSlack — X-Slack-Signature: v0=… (+ X-Slack-Request-Timestamp)
Signs "v0:{timestamp}:{body}" with HMAC-SHA256, hex, prefixed v0=. You must pass the timestamp.
webhook-sig verify --provider slack \
--secret "$SLACK_SIGNING_SECRET" \
--signature "v0=a2114d57..." \
--timestamp 1531420618 \
--body 'token=xyz&team_id=T1&command=%2Fweather'Twilio — X-Twilio-Signature: <base64>
The odd one out. For classic application/x-www-form-urlencoded webhooks, Twilio signs the full request URL with the POST params appended in alphabetical order (key + value, no separators), using HMAC-SHA1, base64-encoded, keyed with your Auth Token.
Pass the URL and params and let the CLI build the canonical string:
webhook-sig verify --provider twilio \
--secret "$TWILIO_AUTH_TOKEN" \
--url "https://your.app/sms" \
--params '{"From":"+14155551212","Body":"hi","MessageSid":"SM123"}' \
--signature "GvWf8...="…or pass the exact pre-built "URL + sorted params" string as --body.
Svix / Standard Webhooks — svix-signature: v1,… (+ svix-id, svix-timestamp)
Used by Svix and providers built on it. Signs "{id}.{timestamp}.{body}" with HMAC-SHA256, base64; the key is the base64-decoded secret after the whsec_ prefix. The header may be a space-separated list of v1,<sig> pairs.
webhook-sig verify --provider svix \
--secret whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw \
--id msg_p5jXN8AQM9LWM0D4loKWxJek \
--timestamp 1614265330 \
--signature "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=" \
--body '{"test": 2432232314}'Options
| Flag | Description |
|------|-------------|
| --provider <name> | stripe, github, shopify, slack, twilio, svix |
| --secret <secret> | Signing secret / auth token |
| --secret-file <p> | Read the secret from a file (keeps it out of shell history) |
| --body <string> | Inline raw payload |
| --file <path> | Read raw payload from a file |
| (stdin) | If neither --body nor --file is given, payload is read from stdin |
| --signature <hdr> | (verify) the signature header value the provider sent |
| --timestamp <unix> | Stripe/Slack/Svix timestamp (generate: defaults to now) |
| --id <id> | Svix message id |
| --tolerance <secs> | Replay window for time-based providers (default 300, 0 disables) |
| --url, --params | Twilio: request URL and POST params (JSON object) |
| --header | (generate) print the full Header-Name: value line |
| --json | Machine-readable JSON output |
| --quiet | No text; exit code only |
| -h, --help / -v, --version | |
Exit codes: 0 = valid signature / success, 1 = invalid signature, 2 = usage error.
Use it in CI
# Fail the job if a recorded fixture's signature doesn't verify
webhook-sig verify --provider stripe --secret "$STRIPE_WHSEC" \
--signature "$(cat fixtures/sig.txt)" --file fixtures/event.json --tolerance 0 --quiet \
|| { echo "Signature broke!"; exit 1; }Use it as a library
The signing/verification schemes are exported, with zero dependencies:
import { PROVIDERS, getProvider } from 'webhook-sig';
const { header } = PROVIDERS.github.sign({ body: '{"hi":1}', secret: 'k' });
const { valid } = getProvider('github').verify({ body: '{"hi":1}', secret: 'k', header });Privacy & security
- No network. This tool never opens a socket. You can run it offline.
- No telemetry. Nothing is collected, ever.
- Comparisons use constant-time equality (
crypto.timingSafeEqual). - Prefer
--secret-fileor stdin over--secretso secrets don't land in shell history or process listings.
Going to production?
webhook-sig checks one request at a time — perfect for debugging and CI. But a single signature check is the easy part of receiving webhooks. In production you also have to deal with:
- Retries when your endpoint is briefly down (and providers give up after a few attempts).
- A dead-letter queue so a bad deploy doesn't silently drop events forever.
- Replay to re-process events after you ship a fix.
That's what EventDock does. Point your providers at EventDock, verify signatures once at the edge, and get automatic retries, a DLQ, and one-click replay for your incoming webhooks. Free for 5,000 events/month — no credit card.
License
MIT © EventDock
