Phone:

Hidden from the page source until you click: friction against scrapers, not a guarantee.

Email:

[email protected]

Role:

Sole designer & engineer

Stack:

Go 1.24, standard library only, distroless container

Status:

v3.0, live at webhooks.dixon.cx, GPLv3

Webhooks to Email

Point any webhook at a URL with your email address in it and the whole request lands in your inbox, formatted so you can actually read it. No account, no dashboard, no database. It exists because "what is this service actually sending me?" should be a thirty-second question, not an afternoon of print statements.

  • Go
  • Developer tools
  • Email
  • Open source
  • Zero dependencies
Webhooks to Email
The itch

Every integration I have ever built started the same way: a third party promises to call my endpoint, and I have no idea what the payload looks like until it arrives. The documentation is out of date, the sample is from two versions ago, and the only way to find out is to stand up a throwaway endpoint, log everything, and squint at the output. I have done that dance more times than I care to count, and one evening I decided I was never doing it again.

So the URL became the configuration. Encode your email address, put it in the path, and that is the entire setup. Add a parser name on the end and the email arrives formatted for the service that sent it. The landing page has a URL builder if you would rather not think about percent-encoding an at sign.

What I built

A single Go binary, about 1,400 lines including tests, with no third-party dependencies at all. It accepts any HTTP method and any content type, captures headers, query string, form fields, uploaded file names and the raw body, and sends the lot over SMTP as a properly built HTML email: quoted-printable body, RFC 2047 subject so the emoji survive every relay in the chain, and real tables rather than divs so it lays out correctly in Gmail, Outlook and Apple Mail.

Every request gets a structured JSON acknowledgement rather than a wall of debug output. It tells you which parser ran, what subject line went out, how many headers arrived, how big the body was, and echoes the decoded JSON back so you can see exactly what the server understood. A failed send returns a 502 instead of a false 200, so the sending service retries rather than believing the delivery succeeded.

Four parsers ship with it. GitHub renders pushes as commit cards with linked hashes and full multi-line messages, plus pull requests, issues and releases. Grafana and Prometheus Alertmanager payloads come out with firing or resolved status, grouped labels and one card per alert. The generic JSON parser turns any body into tables, splitting nested objects out with path breadcrumbs so a value five levels deep is still findable. And Webex Interact, because I spend my working life near SMS gateways, decodes every event type that API emits. If a parser cannot make sense of the payload it falls back to the raw format, so nothing is ever lost.

Engineering decisions
  • No dependencies, on purpose: the whole thing is net/http, net/smtp, encoding/json and html/template. There is nothing to audit, nothing to update, and the container is a static binary on distroless running as a non-root user.
  • Store nothing: the request is forwarded and forgotten. There is no log of payloads, no replay, no retention. That is a deliberate privacy stance as much as a simplification.
  • Escape everything: every free-text value goes through one escaping function that also preserves line breaks, so a commit message or an SMS body cannot inject markup into the email.
  • Trust the proxy, not the client: the reported client IP is only taken from forwarding headers when the peer is a private address. A public peer cannot spoof its origin, and there is a test that proves it.
  • Byte-for-byte compatibility: the Go rewrite replaced a PHP service. The emails and the JSON response are identical to the old version, down to JSON object keys kept in sender order, so nobody's inbox filters broke.
  • A parser is one function: adding support for a new service means one file with one function, registered in a map. The parser guide in the repo walks through it.
What it demonstrates

Taking a tool that was good enough and making it properly finished: a rewrite that kept every observable behaviour, a test suite covering routing, the JSON contract, IP forwarding and every parser, and a deployment that is one systemd unit and one container. It is small by design, and the smallness is the point.

Using it

Replace the at sign in your email address with %40 and use https://webhooks.dixon.cx/you%40example.com as the webhook endpoint. Add /github, /grafana, /json or /wxinteract to the end for a formatted email. Plus addressing works, so you+stripe%40example.com lets you filter by source.

Anyone who has the URL can send mail to that address, so treat it as a debugging tool rather than a production integration. Bodies over 10 MB are refused, and an invalid address gets a 400 with a hint rather than a silent drop.

Prefer to run your own? Clone the repo, go build, and set three environment variables for the listen address, SMTP relay and From address. There is a Dockerfile and the exact systemd unit I run in production.