Inbound webhooks
A vendor that delivers events to a webhook URL (a payment processor, a campaign
platform, a CRM) cannot carry a NimbleBrain token, so it cannot call your server
directly. Declaring a hooks entry asks the host for a URL that it will accept
those deliveries on and forward to you.
hooks is a capability of the ai.nimblebrain/host block.
The host reads it from your server’s connectors catalog
entry only. A hooks block in an MCPB bundle manifest has no effect, because the
route and registration tool decide where a delivery is sent, and that has to come
from metadata the operator published.
Declare a stream
Section titled “Declare a stream”"_meta": { "ai.nimblebrain/host": { "host_version": "1.0", "hooks": [ { "vendor": "acme", "route": "/ingest/acme", "register_tool": "set_webhook_url", "description": "Order events: created, paid, refunded" } ] }}| Field | Type | Required | Description |
|---|---|---|---|
vendor |
string |
Yes | Slug for the vendor, lowercase alphanumeric with internal hyphens. One entry per vendor; a duplicate is dropped. |
route |
string |
Yes | Absolute path on your own server that deliveries are forwarded to. Must stay on your origin: no //host, no .., no backslash, no fragment, no scheme. |
register_tool |
string |
Yes | A tool on your server that the host calls with { vendor, url }, both strings, to hand you the minted URL. |
description |
string |
No | What the stream carries. Shown to operators; the host never acts on it. |
header_renames |
object |
No | { from: to } header renames applied before the forward. See signature headers below. |
What happens
Section titled “What happens”- When your server comes up in a workspace, the host mints a URL for exactly one
(workspace, connector, vendor)and calls yourregister_toolwith it. Store it, and register it with the vendor. - On delivery, the host checks the URL, then forwards the request to your
route, body bytes unchanged and the vendor’s own headers passed through. It arrives with the samex-tenant-id/x-workspace-ididentity headers a tool call gets. - Your server answers, and the host returns your status code to the vendor unchanged. Answer
2xxonce you have durably recorded the delivery; answer5xxonly when you want the vendor to retry.
The host never parses a delivery body. Signature verification, parsing, idempotency, and state are entirely yours. Vendors redeliver, so record each delivery under the vendor’s own event id and treat a repeat as a no-op.
Provisioning runs every time your connector’s connection comes up, not once at
install. That covers a fresh install, a host restart, and an interactive OAuth
flow that finishes long after the install returned. It mints only what is
missing. If your register_tool call itself errors, the host keeps the
registration; a rotation or a reinstall hands you the URL again.
Your register_tool must accept { vendor, url }
Section titled “Your register_tool must accept { vendor, url }”The host checks this against your advertised tools/list when the connection
comes up. If the tool is missing, or does not accept both arguments as strings,
no URL is minted for any of your streams and the operator is shown a warning
naming the declaration. The connector still installs, and its tools still work.
A stream whose registration tool is wrong would otherwise fail silently, months
later, at a vendor nobody is watching.
The check re-runs every time the connection is re-established and every time your advertised tool set changes, so fixing the manifest and reconnecting provisions the streams without a reinstall.
Your tools do not have to be ready the moment the host connects. A server that advertises nothing yet is not a contract failure: nothing is minted, nothing is reported wrong, and the streams provision as soon as your tool list appears.
@mcp.tool()def set_webhook_url(vendor: str, url: str) -> dict: # Store the URL in your own per-workspace config, then register it with # the vendor. Called again with a new URL whenever it is rotated. ...The host calls register_tool itself. That call is not subject to
admin_tools, so you do not need to, and should not,
name it there.
Signature headers
Section titled “Signature headers”Your vendor’s own headers reach you (Stripe-Signature, X-Acme-Signature,
whatever it uses), so you can verify the delivery came from the vendor and not
merely from someone holding the URL. Verify them. Possession of the URL
proves only that the host should forward the request to you for this workspace;
it does not prove who sent it.
One class of header does not reach you: anything a caller could use to assert
identity. That is Authorization, X-Api-Key, X-Tenant-Id, X-Workspace-Id,
X-User-Id, and everything under the reserved X-NB-* prefix. These are
stripped, because the only identity on a forwarded delivery must be the one the
platform verified. If your vendor signs on one of those headers, declare a
rename. Cookie never reaches you either, and cannot be renamed: a browser
posting to the URL from the platform’s own origin attaches the user’s session
cookie, and no vendor signs with it. A rename whose target is itself a stripped
header is ignored.
{ "vendor": "acme", "route": "/ingest/acme", "register_tool": "set_webhook_url", "header_renames": { "authorization": "x-acme-signature" } }Rotation
Section titled “Rotation”A workspace admin sees each URL, the connector and route it feeds, and when it
was last rotated, under Settings → Webhooks. Rotating mints a new URL and
calls your register_tool with it. The previous URL keeps working for 24 hours
so in-flight redeliveries land. Your register_tool should therefore replace the
URL it holds and re-register with the vendor, not assume it is called once.
A URL does not expire on its own. It stops working when it is rotated out, or when the connector is uninstalled, in which case the host retires your registrations itself.
Limits
Section titled “Limits”| Body size | 256 KiB; larger deliveries are refused with 413 before reaching you |
| Method | POST only; any other method gets 405 |
| Unknown or retired URL | 404 with an empty body, the same answer for every reason, so the URL space cannot be probed |
| Retries | None from the host: a single forward attempt. Your vendor’s own retry, plus whatever reconciliation you poll for, is the durability story |
| Correlation | The host adds no header of its own. An operator correlates a delivery to the URL it arrived on through the platform’s own logs, so you do not need to record one |
Related
Section titled “Related”- Manifest Reference: the
hooksfield in theai.nimblebrain/hostblock - Notifications: the other direction, your server telling the host about something it learned
- Lifecycle: the install and uninstall events