Skip to content

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-Secret request 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:

{ "status": "queued", "run_id": "…" }

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

GET /admin/v1/gateways/{gateway}/agents/{agent}/webhook-triggers

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

PATCH /admin/v1/gateways/{gateway}/agents/{agent}/webhook-triggers/{id}
{ "enabled": false }

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

DELETE /admin/v1/gateways/{gateway}/agents/{agent}/webhook-triggers/{id}

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.