hookinlet receives webhooks into named inboxes, verifies their signatures, and keeps a queryable history. Agents read it over REST, SSE, or MCP, with no glue code.
# give any MCP client the inbox (Claude Code shown);
# the token is printed when you create an inbox$ claude mcp add --transport http hookinlet \
http://localhost:8080/mcp \
--header "Authorization: Bearer $HOOKINLET_AGENT_TOKEN"
agent → wait_for_messages(inbox: "stripe", after: 41)
tool ← 1 message
#42 verified 19:04:11Z
untrusted: { event_type: "invoice.paid", … }
agent → "Invoice paid. Marking order 1182 as active."
# after the quick start: check Stripe's signature$ SECRET=$(stripe listen --print-secret)
$ curl -s localhost:8080/v1/admin/inboxes \
-H "Authorization: Bearer $HOOKINLET_ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"slug":"stripe","verifier":"stripe",
"verifier_secret":"'$SECRET'"}'
{"agent_token":"hki_…",
"inbox":{"ingest_url":"http://localhost:8080/in/Zk2m…", …}}
# forward real, signed events to that URL$ stripe listen \
--forward-to http://localhost:8080/in/Zk2m… &
$ stripe trigger invoice.paid
# read them back (forged events get a 401
# and are never stored)$ curl -H "Authorization: Bearer hki_…" \
'localhost:8080/v1/inboxes/stripe/messages?type=invoice.paid'
{"messages":[{"seq":7,"verified":true,
"event_type":"invoice.paid", …}], …}
# 1 · run it (keep both secrets)$ export HOOKINLET_ADMIN_TOKEN=$(openssl rand -base64 32)
$ export HOOKINLET_MASTER_KEY=$(openssl rand -base64 32)
$ docker run -d -p 127.0.0.1:8080:8080 -v hookinlet-data:/data \
-e HOOKINLET_ADMIN_TOKEN -e HOOKINLET_MASTER_KEY \
ghcr.io/hookinlet/hookinlet
# 2 · create an inbox (this prints its URL and token)$ curl -s localhost:8080/v1/admin/inboxes \
-H "Authorization: Bearer $HOOKINLET_ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"slug":"demo","verifier":"none"}'
{"agent_token":"hki_…",
"inbox":{"ingest_url":"http://localhost:8080/in/q3V9…", …}}
# 3 · send it a webhook, then read it back$ curl -d '{"hello":"agents"}' http://localhost:8080/in/q3V9…
{"seq":1}
$ curl -H "Authorization: Bearer hki_…" \
localhost:8080/v1/inboxes/demo/messages
{"messages":[{"seq":1,"body":{"hello":"agents"}, …}], …}
# verifier "none" checks nothing; real senders use
# stripe, github, slack… (next tab)
how it works
Senders post. hookinlet holds. Agents read when they're ready.
Every inbox has its own random ingest URL, its own signing secret and its own sequence. Senders get their 2xx only after the event is committed; readers catch up whenever they run.
1 · sender
Any webhook source
stripe → /in/q3V9xR1h…
github → /in/Zk2mP8uW…
slack → /in/b6A0yTnc…
POST
2 · inboxstripe
Verify → append → retain
#40customer.created✓
#41invoice.created✓
#42invoice.paid✓
Signature checked on arrival. Each event gets the next sequence number. Kept for the retention you set.
after=41
3 · agent
Reads at its own pace
REST · poll
SSE · stream
MCP · tools
1 binary
static Go, or one Docker image
SQLite
built in, nothing else to run
3 protocols
REST · SSE · MCP
AGPL-3.0
read every line
consume
Three ways to read an inbox. Same cursor everywhere.
A cursor is the last sequence number you handled. Pass it back and you resume exactly where you stopped — across restarts, reconnects and protocols.
REST
poll
Page through history. Good for cron jobs and batch agents.
One endpoint, four read-only tools. wait_for_messages is the whole polling loop in one call, and what the sender wrote is kept under untrusted.
# endpoint, with an agent token
http://localhost:8080/mcp
# tools
list_inboxes()
query_messages(inbox, after, limit, bodies)
get_message(inbox, seq)
wait_for_messages(inbox, after, timeout_seconds)
# in a session
→ wait_for_messages("slack", after=56)
← #57 verified
untrusted: { event_type: "app_mention", … }
console
See what is arriving.
Open /admin on your server and sign in with the admin token. It does what the command line does, in a browser.
✓Every message: body, headers and the exact bytes
✓Follow new messages live
✓New inboxes, with a form for each kind of sender
✓Secrets, tokens, retention and backups
Built into the binary; turn it off with HOOKINLET_ADMIN_UI=false. What a sender wrote is always shown as text, never as HTML.
sample data
Small surface. The boring parts done right.
Signature presets
Stripe, GitHub, Slack, Standard Webhooks (Svix), a configurable HMAC and a shared token, all checked on the exact bytes sent. Set verifier: stripe and the secret.
Replay-safe cursors
Per-inbox sequence numbers, assigned in commit order. Keep the last one you handled and you see every message exactly once. You are told if retention removed messages you had not read, or if the inbox was restored or recreated.
Retention per inbox
Keep by age, by count, or both. Messages expire on your schedule, never because a reader was slow.
Single binary
One static Go binary or one Docker image. SQLite by default, so there's nothing else to run.
Config as code
Declare inboxes in a YAML file and run hookinlet apply. It creates what's missing, changes only what differs, and never deletes. Secrets come from the environment, never the file.
Agent tokens
Issue as many named read-only tokens as you need, each scoped to some inboxes or all. Replace one by issuing a new one and revoking the old, with no gap. Every message says whether it was verified.
Exact bytes kept
Bodies are stored byte for byte. Read them back from /raw and the sender's signature still verifies.
Bring your own host
A VPS, a container, a box under your desk: one process per data directory, with TLS in front. No telemetry, no phone-home.
Committed before 2xx
A sender gets its 2xx only after the message is committed and synced to disk. Forged signatures get a 401 and are never stored. Verifier secrets are encrypted at rest and tied to their inbox.
The same inboxes on a hosted endpoint, with nothing to deploy. Tell us what you'd use it for and we'll shape it around that.
Join the waitlistFive short questions about your use case, volume and what you would pay. It opens a form on another site. We email once, when Cloud opens. No newsletter, no sharing. Privacy