Webhook triggers: POST /v1/triggers/:token.
A third party gets a scoped secret that wakes exactly one schedule (kind: "webhook"). The
token is not the gateway bearer token. Only its SHA-256 is stored, and it is compared in
constant time (Plug.Crypto.secure_compare/2). The route runs before the JSON parser and
reads its own body, so nobody reaches a parser or a limit without a valid token, and the
body cap is enforced while reading.
| Answer | Meaning |
|---|---|
202 | Queued. The sender never waits for an LLM turn. |
401 | Unknown, cancelled or paused trigger. One answer for all three, so a token cannot be probed. |
400 | Body is not valid UTF-8. |
413 | Body is larger than config :super_pi, :trigger_max_bytes (default 32768). |
The body is put into the schedule's prompt, fenced and marked as data from an outside sender
({{payload}} in the prompt marks the place, else it is appended). It is untrusted input to
an unattended agent: give a webhook schedule a prompt and a harness that cannot do harm with it.
An Idempotency-Key header (up to 200 printable characters) is the second half of the unique job
key, so a retried delivery is a no-op for as long as Oban keeps the job (24 h with the default
Pruner). Without it every delivery is a new run.
Each accepted delivery adds a note (payload, truncated) to pi.inbox of the owning conversation.
Webhooks exist only where a harness opts in: Plugins.Scheduler creates a webhook schedule only
when its config has allow_webhooks: true (default false). Spending money from an endpoint
that is not behind the bearer token has to be a deliberate choice.
Summary
Functions
Handles a delivery for token. Reads the body itself, so call it before Plug.Parsers.
Always answers; returns the conn.
Maximum body size in bytes.
Functions
@spec handle(Plug.Conn.t(), String.t()) :: Plug.Conn.t()
Handles a delivery for token. Reads the body itself, so call it before Plug.Parsers.
Always answers; returns the conn.
Maximum body size in bytes.