How I test webhooks before trusting them
· 5 min read
A webhook is a simple idea: one system sends an HTTP request to another system when something happens.
For example:
- Stripe sends a webhook when a payment succeeds;
- GitHub sends a webhook when someone pushes code;
- Shopify sends a webhook when an order is created.
It sounds like a normal API call, but webhooks need extra care because they come from outside your app. You should not trust a webhook only because it reached your server.
The Webhook Tester helps you build a sample payload, sign it, verify it, and understand what your backend should check.
The simple mental model
Think of a webhook like a delivery package.
The payload is the package content:
{
"event": "payment.succeeded",
"amount": 50000
}
The signature is the tamper seal:
t=1790467200,v1=...
Your server should ask two questions:
- Did this package really come from the service I trust?
- Was the package changed on the way?
That is what signature verification is for.
Step 1: know the webhook provider
Different providers sign webhooks differently. Do not assume all webhook signatures work the same way.
The Webhook Tester supports common styles such as:
- Stripe-style signatures;
- GitHub-style signatures;
- Shopify-style signatures.
Pick the provider style that matches the service you are integrating with.
This matters because the header names and signing formats are different.
Step 2: prepare a sample payload
Start with a small JSON payload. For example:
{
"id": "evt_test_123",
"type": "payment.succeeded",
"data": {
"amount": 50000,
"currency": "IDR"
}
}
Keep the first test simple. If the small payload works, move to a real provider payload later.
The payload should be treated as raw text when signing. This is important. Changing spaces, line breaks, or key order can change the signature result depending on the provider.
Step 3: choose or generate a secret
Webhook signatures usually use a shared secret.
For testing, you can use something obvious:
whsec_test_secret
For production, use a long random secret from the provider dashboard or a secure secret generator. Do not hard-code production secrets in frontend code. Webhook secrets belong on the server.
Step 4: generate the signature
In the Webhook Tester, choose the provider style, paste the payload, and enter the secret.
The tool builds the signature header that your backend should expect.
For Stripe-style webhooks, the header may look like:
Stripe-Signature: t=1790467200,v1=<signature>
For GitHub-style webhooks, it may look like:
X-Hub-Signature-256: sha256=<signature>
For Shopify-style webhooks, it may look like:
X-Shopify-Hmac-Sha256: <signature>
The exact header matters. If your backend reads the wrong header, verification will fail even if the secret is correct.
Step 5: verify the signature
Now use the verification part of the tool with the same payload, secret, and header.
If verification passes, it means:
- the payload matches the signature;
- the secret is correct;
- the selected provider format is correct.
If verification fails, check these first:
- did you choose the correct provider style?
- did you paste the same payload text?
- did the payload get reformatted?
- did you use the right secret?
- did you include the right timestamp or header prefix?
Most webhook signature bugs are caused by signing one version of the payload and verifying another version.
Step 6: understand raw body vs parsed JSON
This is one of the most common webhook mistakes.
Many web frameworks parse JSON automatically. That is convenient for normal API routes, but it can break webhook verification.
For signature checking, providers usually expect you to verify the raw request body, exactly as received.
This can fail:
receive raw JSON -> parse it -> stringify it again -> verify
because the final string may not be byte-for-byte identical to the original body.
The safer flow is:
receive raw body -> verify signature -> parse JSON -> handle event
Verify first, parse after.
Step 7: check the timestamp
Some providers include a timestamp in the signature header. Stripe-style signatures,
for example, include t=.
The timestamp helps prevent replay attacks.
A replay attack is when someone captures a real webhook and sends it again later. The signature may still be valid, but the request is old.
A backend usually checks that the timestamp is recent, for example within five minutes.
So your server should check:
- Is the signature valid?
- Is the timestamp recent enough?
- Has this event id already been processed?
Step 8: make the event handler idempotent
“Idempotent” means safe to run more than once.
Webhook providers may retry delivery if your server is slow or returns an error. That means your app may receive the same event more than once.
If the event is:
{
"id": "evt_test_123",
"type": "payment.succeeded"
}
store evt_test_123 after processing it. If the same event arrives again, skip the
side effect.
This prevents bugs like:
- marking the same invoice paid twice;
- sending two emails;
- adding duplicate credits;
- creating duplicate orders.
Step 9: send a test request carefully
The Webhook Tester can help prepare a test request. When sending it to your local or staging endpoint, check:
- the URL is correct;
- the method is
POST; - the
Content-Typeis correct, usuallyapplication/json; - the signature header is included;
- your backend reads the raw body;
- your backend logs enough detail to debug failures.
Do not start by testing against production. Use local development or staging first.
Step 10: decide what to log
Webhook logs are helpful, but be careful not to log secrets.
Good things to log:
- provider name;
- event id;
- event type;
- timestamp;
- verification result;
- reason for rejection.
Avoid logging:
- webhook secret;
- full payment details if not needed;
- sensitive customer data;
- authentication tokens.
A good rejection log might say:
webhook rejected: invalid signature, provider=stripe, event_id=evt_test_123
That is useful without leaking the secret.
My webhook checklist
When testing a webhook, I go through this checklist:
- Choose the correct provider format.
- Use a small known JSON payload first.
- Generate a signature with the test secret.
- Verify the same payload and header.
- Make sure the backend verifies the raw request body.
- Check timestamp tolerance if the provider uses timestamps.
- Store event ids to avoid processing duplicates.
- Test locally or in staging before production.
- Log verification failures without logging secrets.
The key lesson is simple:
A webhook is not trusted because it arrived. It is trusted only after the signature, timestamp, and event id checks pass.
The Webhook Tester gives you a safe place to practice that flow before wiring it into a real backend.
Comments
Comments are welcome — please read the comment policy first. Powered by giscus and GitHub Discussions.