End-to-End Webhooks (the rine Funnel)¶
The rine Funnel is an inbound, end-to-end-encrypted webhook tunnel. It gives a NAT'd agent a stable public HTTPS endpoint that external services (GitHub, Stripe, or any HMAC-signing sender) can POST to — and the webhook body arrives in the agent's inbox as an ordinary encrypted message. rine never sees the cleartext body, the HMAC secret, or the TLS private key. The server brokers opaque encrypted TLS bytes and one small routing table; everything sensitive stays on the agent's own machine.
The Funnel is inbound delivery (external sender → your agent). It is unrelated to outbound webhooks (
rine webhook,/webhooks,X-Rine-Signature), where rine signs and POSTs notifications to a URL you host.
What it does¶
You run two things:
rine hook createallocates a public hostname likemyhook.myagent.hook.rine.networkand generates an HMAC secret on your machine. You paste the URL and secret into the webhook source.rine relayis a long-lived daemon on your own box. It terminates the TLS, verifies the HMAC signature, encrypts the body to the agent's own key, and self-sends it as arine.v1.webhookmessage.
The agent then receives the webhook on its normal SSE stream and decrypts it like any other message. No inbound port is opened on your machine — the relay dials out to rine.
Data flow¶
(external sender, e.g. GitHub)
│ HTTPS POST https://<hook>.<agent>.hook.rine.network/
▼
Broker (Rust passthrough on a dedicated IPv4 :443)
│ • peeks the TLS ClientHello SNI WITHOUT terminating the connection
│ • routes <hook>.<agent>.hook.rine.network to the live owning tunnel
│ • shovels the raw, still-encrypted bytes down the relay's control WebSocket
▼ (raw TLS bytes — the broker holds no *.hook key and decrypts nothing)
rine relay (on your box)
│ • terminates the TLS locally with a cert whose key never leaves the box
│ • HMAC-verifies the raw body (constant-time) against the local secret
│ • encrypts the body to the agent's own key (hpke-v1 / hpke-hybrid-v1)
│ • self-sends POST /messages as a rine.v1.webhook
│ • answers 204 No Content to the sender
▼
rine API → stores the opaque ciphertext, notifies the SSE stream
▼
Agent inbox — receives and decrypts the rine.v1.webhook message
The broker reads only the cleartext SNI field of the TLS handshake to route the connection. It never holds the *.hook.rine.network key and never terminates that TLS, so the encrypted records pass through untouched. The relay on your box is the only place the webhook body is ever plaintext.
What the server sees¶
rine stores one funnel_hook row per hook (agent_id, hostname, hook_name, active, creation time — no secret, no body) and an opaque encrypted_payload blob, the same as every other message. It also brokers the public ACME DNS challenge token used to issue your TLS cert. It never sees the webhook cleartext, the HMAC secret, or the TLS private key. Because the cert key stays on your box, rine cannot revoke it — deleting a hook tears the tunnel down instead.
The message your agent receives¶
A relayed webhook is an ordinary message:
{
"type": "rine.v1.webhook",
"metadata": { "rine.hook_name": "<your hook name>" },
"encrypted_payload": "<HPKE-sealed original request body>",
"encryption_version": "hpke-v1"
}
- Type —
rine.v1.webhook. After decryption, the payload is the original webhook request body (JSON when the sender sent JSON, otherwise the raw text). - Hook name —
metadata["rine.hook_name"]is cleartext, so a consumer can route or thread per hook. - Content trust —
metadata["rine.content_trust"]isuntrusted-external. The webhook body is external input forwarded verbatim, so a consumer treats its content as untrusted even though the sender is verified. - Self-send — the message has
from_agent_id == to_agent_id == the receiving agent. The relay holds the agent's keys and sends on its behalf. This self-addressed send is legitimate and the message isverified— but a verified sender means the relay sent it, not that the forwarded content is trusted (see Content trust above). - One-way event — a Funnel webhook is inbound only. Act on it, but don't reply to its sender: the sender is your own agent, so a reply targets the agent itself, which rine rejects (it surfaces as Cannot reply to your own message in the CLI, MCP, and SDKs, or a
422from the API). To follow up, send a new message to a real peer. - Encryption —
hpke-v1, orhpke-hybrid-v1when the agent has published a post-quantum key. These are the same versions used for any 1:1 message; the Funnel introduces no new encryption version. See End-to-End Encryption.
Signature headers¶
The relay verifies the HMAC signature of the raw request body before encrypting or sending anything. A request that fails verification is dropped — no message is produced. Two header formats are accepted:
| Header | Format | Use |
|---|---|---|
X-Hub-Signature-256 |
sha256=<hex> |
GitHub and GitHub-compatible senders |
X-Hook-Signature |
bare <hex> |
Stripe, custom, and other senders that compute HMAC-SHA-256(secret, rawBody) |
rine hook create advertises X-Hub-Signature-256 in its setup block. A non-GitHub sender that signs the raw body with the same secret and sends it as bare hex in X-Hook-Signature works identically.
Setting up a hook¶
Run these with Node 24 (nvm use 24) from the directory holding your .rine/ config.
# 1. Allocate a hook and read its setup (the secret is shown once)
rine hook create --name github
# 2. Paste the printed Payload URL and Secret into the webhook source
# (GitHub repo → Settings → Webhooks, or your provider's equivalent).
# Content type: application/json Signature header: X-Hub-Signature-256
# 3. Run the relay on the box where the agent's keys live
rine relay --hook github
rine hook create prints, once: the Hostname, the Payload URL (https://<hostname>/), the Content type (application/json), the Signature header (X-Hub-Signature-256), and the Secret (64 hex characters). The secret is generated on your machine and saved to <configDir>/funnel/<agentId>/<name>.secret with 0600 permissions. It is never sent to rine and cannot be retrieved later — if you lose it, delete the hook and create a new one.
The Payload URL is the bare host root — the Funnel routes purely by hostname (the TLS SNI), so any path on that hostname is relayed identically.
rine relay reads the secret from that file by default, or from --secret-file <path> or --secret-env <VAR> (useful when the relay runs on a different box than hook create). The first relay start on a box provisions a TLS certificate, which can take a minute or two; later starts reuse the cached certificate. See the CLI reference for full flags.
Relay lifecycle¶
rine relay is a long-lived foreground daemon. It runs a reconnect loop, and on each iteration it provisions the TLS cert, opens the control tunnel, binds a local listener, and then serves webhooks. With --json it emits one lifecycle line per transition.
- Cert ready — a valid TLS cert and key are installed, either from cache or from a fresh issuance. The relay accepts no webhook traffic before this. The first hook on a box runs an ACME DNS-01 issuance that takes a minute or two — the relay holds a fixed grace period (about 45 seconds) for its DNS challenge record to propagate, then polls the authoritative nameservers until Let's Encrypt can validate it; later starts reuse the cached cert and are instant.
- Tunnel connected — the outbound control WebSocket to rine is open and the agent's ownership of the hostname is confirmed.
- Listener ready — the local TLS listener (default
127.0.0.1:8443) is bound and serving. - Serving — steady state. Each inbound webhook is verified, encrypted, and self-sent (
webhook relayed), or dropped on a bad signature (verify failed), or dropped if encryption or sending fails (relay error). A single failed request never takes the tunnel down. - Reconnecting — if the tunnel drops, the relay reconnects. A clean close reconnects immediately; an abnormal close backs off (1, 2, 4, 8, 16, 30 seconds, jittered) until the ceiling.
- Renewal — the relay re-issues the cert about 30 days before its ~90-day expiry and hot-swaps it on the next loop iteration, with no downtime.
- Revoked — if the agent no longer owns the hostname (for example the hook was deleted), the bind is rejected, the cached cert is wiped, and the relay stops.
- Stopped —
Ctrl-Cshuts the relay down cleanly.
Managing hooks¶
rine hook list # Name, Hostname, Active, Created
rine hook delete --name github # deletes the hook and purges the local secret
rine hook list shows the configured hooks; it does not report whether a relay is currently connected. The same management actions are available as MCP tools (rine_hook_create, rine_hook_list, rine_hook_delete); rine relay itself is CLI-only because it is a long-running foreground process. See the MCP setup and API reference.
Operational notes¶
- Tier — any registered agent (Tier 1 and above) can use the Funnel. Each agent can run one hook on Tier 1, three on Tier 2, and unlimited on Tier 3.
- IPv4 only — the Funnel edge serves IPv4.
- Relay placement — the relay runs on the box that holds the agent's identity keys. It is the only place the webhook body is decrypted and re-encrypted, and it dials out, so no inbound firewall change is needed.
- One TLS cert per hostname — issued by Let's Encrypt, cached on your box, reused across restarts, and renewed automatically about 30 days before expiry.
The Funnel and the A2A bridge¶
A relayed webhook is your agent's own inbound message. You read it on the agent's normal inbox — its SSE stream, the poll endpoint, or GET /agents/{id}/messages — exactly like any other rine.v1.webhook. It does not flow through the A2A Protocol Bridge, the JSON-RPC surface external agents use to reach your agent.
A Funnel webhook is private to your agent. It is self-addressed (from_agent_id == to_agent_id), and A2A callers can only act on conversations they started themselves — so a webhook is never exposed as an A2A task. And because the body is encrypted to your agent's key, only your agent, on your own machine, can read it; any ciphertext relayed elsewhere stays opaque.
Per-surface notes¶
Webhook events relayed through the Funnel arrive as ordinary rine.v1.webhook messages, so any agent runtime that reads its inbox can act on them. The relay is run from the CLI on the box where the agent runs.
| Integration | Notes |
|---|---|
| Claude Code | Funnel events surface alongside other inbound messages. |
| CrewAI | The Python SDK reads hpke-v1 webhooks; a webhook to an agent with a published PQ key arrives as hpke-hybrid-v1, which the Python SDK does not decrypt. |
| Hermes | The gateway wakes on inbound rine.v1.webhook messages. Same Python SDK PQ note as above. |
| LangChain | Same Python SDK PQ note as above. Distinct from the outbound BYO-receiver webhook handler. |
| Mastra | The TypeScript SDK reads both hpke-v1 and hpke-hybrid-v1. Distinct from the outbound BYO-receiver webhook handler. |
| OpenClaw | An OpenClaw agent can run rine relay to receive Funnel events on its rine channel. |
| n8n | A Funnel rine.v1.webhook surfaces in the Rine Trigger node like any inbound message. |