Webhooks,
delivered home.
A public endpoint for your local application. Receive events while your machine is offline, then deliver them over an outbound connection when you return.
Durable queue · encrypted storage · explicit ACKsGet connected.
Use Node.js 22+ and a local checkout of the AgentHook project. Run these commands from its directory. Cloudflare credentials are only needed to deploy the service.
01 / Create your inbox
Generate a local identity and register it. Save the webhook URL printed by this command.
npm ci
node bin/agenthook.js init \
--server https://hook.houseAlready have an identity? Run register instead of init. Back up agenthook.identity.json; its private key is required to decrypt your queue.
02 / Bring events home
Start your application on port 3000, then start the forwarder in another terminal.
node bin/agenthook.js listen \
--server https://hook.house \
--forward http://localhost:3000WebSockets notify the client when events arrive. Polling takes over if that connection is unavailable.
03 / Send a webhook
Replace FINGERPRINT with the 64-character inbox fingerprint printed at registration. Set your webhook provider to this URL, with your chosen path.
curl -X POST 'https://hook.house/FINGERPRINT/github' \
-H 'Content-Type: application/json' \
-d '{"hello":"home"}'The sender gets an empty 200 OK after durable storage. Your application receives POST /github with the original body and application signature headers. The forwarder ACKs completed 2xx responses except 202; failures retry.
A small API. A durable inbox.
Base URL: https://hook.house. Below, :key is the SHA-256 fingerprint of the raw Ed25519 public key. Consumer routes require signed owner requests; use the CLI or requestInbox from src/client.js to sign requests and decrypt responses.
| Method / route | Purpose |
|---|---|
GET /health | Public service health: {"ok":true}. |
POST /:key/your/path | Public ingestion into a registered inbox. Stores ordinary deliveries and returns an empty 200. MCP verification requests get the challenge echoed as JSON instead. No owner signature required. |
POST /v1/inboxes/:key/register | Create or re-register an inbox with proof of key ownership. |
GET /v1/inboxes/:key/events | Claim the oldest available event. Include format=hpke-x25519-sha256-aes256gcm-v1 and optional leaseSeconds=30. Returns a lease and zero or one encrypted events. |
POST /v1/inboxes/:key/ack | Confirm processing: {"ids":["EVENT_ID"],"lease":"LEASE_TOKEN"}. |
POST /v1/inboxes/:key/nack | Release for retry. Same IDs and lease, with optional retryAfterSeconds and error. |
POST /v1/inboxes/:key/renew | Extend processing time. Same IDs and lease, with optional leaseSeconds. |
GET /v1/inboxes/:key/status | Inspect queue and processing counts. |
GET /v1/inboxes/:key/receipts/:id | Inspect an event's delivery state. Stored error text is encrypted. |
GET /v1/inboxes/:key/notifications | Authenticated WebSocket upgrade. Availability hints only; claim events over signed HTTP. |
Process with explicit confirmation
node bin/agenthook.js receive --server https://hook.house
# Finish processing, then acknowledge with the returned ID and lease:
node bin/agenthook.js ack --server https://hook.house \
--id EVENT_ID --lease LEASE_TOKEN
node bin/agenthook.js status --server https://hook.housereceive claims an event; it does not ACK. Pass --identity /path/to/file.identity.json to each command if you use a different identity file.
Signing requests directly
Send x-agenthook-key (base64 raw public key), x-agenthook-time (epoch milliseconds), x-agenthook-nonce (32 lowercase hex characters), and x-agenthook-signature (base64 Ed25519 signature). Sign the UTF-8 string below, with actual newline separators:
METHOD
PATH_AND_QUERY
TIME
NONCE
SHA256_BODY_HEXUse an uppercase method and the exact path and query, excluding the origin. Hash the raw request body bytes, or the empty body for GET. Requests expire after 60 seconds; nonce reuse is rejected.
Know what gets delivered.
- Events remain queued until ACKed. Delivery is at least once; deduplicate by event ID.
- Strict FIFO: a leased, delayed, or failing head event blocks later events.
- Maximum request body: 256 KiB. Maximum queue: 1,000 events per inbox. Oversized bodies return 413; full inboxes return 429.
- Lease duration: 5–300 seconds. Retry delay: 1–3,600 seconds.
- Webhook contents are encrypted before queue storage. Cloudflare still receives incoming HTTPS contents in plaintext before encryption.
- Keep your webhook URL private: anyone who knows it can submit events. Verify provider signatures in your local handler; delayed events can fail timestamp freshness checks.
This is a hosted pilot. Account-wide abuse controls, key recovery, and provider-specific freshness adapters are not implemented. Start with light use.
MCP Events: automatic challenge response.
Use your registered inbox URL, such as https://hook.house/FINGERPRINT/mcp, as the webhook destination for events/subscribe. AgentHook answers the MCP verification challenge immediately, even while your local client is offline. No tunnel or local challenge handler is needed.
For agents: supply the inbox URL as delivery.url, select delivery.mode: "webhook", and supply the provider-required whsec_ signing secret as delivery.secret. AgentHook is the receiver; event discovery and subscription calls go to the provider's MCP server.
A verification POST must include nonempty X-MCP-Subscription-Id, webhook-id, webhook-timestamp, and webhook-signature headers and this JSON body:
{"type":"verification","challenge":"YOUR_NONCE"}The response is HTTP 200 with {"challenge":"YOUR_NONCE"}. Handshakes are not queued; ordinary event deliveries are encrypted and queued for the local consumer.
AgentHook detects the handshake format without verifying its signature or storing the signing secret. Verify ordinary event signatures in your consumer; offline delays can affect timestamp freshness checks. This supports the MCP echo handshake, not every provider-specific verification protocol.