Skip to content
integration

Debug webhooks without deploying: a temporary inbox you can spin up in 10 seconds

| 5 min read
Notification bell with incoming data streams
Photo by Brett Jordan on Unsplash

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

  1. Create an inbox to get a unique receive URL
  2. Point your webhook source at that URL
  3. 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.

Developer debugging with network inspector open
Photo by Fotis Fotopoulos on Unsplash

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.