Webhook triggers API
A webhook trigger lets an external system start an agent run by calling a
unique inbound URL. It is the event-driven counterpart to the time-based
scheduler: instead of a clock, an inbound HTTP POST fires the run.
Each trigger has:
- a unique URL path
POST /hooks/{token}on the public inference host (https://ai-api.myra.eu), where{token}is a 128-bit random hex string, and - a secret, sent in the
X-AIG-Webhook-Secretrequest header.
The run executes as the agent's owner (agent.created_by) — the same
run-as-owner identity the scheduler uses — with exactly the tools that owner
configured on the agent. The caller supplies only the token, the secret, and a
payload; the agent, owner, tenant and gateway are all resolved server-side from the
stored trigger, so a caller can never target another agent, owner or tenant.
Only a hash of the secret is stored (sha256); the plaintext secret is shown
once at creation and cannot be retrieved afterwards.
Firing a trigger (public, no session)
POST https://ai-api.myra.eu/hooks/{token}
X-AIG-Webhook-Secret: {secret}
Content-Type: application/json
{ "any": "json or text payload" }
Configure your sender with the API host directly
The receiver lives on the public inference host (https://ai-api.myra.eu). A
request that reaches the app host (https://ai.myra.eu/hooks/{token}) is answered
with a 307 redirect to the API host — a browser or curl follows it, but many
webhook senders (Stripe, GitHub, GitLab, …) treat any 3xx as a failed delivery and
will not re-POST. Always register the API-host URL the product shows you; never the
app host.
Accepted request shape:
| Part | Accepted | Rejected → status |
|---|---|---|
| Method | POST only |
any other → 405 |
| Path | /hooks/{token}, {token} = 16–64 hex chars |
malformed → 404 |
X-AIG-Webhook-Secret |
non-empty; must match the trigger secret | missing/empty → 401; wrong → 403 |
| Body | any UTF-8, ≤ 32 KB | > cap → 413; non-UTF-8 → 400 |
| Rate | per-source-IP and per-trigger sliding windows | exceeded → 429 |
| Backpressure | ≤ 50 unstarted runs already queued for the trigger | exceeded → 429; count unreadable (transient DB error) → 503 (retry) |
| Agents feature | the tenant's Agents feature must be enabled (agent triggers) | disabled → 503 |
Success returns 202 Accepted:
The run is enqueued, not executed on the inbound connection. It is picked up by
the scheduler on its next tick, so end-to-end latency is ≤ ~one scheduler tick
(about a minute) plus the run's own execution time. Delivery is at-most-once:
each fire is its own durable run, so a burst of N calls yields N runs (no
collapse), and a crash mid-run marks that run failed rather than re-running it.
Under sustained overload the queue drains FIFO; the 429 backpressure above bounds
how far it can grow. There is no de-duplication — if your producer retries, it
enqueues another run, so make retries idempotent on your side.
Once a run is durably enqueued the response is 202 even if the trigger's
last_fired_at bookkeeping fails — that timestamp is a best-effort admin readout,
never load-bearing. A cosmetic bookkeeping error is deliberately not surfaced as a
5xx, so it can never provoke a producer retry that would enqueue a duplicate run.
Rejection behaviour (fail-closed)
Every rejection enqueues no run. To avoid a token-existence oracle, an unknown
token, a disabled trigger, and a wrong secret all return an identical 403
{"error":"forbidden"}; a missing secret header returns 401 before any lookup.
The secret and its hash are never echoed in a response or written to a log.
Security note — the payload is untrusted data
The webhook body becomes the agent's user turn, wrapped in an explicit data-not-instructions envelope. A model can still be influenced by a user turn, and a webhook run has exactly the tools the owner configured on that agent. Only expose a trigger for an agent whose tool access you are willing to drive with external input — or set the no-egress tool profile below to remove that risk.
No-egress tool profile (no_egress)
Set no_egress: true when creating a trigger to run its fires with a restricted
profile that strips every externally-egressing tool from the run:
fetch_url/agentic_fetch,web_search,- external MCP connectors,
- sub-agent delegation,
- image generation, and the code interpreter.
Local project tools (read_file / write_file / knowledge search) are kept —
the profile removes only the tools that can send data off the gateway, so an
unattended, externally-triggered run cannot be steered into exfiltration (SSRF /
data egress). It narrows tools only; the agent's model routing is unchanged, so
a summarize-and-deliver agent on a cloud model still works.
The profile is opt-in and never forced (default false = current behaviour).
It is enforced server-side at run assembly — the client is never the boundary —
and is fail-closed: a run inherits the flag its trigger had when it fired
(snapshot), so re-toggling the trigger does not change already-queued runs.
Managing triggers (admin plane)
Admin endpoints require an authenticated admin session and gateway access; the base
URL is https://ai-api-admin.myra.eu/admin/v1. Triggers are owned by their agent's
owner (member, KI-Manager, tenant_admin, or admin); a viewer or demouser cannot create them.
The same operations are available in the admin UI under an agent's Webhooks tab
(create — which reveals the secret once — list, enable/disable, delete, and copy the
/hooks/{token} URL). The endpoints below are the exact calls that panel makes.
Create
POST /admin/v1/gateways/{gateway}/agents/{agent}/webhook-triggers
{ "name": "PagerDuty alerts", "prompt": "Summarize this alert.", "no_egress": true }
// prompt optional; no_egress optional (default false — see the no-egress profile above)
201 returns the secret once:
{
"id": "…",
"name": "PagerDuty alerts",
"enabled": 1,
"agent_id": "…",
"no_egress": 1,
"url_path": "/hooks/ab12…",
"secret": "…", // shown ONCE — store it now
"secret_header": "X-AIG-Webhook-Secret"
}
400 if name is missing or longer than 255 chars, prompt is not a string or is
longer than 60000 chars, or no_egress is not a boolean (it is validated strictly — a
truthy string/number is rejected, not coerced). There is no rotation endpoint; to change the secret, delete
the trigger and create a new one. no_egress is set at creation; to change it,
recreate the trigger.
List
Returns each trigger's id (needed for the PATCH/DELETE {id} routes), agent_id,
url_path, name, enabled, no_egress, secret_header, last_fired_at, and
created_at — never the secret or its hash. Empty list is [].
Changing the enabled state
Disabling is a real kill switch: it also cancels any already-queued (pending) runs
for that trigger. A disabled trigger's URL returns 403.
Delete
Cancels the trigger's still-pending runs and removes it. Create/enable/disable/delete are recorded in the gateway audit log (never with the token or secret hash).
Workflow triggers (event-driven Baukasten runs)
A webhook can also fire a visual Workflow (Baukasten) run instead of an agent
run — the "event trigger" for a workflow. The same POST /hooks/{token} receiver
is reused (identical method / rate-limit / secret / body-cap / UTF-8 rules above); the
only difference is what the fire enqueues and how the body is interpreted.
A workflow trigger is created on the workflow, not an agent, and is limited to one per workflow (MVP):
GET /admin/v1/gateways/{gateway}/workflows/{id}/webhook → the trigger, or {"webhook": false}
POST /admin/v1/gateways/{gateway}/workflows/{id}/webhook → create (reveals the secret ONCE); 409 if one exists
DELETE /admin/v1/gateways/{gateway}/workflows/{id}/webhook → delete + cancel the workflow's pending webhook runs
These require the WORKFLOWS_AUTHOR permission (the built-in member, ki_manager,
tenant_admin, or admin roles, or a custom role granting it; viewer/demouser are
refused 403), gateway access, and the workspace workflow feature flag — the same gate
as the workflow build routes. The trigger
stores the workflow's own tenant/gateway, so the fire path derives the run's identity
(owner, tenant, gateway, version) entirely from the workflow — a caller can never
target another tenant's workflow.
Firing a workflow trigger
POST https://ai-api.myra.eu/hooks/{token}
X-AIG-Webhook-Secret: {secret}
X-AIG-Idempotency-Key: {optional-key}
Content-Type: application/json
{ "betreff": "…", "prioritaet": "hoch" }
The body must be a JSON object. Its top-level fields become the trigger node's
output, addressable in the builder as {{trigger.output.<field>}}.
| Part | Accepted | Rejected → status |
|---|---|---|
| Body | a JSON object ({…}); empty body = {} |
a JSON array/scalar, or invalid JSON → 400; over cap → 413; non-UTF-8 → 400 |
| Workflow state | published + workspace feature on | unpublished, feature off, or no published version → 403 |
| Backpressure | ≤ 50 in-flight (pending + running + suspended) webhook runs | exceeded → 429; count unreadable (transient DB error) → 503 (retry) |
X-AIG-Idempotency-Key |
optional; a string ≤ 64 chars of A-Z a-z 0-9 - _ . :; collapses a retry to one run |
absent → each fire is its own run; longer / illegal characters → 400 invalid idempotency key |
Success returns 202 { "status": "queued", "run_id": "…" }. The run is picked up
by the dedicated workflow runner within ~2 s (not a full scheduler tick).
The payload is untrusted data — single-pass, never re-interpreted
The payload is treated as opaque data. Template resolution is single-pass: a
graph template {{trigger.output.x}} is resolved exactly once, and the substituted
value is never re-scanned — so a payload value that itself contains {{…}} (e.g.
{"x": "{{secret.leak}}"}) is delivered/used literally, never expanded into
another step's output. A missing field a template references is a fail-closed run
error (template_unresolved), and a null on the path fails with template_null_path
— the run never silently drops a slot. As with agent triggers, a payload value still
becomes model input on an agent step, so design the workflow's steps to treat trigger
fields as data; workflow delivery additionally passes the fail-closed PII gate
(blocked_pii) before any egress.
Deletion / export
Deleting the trigger cancels the workflow's still-pending webhook runs (clearing their
stored payload) and removes the config row; deleting the workflow cascades its trigger
away. A fired run's payload is stored on workflow_run.trigger_input (bounded), which
is exported with the run (Art. 15) and erased on tenant purge (Art. 17).
Data export
Webhook triggers appear in the tenant exit export (webhook_triggers section /
webhook_triggers.csv) with the token (the URL path) but never the secret hash —
the secret is unrecoverable and does not leave the gateway.