Debug webhooks without deploying: a temporary inbox you can spin up in 10 seconds
You want to see what Stripe sends when a payment succeeds. Or what GitHub sends when someone opens a pull request. The standard approach: start a local server, install ngrok, configure a tunnel, keep a terminal window open, hope the session does not expire mid-test. Or worse: deploy a half-finished handler to staging and tail logs while you wait.
Both workflows waste time on infrastructure when the real question is simple: *what does the payload look like?*
Botoi's webhook inbox gives you a disposable URL that captures incoming payloads and stores them for 24 hours. Three API calls. No accounts, no tunnels, no servers.
The 3-step workflow
- Create an inbox to get a unique receive URL
- Point your webhook source at that URL
- List the payloads to inspect what arrived
Each step is a single curl command.
Step 1: Create an inbox
curl -X POST https://api.botoi.com/v1/webhook/inbox/create Response:
{
"success": true,
"data": {
"inbox_id": "a1b2c3d4",
"url": "https://api.botoi.com/v1/webhook/inbox/a1b2c3d4/receive",
"expires_in": 86400
}
}
Save the inbox_id and url values. The inbox lives for 24 hours (86,400 seconds), then
the URL and all stored payloads are deleted.
Step 2: Send a webhook to the inbox
Point your webhook provider at the url from step 1. The inbox accepts any JSON body:
curl -X POST https://api.botoi.com/v1/webhook/inbox/a1b2c3d4/receive \
-H "Content-Type: application/json" \
-d '{
"event": "payment_intent.succeeded",
"amount": 4999,
"currency": "usd",
"customer_email": "buyer@example.com"
}' Response:
{
"success": true,
"data": {
"received": true,
"payload_id": "e5f6g7h8"
}
} Step 3: Inspect the payloads
curl -X POST https://api.botoi.com/v1/webhook/inbox/a1b2c3d4/list Response:
{
"success": true,
"data": {
"inbox_id": "a1b2c3d4",
"payloads": [
{
"id": "e5f6g7h8",
"received_at": "2026-03-26T10:00:00Z",
"body": {
"event": "payment_intent.succeeded",
"amount": 4999,
"currency": "usd",
"customer_email": "buyer@example.com"
}
}
],
"count": 1
}
} Every payload is stored with a timestamp and unique ID. You can call the list endpoint as many times as you want within the 24-hour window.
Real-world example: debugging a Stripe webhook
Stripe's test mode lets you trigger events from the dashboard. Instead of standing up a server to receive them, point Stripe at your inbox URL.
1. Create the inbox
INBOX=$(curl -s -X POST https://api.botoi.com/v1/webhook/inbox/create)
INBOX_ID=$(echo $INBOX | jq -r '.data.inbox_id')
INBOX_URL=$(echo $INBOX | jq -r '.data.url')
echo "Inbox URL: $INBOX_URL" 2. Add the URL to Stripe
Go to Stripe Dashboard > Developers > Webhooks.
Click "Add endpoint" and paste the INBOX_URL. Select the events you care about,
like payment_intent.succeeded and invoice.payment_failed.
3. Trigger a test event
Click "Send test webhook" in the Stripe dashboard. Then inspect what arrived:
curl -s -X POST "https://api.botoi.com/v1/webhook/inbox/$INBOX_ID/list" | jq '.data.payloads' You now have the exact payload Stripe sends, with all the nested objects, field names, and types. Use this to write your handler with confidence instead of guessing at the schema.
Real-world example: testing a GitHub webhook integration
GitHub repository webhooks fire on events like push, pull_request, and issues.
Here is how to capture one without running any code locally.
1. Create an inbox and configure GitHub
# Create the inbox
INBOX=$(curl -s -X POST https://api.botoi.com/v1/webhook/inbox/create)
INBOX_URL=$(echo $INBOX | jq -r '.data.url')
INBOX_ID=$(echo $INBOX | jq -r '.data.inbox_id')
# Add webhook to your repo via GitHub API
curl -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: application/json" \
"https://api.github.com/repos/your-org/your-repo/hooks" \
-d "{
\"config\": {
\"url\": \"$INBOX_URL\",
\"content_type\": \"json\"
},
\"events\": [\"pull_request\"]
}" 2. Open a pull request, then check the inbox
curl -s -X POST "https://api.botoi.com/v1/webhook/inbox/$INBOX_ID/list" \
| jq '.data.payloads[0].body | {action, number: .pull_request.number, title: .pull_request.title}' Output (example):
{
"action": "opened",
"number": 42,
"title": "Add rate limiting to /api/orders"
}
You can see the exact structure GitHub sends, including fields like action,
sender, repository, and the full pull_request object.
This is faster than reading GitHub's docs and guessing which fields are populated for each event type.
Automating it: a test script
This bash script creates an inbox, sends a test payload, retrieves it, and verifies the round trip.
Save it as test-webhook.sh and run it to confirm your integration works end to end.
#!/bin/bash
set -euo pipefail
API="https://api.botoi.com/v1/webhook/inbox"
echo "Creating inbox..."
INBOX=$(curl -s -X POST "$API/create")
INBOX_ID=$(echo "$INBOX" | jq -r '.data.inbox_id')
INBOX_URL=$(echo "$INBOX" | jq -r '.data.url')
echo "Inbox ID: $INBOX_ID"
echo "Receive URL: $INBOX_URL"
echo ""
echo "Sending test payload..."
SEND=$(curl -s -X POST "$INBOX_URL" \
-H "Content-Type: application/json" \
-d '{
"event": "order.created",
"order_id": "ord_98765",
"total": 129.99,
"items": [
{"sku": "WIDGET-A", "qty": 2},
{"sku": "GADGET-B", "qty": 1}
]
}')
PAYLOAD_ID=$(echo "$SEND" | jq -r '.data.payload_id')
echo "Payload ID: $PAYLOAD_ID"
echo ""
echo "Retrieving payloads..."
LIST=$(curl -s -X POST "$API/$INBOX_ID/list")
COUNT=$(echo "$LIST" | jq -r '.data.count')
if [ "$COUNT" -ge 1 ]; then
echo "Success: $COUNT payload(s) received"
echo "$LIST" | jq '.data.payloads[0].body'
else
echo "Error: no payloads found"
exit 1
fi Expected output:
Creating inbox...
Inbox ID: a1b2c3d4
Receive URL: https://api.botoi.com/v1/webhook/inbox/a1b2c3d4/receive
Sending test payload...
Payload ID: e5f6g7h8
Retrieving payloads...
Success: 1 payload(s) received
{
"event": "order.created",
"order_id": "ord_98765",
"total": 129.99,
"items": [
{"sku": "WIDGET-A", "qty": 2},
{"sku": "GADGET-B", "qty": 1}
]
} Comparison: botoi inbox vs. the alternatives
| Feature | Botoi inbox | ngrok | webhook.site | RequestBin |
|---|---|---|---|---|
| Setup time | One curl command | Install CLI + auth | Open browser | Open browser |
| Local server required | No | Yes | No | No |
| Account required | No | Yes (free tier) | No (limited) | Yes |
| Programmatic access | Full API | API (paid) | API (paid) | API (paid) |
| CI/CD friendly | Yes; curl + jq | Possible; complex | No | No |
| TTL | 24 hours | Session-based | Varies | 48 hours |
| Process stays running | No | Yes | No | No |
| Free | Yes | Limited | Limited | No |
The main advantage of the botoi inbox is that everything happens through the API. You can create inboxes, send test payloads, and retrieve results inside shell scripts, CI pipelines, and integration tests without opening a browser or keeping a background process alive.
When to use this
- Exploring a new webhook provider. Before writing handler code, capture real payloads to understand the data shape, field names, and edge cases.
- Integration tests in CI. Spin up an inbox in your test suite, trigger the webhook, poll the list endpoint, and assert on the payload contents.
- Debugging a broken handler. Temporarily swap your production webhook URL with an inbox URL to capture the exact payload causing failures.
- Pair programming or demos. Share the inbox ID with a teammate. Both of you can send payloads and inspect the results from different machines.
The inbox is disposable by design. Create one when you need it, use it for the session, and let it expire. No cleanup, no lingering endpoints, no billing surprises.
Frequently asked questions
- How long does a webhook inbox last?
- Each inbox expires after 24 hours. The inbox URL, all received payloads, and metadata are deleted at expiration. Create a new inbox whenever you need one.
- Do I need an API key to create an inbox?
- No. The free tier allows anonymous access at 5 requests per minute with IP-based rate limiting. You can start testing in seconds without signing up.
- Is there a size limit on webhook payloads?
- The receive endpoint accepts any valid JSON body. The standard Cloudflare Workers request size limit applies (100 MB for most plans).
- Can I use this with non-JSON webhooks?
- The receive endpoint expects a JSON body. If your webhook source sends form-encoded data or XML, you will need a small proxy to convert the payload to JSON before forwarding it to the inbox URL.
- How is this different from ngrok?
- ngrok creates a tunnel to a running local server. Botoi webhook inbox is a hosted endpoint that stores payloads for you to retrieve later. No local server needed, no CLI to install, no process to keep alive.
Try this API
Webhook Inbox API — interactive playground and code examples
More integration posts
Start building with botoi
150+ API endpoints for lookup, text processing, image generation, and developer utilities. Free tier, no credit card.