Build a webhook handler with no provider account, dashboard, or real event. otterkit send fires a correctly signed synthetic event straight at your local handler, and otterkit verify tells you whether captured requests carry a valid signature. Both run entirely on your machine, and the signing secret never leaves it.
Send a signed event#
Pass the provider's signing secret and the event is signed exactly the way the provider signs it, so your handler's verification passes:
# Stripe: send a signed payment_intent.succeeded to your handler
npx otterkit send stripe:payment_intent.succeeded 127.0.0.1:3000/webhooks/stripe \
--secret whsec_your_signing_secret
# GitHub, Shopify, and Slack work the same way
npx otterkit send github:push 127.0.0.1:3000/hooks/github --secret my-webhook-secret
npx otterkit send shopify:orders/create 127.0.0.1:3000/hooks --secret shpss_...
# Customize the payload
npx otterkit send stripe:checkout.session.completed 127.0.0.1:3000/hooks \
--secret whsec_... --body '{"data":{"object":{"amount_total":19900}}}'These events are built in:
| Provider | Events |
|---|---|
stripe | payment_intent.succeeded, checkout.session.completed, invoice.paid, customer.subscription.deleted |
github | push, pull_request.opened, issues.opened |
shopify | orders/create, orders/paid |
slack | event_callback.message, url_verification |
Without --secret the event is sent unsigned, which is useful for checking that your handler rejects unsigned requests. Add --json for a machine-readable result.
Verify captured signatures#
The most common webhook question is “is my signature check wrong, or is the payload wrong?” Point verify at a capture log with the secret and it gives a clear answer:
npx otterkit verify myhooks stripe --secret whsec_...
# ✓ 08:14:02.101 POST /webhooks/stripe
# ✗ 08:15:11.882 POST /webhooks/stripe
# Stripe-Signature: signature_mismatchIf a request fails with the correct secret, the payload was altered in transit or your code isn't verifying the raw bytes. If every request passes, the bug is in your handler. verify checks the last 20 requests by default; change that with --last <n>. It supports Stripe, GitHub, Shopify, and Slack. For the other providers, use server-side --verify on the webhook endpoint.
Replay with re-signing#
Editing the body of a captured signed request breaks its signature, so your handler rejects the replay. --resign signs the edited body again so it still passes:
npx otterkit replay myhooks --set data.object.amount=999 \
--resign stripe --secret whsec_...