Skip to main content

HTTP triggers

The default trigger needs no trigger object at all: a function is an HTTPS endpoint from the moment its deploy lands. This page covers both doors — calling the function's own public URL directly, and calling it through the platform's shared invoke path. They differ in one way that matters: what reaches your handler.

Which door do I want?
  • Direct URL — your handler receives the JSON body exactly as sent. Use it for arbitrary bodies, status-code control, and the lowest latency.
  • Shared invoke path — your handler receives only message (plus session fields). Use it when the caller should not know the function's URL, or when the same client code also calls agents.

Who needs a credential

Deploying and managing functions always requires authentication, and the two doors then differ:

  • Door 2, the shared invoke path, requires a bearer token by default. INVOKE_AUTH_REQUIRED defaults to true and the platform chart ships it as true, so every curl below carries an Authorization header. Without one you get 401. An administrator can open it with INVOKE_AUTH_REQUIRED=false, but that opens every agent and function in every project at once.
  • Door 1, the function's own URL, has no default at all. It does not exist on the public gateway until you publish it, and publishing makes you name the protection: apikey, jwt, or none. A function published with none answers anyone who finds the address; one published with apikey wants the key the gateway minted.

See API authentication.

Door 1: the function's own URL

Get it from GET /v1/agents/{name} — it is the public_url field — and publish the function first if you have not (the quickstart walks through it: call it at its public URL):

platformctl gateway publish function/my-function --auth none --rate-limit 100/minute
export FN_URL=https://my-function-ab12cd.apps.codyhill.dev

--auth is not optional: the API answers 400 naming auth.mode if you leave it out, because a public endpoint and a protected one are both things you have to have typed. The curl calls below send no credential, so they need --auth none; use --auth apikey for anything you would not hand to a stranger. Setting the environment variables directly works too — CAI_EXPOSE_EXTERNAL takes the same three modes and CAI_EXPOSE_RATE_LIMIT is required alongside it — but a bare CAI_EXPOSE_EXTERNAL=true is refused, and the refusal surfaces one step removed, when the function drops out of ready. See call it at its public URL for that failure in full.

GET

Any GET (other than the platform's own /healthz) calls your handler with an empty {} event:

curl -s "$FN_URL"
{"statusCode": 200, "body": "hello, world, from a Python function"}

POST with a JSON body

The parsed JSON body becomes the event, verbatim:

curl -s -X POST "$FN_URL" \
-H 'Content-Type: application/json' \
-d '{"name": "ada"}'
{"statusCode": 200, "body": "hello, ada, from a Python function"}

Your handler sees only the JSON body — never the URL path, query string, or request headers. Bodies are capped at 8 MiB (a larger body is refused with 413 before your handler runs). Include statusCode in the return value to control the HTTP status; the whole return value becomes the response body.

A complete worked example

The handler (Python shown; the same body contract holds in all four languages — see the language guides):

def handle(event: dict) -> dict:
if "user_id" not in event:
return {"statusCode": 400, "error": "user_id is required"}
return {"statusCode": 200, "user": event["user_id"], "ok": True}

Call it badly, then well:

curl -s -X POST "$FN_URL" -H 'Content-Type: application/json' -d '{}'
# {"statusCode":400,"error":"user_id is required"} (HTTP 400)

curl -s -X POST "$FN_URL" -H 'Content-Type: application/json' -d '{"user_id":"u-123"}'
# {"statusCode":200,"user":"u-123","ok":true} (HTTP 200)

The full request and response contract — the 8 MiB cap, the handler raised error shape, exception behavior — is documented on HTTP and events.

Door 2: the shared invoke path

The same path agents use:

platformctl invoke my-function "some text"

You should see a blank line and a session id — the invoke envelope reports an agent's output field, and a function's return value has no output key:


(session: 3f6c1f0e-9c1c-4f1a-8f2e-2a0d5f6b7c81)

However you call it, this path delivers your text to the handler as event["message"]. Only session_id, user_id, message, and memorize survive the trip — the endpoint decodes the body into a fixed shape and re-sends that, so any other key you add is dropped before your handler sees it. When you need an arbitrary body, use Door 1.

Streaming: invoke/stream (NDJSON)

POST /v1/agents/{name}/invoke/stream takes the same body and responds with application/x-ndjson — newline-delimited JSON, one complete object per line, in the order things happened:

curl -sN -X POST "$CAI_API/v1/agents/my-function/invoke/stream" \
-H "Authorization: Bearer $CAI_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"message": "some text"}'

For a function (which has no intermediate agent events) the stream is typically a single terminal event, but the contract is identical to the agent one, so one NDJSON client handles both. Parse line by line; never treat the body as one JSON document. See invoke for the event shapes.

When you need real HTTP routing

One function answers on one path with a JSON-body-only view of the world. When you genuinely need path routing, query strings, header access, or file uploads, deploy a serverless container service where you own the web server — or split the work across separate functions per path.

Next steps