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 function 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 function 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 functions are exposed
Keebai has other kinds of function — calendar integrations, ecommerce lookups, the ones generated automatically from aproduct_info block. None of them appear here. GET /v1/functions 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 functions are never of type webhook, they fall outside this surface by construction: there is no way to create one, re-point one at a different block, or delete one through this API.
Next steps
Create a function
The full body, with parameters and placeholders.
Update a function
The merge rules, in detail.