# Webhook Mock Sender

> Free CLI that sends signed Stripe, GitHub and Shopify test webhooks to your endpoint, plus wrong signatures, replays and duplicates to test rejection.

Source: https://webhooker.eu/tools/webhook-mock-sender
Last updated: 2026-09-18

[Open-source tools](https://webhooker.eu/tools) / Webhook Mock Sender

Webhook Mock Sender builds a realistic Stripe, GitHub or Shopify event, signs it with your webhook secret using that provider's own scheme, and POSTs it to any URL, localhost included. It also sends what a provider never will on demand: a wrong signature, a replayed request, a duplicate delivery. That is how you find out whether your handler refuses what it should.

- Python
- MIT licensed
- Docker image
- No account

[View on GitHub](https://github.com/webhooker-eu/webhook-mock-sender) Quick start

## 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](https://docs.astral.sh/uv/):

```bash
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
```

```text
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:

```bash
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:

```bash
# 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

```bash
# 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:

```python
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](https://webhooker.eu/) takes it over: one EU-hosted ingest URL per source, the provider's signature [verified before the payload is accepted](https://webhooker.eu/docs/inbound-verification), 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](https://webhooker.eu/blog/verify-stripe-webhook-signatures)
- [Webhook signature verification failed: 6 causes and fixes](https://webhooker.eu/blog/webhook-signature-verification-failed)
- [Verifying GitHub webhook signatures](https://webhooker.eu/blog/verify-github-webhook-signature)
- [Webhook replay attacks and timestamp tolerance](https://webhooker.eu/blog/webhook-replay-attacks-timestamp-tolerance)
- [Idempotency keys for webhook consumers](https://webhooker.eu/blog/webhook-idempotency-keys)

## More open-source tools

[Webhook Tester  →   Self-hosted request bin in one Docker container.](https://webhooker.eu/tools/webhook-tester)[Discord Webhook Tester  →   Send a test message to a Discord webhook, and build the embed visually.](https://webhooker.eu/tools/discord-webhook-tester)

Everything we publish lives in the [webhooker-eu GitHub organisation](https://github.com/webhooker-eu), and the full list with a short description of each is on the [open-source tools page](https://webhooker.eu/tools).

## When the testing is done, the events still have to arrive.

Webhooker is the hosted side of the same problem: one EU-hosted ingest URL for any provider, signatures verified before a payload is accepted, every event stored, retried and replayable. Free for 10,000 events a month.

[Start for free](https://app.webhooker.eu/register) [Read the docs](https://webhooker.eu/docs)
