What Webhook Mock Sender does
It removes the provider from the loop. No dashboard, no test mode, no tunnel. Just a signed request arriving at your endpoint, and a report of what your endpoint said back.
| Feature | What it means |
|---|---|
| Valid signatures | Stripe-Signature, X-Hub-Signature-256 and
X-Shopify-Hmac-Sha256 computed over the exact bytes that are sent. The
Stripe signature is tested against the official stripe library.
|
| 28 event templates | Payments, subscriptions and invoices for Stripe; push, pull requests, issues and releases for GitHub; orders, products and customers for Shopify. Fresh IDs and timestamps on every run. |
| Provider headers | X-GitHub-Event, X-GitHub-Delivery,
X-Shopify-Topic, X-Shopify-Webhook-Id, the provider's
user agent and the rest.
|
| Failure scenarios | --invalid-signature, --timestamp-offset for a replay,
--repeat for a duplicate, and --expect to assert the answer.
|
| Editable payloads |
Change a field with --set, or sign and send your own JSON with
--payload-file.
|
| Web form | Pick an event, edit the body, watch the signature header update as you type, send, and read the response. |
Quick start
Run it without installing anything, using uv:
export STRIPE_WEBHOOK_SECRET='whsec_...' # the secret your endpoint verifies against
uvx --from git+https://github.com/webhooker-eu/webhook-mock-sender \
webhook-mock-sender send stripe payment_intent.succeeded http://localhost:3000/webhooks/stripe Stripe payment_intent.succeeded → http://localhost:3000/webhooks/stripe
✓ Delivery: HTTP 200 in 12 ms
response {"received": true}
Or through Docker, where host.docker.internal is your machine as seen from inside
the container:
docker run --rm --add-host host.docker.internal:host-gateway \
-e STRIPE_WEBHOOK_SECRET ghcr.io/webhooker-eu/webhook-mock-sender \
send stripe payment_intent.succeeded http://host.docker.internal:3000/webhooks/stripe
The secret comes from --secret, or from the variable you probably already have:
STRIPE_WEBHOOK_SECRET, GITHUB_WEBHOOK_SECRET,
SHOPIFY_WEBHOOK_SECRET, with WEBHOOK_MOCK_SECRET as a fallback for
any provider.
Testing the unhappy paths
A handler that accepts a correctly signed event proves very little. The interesting tests are the ones a provider will not run for you:
# A wrong signature must be refused
webhook-mock-sender send stripe invoice.paid "$URL" --invalid-signature --expect 400
# Signed 10 minutes ago: Stripe's libraries allow 5, so this is a replay
webhook-mock-sender send stripe invoice.paid "$URL" --timestamp-offset -600 --expect 400
# The same delivery twice: the handler should process it once
webhook-mock-sender send github push "$URL" --repeat 2
With --expect, the exit code is 0 only when your endpoint answers
with that status, so these lines work as assertions in CI. If the endpoint accepts a request
with a wrong signature, the tool says so in as many words: your endpoint does not verify
signatures.
Changing the payload
# Change fields of the template
webhook-mock-sender send stripe invoice.paid "$URL" \
--set data.object.total=9900 \
--set data.object.customer_email=ada@example.com
# Sign and send your own body, byte for byte
webhook-mock-sender send github label "$URL" --payload-file event.json
# Look at the signed request without sending it
webhook-mock-sender send stripe charge.refunded --dry-run
webhook-mock-sender send shopify orders/paid "$URL" --curl How each provider signs a webhook
All three use HMAC-SHA256 with a shared secret over the raw request body. What differs is the header, the encoding, and whether a timestamp is part of the signed string.
| Provider | Header | Value | Signed content | Replay protection |
|---|---|---|---|---|
| Stripe | Stripe-Signature | t=<unix time>,v1=<hex digest> | <unix time>.<raw body> | Yes: the time is signed, and libraries reject anything older than 5 minutes |
| GitHub | X-Hub-Signature-256 | sha256=<hex digest> | raw body | No, deduplicate on X-GitHub-Delivery |
| Shopify | X-Shopify-Hmac-Sha256 | <base64 digest> | raw body | No, deduplicate on X-Shopify-Webhook-Id |
Which events are included
| Provider | Mock events |
|---|---|
| Stripe | payment_intent.succeeded, payment_intent.payment_failed, charge.succeeded, charge.refunded, checkout.session.completed, customer.created, customer.subscription.created / updated / deleted, invoice.paid, invoice.payment_failed |
| GitHub | ping, push, pull_request, issues, issue_comment, release, star, workflow_run |
| Shopify | orders/create, orders/paid, orders/fulfilled, orders/cancelled, products/create, products/update, customers/create, checkouts/create, app/uninstalled |
The payloads follow the providers' documented shapes but are trimmed: they exercise your
handler's plumbing, not every field of the provider's API reference. For anything not listed,
pass the real JSON with --payload-file.
Inside a test suite
The same request can be built in Python and handed straight to your framework's test client, so a signature test runs without a network:
from webhook_mock_sender import build_mock_request, get_provider
mock_request = build_mock_request(
get_provider("stripe"),
"payment_intent.succeeded",
"whsec_test_secret",
overrides=["data.object.amount=5000"],
)
# In a test, without a network
response = test_client.post(
"/webhooks/stripe",
content=mock_request.body,
headers=mock_request.headers,
) From testing to production traffic
Mock events tell you your handler is correct. They say nothing about what happens when your service is redeploying at the moment a real payment lands, or when a provider gives up after three failed attempts. That is a delivery problem, not a verification one.
Webhooker takes it over: one EU-hosted ingest URL per source, the provider's signature verified before the payload is accepted, every event stored durably, six delivery attempts over about five hours, a dead-letter queue and one-click replay. The free plan covers 10,000 events a month.
Frequently asked questions
How do I test a Stripe webhook locally without the Stripe CLI?
Set STRIPE_WEBHOOK_SECRET to the same value your app verifies against and run webhook-mock-sender send stripe payment_intent.succeeded http://localhost:3000/webhooks/stripe. The Stripe-Signature header is computed the way Stripe computes it, so stripe.Webhook.construct_event accepts the request. No Stripe account, dashboard or login is involved.
Why does my endpoint reject a correctly signed request?
Almost always because the signature is checked against a re-serialized body. Verify the raw bytes of the request, before any JSON middleware parses them. The other usual causes are the wrong secret (test versus live, or the stripe listen secret versus the dashboard one) and a proxy that rewrites the body on the way in.
How do I check that my handler refuses a forged webhook?
Send one. --invalid-signature signs with a wrong secret, --timestamp-offset -600 imitates a replayed request, and --repeat 2 sends the same delivery twice. Pair any of them with --expect 400 (or 401) and the exit code is 0 only when your endpoint answered with that status, which makes it a test rather than a manual check.
Can I send an event that is not in the list?
Yes. Save the JSON to a file and pass it with --payload-file; the event name you give is used for X-GitHub-Event or X-Shopify-Topic, and the body is signed exactly as it is. You can also start from a template and change single fields with --set data.object.amount=5000.
Is a mock event the same as a real one?
The headers and the signature scheme are identical. The payload is a trimmed, realistic sample with random IDs, so the objects it refers to do not exist at the provider: a handler that calls the provider's API back will get a 404 there. For plumbing (verification, idempotency, status codes, retries) that makes no difference.
How do I receive real webhooks on localhost?
That is the opposite direction and this tool does not do it. Use a request bin such as Webhook Tester to look at what a provider sends, or the Webhooker gateway to receive, store and replay production webhooks through one ingest URL.
Further reading
- Verifying Stripe webhook signatures
- Webhook signature verification failed: 6 causes and fixes
- Verifying GitHub webhook signatures
- Webhook replay attacks and timestamp tolerance
- Idempotency keys for webhook consumers
More open-source tools
Self-hosted request bin in one Docker container.
Discord Webhook TesterSend a test message to a Discord webhook, and build the embed visually.
Everything we publish lives in the webhooker-eu GitHub organisation, and the full list with a short description of each is on the open-source tools page.