Capturing webhooks with a bin
See exactly what a webhook sender sends, and make the receiver fail on purpose.
A webhook is an HTTP request your system receives when something happens somewhere else: a payment succeeds, a commit is pushed, an order ships. Testing webhooks is awkward because the request comes to you, so you need a public address to receive it and a way to see exactly what arrived. A webhook bin is both: a URL that records every request sent to it, and a page that shows them.
This lesson uses the bins on the webhook testing API. They never send anything themselves; they only capture what you, or a service you're testing, send to them.
Creating a bin
$ curl -X POST https://api.testingapis.com/api/webhooks/bins \
-H "Content-Type: application/json" \
-d '{"name": "orders"}'
{
"message": "Webhook bin created",
"data": {
"id": "3409c114654d",
"name": "orders",
"createdAt": "2026-10-11T12:42:34Z",
"requestCount": 0,
"url": "https://api.testingapis.com/api/webhooks/3409c114654d",
"inspectUrl": "https://api.testingapis.com/api/webhooks/bins/3409c114654d"
}
}url is where webhooks go. inspectUrl is where you read them. The bin accepts any method, any path below its URL, and any body.
Sending and inspecting
Send it something the way a webhook sender would:
$ curl -X POST https://api.testingapis.com/api/webhooks/3409c114654d \
-H "Content-Type: application/json" \
-H "X-Event-Id: evt_1" \
-d '{"event":"order.created","id":42}'
{"received":true,"binId":"3409c114654d","requestId":"req_b6b9584f0642","message":"Request captured"}Then look at what it recorded:
$ curl https://api.testingapis.com/api/webhooks/bins/3409c114654d
{
"id": "3409c114654d",
"name": "orders",
"requestCount": 1,
"requests": [
{
"id": "req_b6b9584f0642",
"method": "POST",
"path": "/api/webhooks/3409c114654d",
"subPath": null,
"query": {},
"headers": {
"Accept": "*/*",
"User-Agent": "curl/8.18.0",
"Content-Type": "application/json",
"Content-Length": "33",
"x-event-id": "evt_1",
...
},
"contentType": "application/json",
"bodySize": 33,
"bodyEncoding": "utf-8",
"bodyTruncated": false,
"body": "{\"event\":\"order.created\",\"id\":42}",
"receivedAt": "2026-10-11T12:42:34.310Z"
}
]
}Everything that arrived, in order, newest first: method, path, query string, every header, and the body exactly as sent. When you're building or testing a webhook sender, this is the ground truth. Libraries and frameworks add, remove and rename headers, reformat JSON and change encodings, and the bin shows you what actually went over the wire rather than what the code meant to send.
Look at the header names too. curl sent X-Event-Id, and it arrived as x-event-id, while Content-Typekept its capitals. Something between the client and the server, most likely the hosting platform's proxy, lower-cased the custom header on the way. HTTP header names are case-insensitive, so nothing is wrong, but a receiver that looks up headers["X-Event-Id"]in a plain object finds nothing. Receivers should match header names without caring about case, because infrastructure you don't control can change it.
Binary bodies
Text bodies are stored as text. Anything else is stored in Base64, so it survives being shown as JSON:
"contentType": "application/octet-stream",
"bodySize": 4,
"bodyEncoding": "base64",
"body": "AP8BAg=="Decode it with the Base64 tool and you get the four bytes that were sent: 0, 255, 1, 2.
Making the receiver fail on purpose
A webhook sender has to cope with receivers that are down or broken. The bin can play a broken receiver: add ?status= and it answers with that code, while still recording the request.
$ curl -i -X POST "https://api.testingapis.com/api/webhooks/3409c114654d/orders/created?status=500" \
-H "Content-Type: application/json" \
-d '{"event": "order.updated", "id": 42}'
HTTP/1.1 500 Internal Server Error
{"received":true,"binId":"3409c114654d","requestId":"req_89f1cfff2e35","message":"Request captured"}That request also shows the extra path: anything after the bin ID is recorded as subPath, here orders/created, so one bin can stand in for several endpoints. ?status=204answers with no body, which some receivers do. A request to a bin that doesn't exist gets 404. Send that case too, because a sender should treat a 404 differently from a 500: retrying a 500 can make sense, but no number of retries will make a missing endpoint appear.
To see the signed sample events used in the next lesson, which come with their signature headers, ask for the list:
/api/webhooks/samplesTesting a webhook sender with a bin
- Create a bin and register its
urlas the webhook address in the system you're testing. - Trigger the event: place the order, push the commit, change the record.
- Read the bin and check the request: method, path, content type, every field of the body, and the event headers.
- Check the signature header against the shared secret, as in the next lesson.
- Switch the bin to
?status=500and check the sender retries, how often, and when it gives up.
| Limit | Value |
|---|---|
| Bins kept | 100; the oldest is removed when a new one would exceed it |
| Requests kept per bin | 50, newest first |
| Body size recorded | 256KB; anything bigger is cut, with bodyTruncated: true |
Exercise
Create a bin and send it three requests: a JSON POST, a form-encoded POST with Content-Type: application/x-www-form-urlencoded, and a PUT to /orders/7 below the bin with ?status=503. Inspect the bin and note the bodyEncoding, subPath and query of each. Then delete the bin and check its inspect URL now returns 404.