// no signup. no dashboard. no, seriously.

Webhooks.
Straight to your inbox.

Put your email in the URL. Point a webhook at it. Every header, every byte of body, every superglobal shows up as an email. Done. Go do the next thing.

https://webhooks.dixon.cx/you%40example.com

Yes the @ becomes %40. Yes that's the whole trick. Add /github, /json, etc. on the end for the fancy parsers — more on that below, if you scroll that far.

Three steps. Two, really.

The third one is just "wait for an email", which you were going to do anyway.

Encode your email@%40. The box above already did it for you. Ok moving on.
Paste the URL into whateverStripe, GitHub, Grafana, that cron job, a curl you're about to forget you wrote.
Check your inboxFull request, nicely formatted. Subject starts with ‼️ so it's filterable. You're welcome.

The fastest possible test

curl -X POST https://webhooks.dixon.cx/you%40example.com \
  -d '{"test": "hello, is this thing on"}'

(the email will arrive before you've alt-tabbed back. probably. depends on your mail server. anyway.)

Examples, because reading is hard

Any method, any content type. GET, POST, PUT, PATCH, DELETE, something you made up. JSON, form data, multipart, XML, plain text, a string of emoji. It all gets forwarded.

# JSON
curl -X POST https://webhooks.dixon.cx/you%40example.com \
  -H "Content-Type: application/json" \
  -d '{"event": "user.created", "user_id": 12345}'

# GET with query string (yes, this works too)
curl "https://webhooks.dixon.cx/you%40example.com?event=test&id=123"
fetch('https://webhooks.dixon.cx/' + encodeURIComponent('you@example.com'), {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ event: 'order.completed', order_id: 'ORD-789', total: 99.99 })
});
import requests
from urllib.parse import quote

requests.post(
    f'https://webhooks.dixon.cx/{quote("you@example.com")}',
    json={'event': 'payment.received', 'amount': 250.00},
    headers={'X-Custom-Header': 'MyValue'},
)

Pro tip you'll ignore and then come back for: use plus addressing. you+stripe%40example.com, you+github%40example.com. Now your inbox filters do the sorting so you don't have to.

Parsers. Ooh, shiny.

Add one word to the end of the URL and the email stops being a wall of raw JSON and starts being something you can actually read at 3am. Parser doesn't understand the payload? It falls back to the raw format. Nothing gets lost.

🐙
/github

Push, pull request, issue and release events get their own layout with commits, authors, branches and links. Everything else still renders, just less fancy. Subjects like 📤 GitHub push - you/repo.

📈
/grafana

Grafana and Prometheus Alertmanager alerts. Firing, resolved, labels, annotations. Finally know which thing is on fire.

🧩
/json

Any JSON at all, rendered as tables. Nested objects get split out into their own tables with path-based titles so you can find data.items[3].sku without squinting.

💬
/wxinteract

Webex Interact SMS events. Inbound messages, delivery receipts, the lot, with the event type in the subject line.

Setting up GitHub, since that's the one everyone asks about

  1. Repo → Settings → Webhooks → Add webhook
  2. Payload URL: https://webhooks.dixon.cx/you%40example.com/github
  3. Content type: application/json
  4. Pick events (or "Send me everything", live a little)
  5. Save. Push something. Refresh inbox. Refresh again. There it is.

What actually lands in your inbox

One HTML email per request. Gmail-safe styling. All of the following, every time, whether you asked for it or not:

And the sender gets

{
    "ok": true,
    "message": "Webhook received and forwarded to you@example.com",
    "forwarded_to": "you@example.com",
    "parser": "default",
    "subject": "‼️ Webhook Request Received - 2026-09-11 17:15:42",
    "received_at": "2026-09-11T17:15:42+00:00",
    "request": { "method": "POST", "content_type": "application/json", "ip": "203.0.113.7", "headers": 9, "body_bytes": 40 },
    "data": {
        "query": { "src": "curl" },
        "form": {},
        "json": { "event": "user.created", "user_id": 12345 },
        "files": []
    }
}

JSON bodies get decoded and echoed back under data.json, so you can eyeball what the server actually understood. Form fields land in form, query string in query. No print_r in sight.

Any 200 is enough for most services to mark the delivery as "succeeded" and stop retrying. Which is the point.

Caveats, small print, that sort of thing

Anyone with the URL can email you. There's no auth. There's no secret. It's your email address in a URL. Use it for debugging and testing. Do not point production payment data at it. Don't make me put a bigger warning here.

Stuff that's true

Good for

debugging a webhook you didn't write seeing what Stripe actually sends capturing real payloads for docs testing before the handler exists "is it even firing?" alerts in your inbox without a whole thing