Which URLs are rejected
Every create and every update that touchescontext.url runs the same validator. A rejection is a 400 with error.code of FUNCTION_URL_NOT_ALLOWED and a details.reason naming the rule.
Alternate encodings of an address are normalised before the check, so
https://0x7f000001/, https://2130706433/, https://127.1/ and https://[::ffff:127.0.0.1]/ are all rejected as not_globally_routable just like https://127.0.0.1/.
timeout is separately bounded to 1–30 seconds; outside that it is a 400 with FUNCTION_TIMEOUT_NOT_ALLOWED.
Placeholders in the host are the interesting case
{{name}} placeholders are a real feature: they are filled at call time from the arguments the model picked, which is what makes https://erp.example.com/stock/{{sku}} work.
What this does not protect against
The validator inspects the URL you stored. It resolves nothing.A hostname that points at a public address today can point at a private one tomorrow, and the name is resolved again when the request is actually made. Closing that gap is a matter of controlling egress, not of validating input, so treat this validator as a guard against the accidental and the casual — a copy-pasted
http://localhost:3000, a cloud metadata endpoint, an internal service name — and not as a boundary against a determined attacker who already holds a write-scoped token for your company.Secrets are write-only
context.headers and context.params are where webhook credentials live in practice: a bearer token, an API key, a signed query parameter.
Reads mask them. Keys stay visible so you can see the shape of the request; every value comes back as the literal string ***.
context.body is not masked. It is the request template the model fills in, not a place credentials normally live, and hiding it would remove the main reason to read a tool at all.
Writing them back
The masking creates one hazard, and the API refuses rather than let you walk into it.
The third row is the one that matters. The obvious client pattern —
GET, change one field, PUT the whole object back — would otherwise store the literal string *** over a live credential, and the tool would start failing with a 401 from your own service for no visible reason. Instead it fails loudly and immediately.
So to change the URL and keep the credentials, omit headers:
Only webhook tools are exposed
Keebai has other kinds of tool — calendar integrations, ecommerce lookups — that it generates itself. None of them appear here.GET /v1/tools filters to webhooks, and asking for a non-webhook by id returns 404 rather than 403, so an id you cannot manage is indistinguishable from one that does not exist.
Because auto-generated tools are never of type webhook, they fall outside this surface by construction: there is no way to create, re-point or delete one through this API.
Next steps
Create a tool
The full body, with parameters and placeholders.
Update a tool
The merge rules, in detail.