Webhooks
Webhooks let external systems inject typed events into OpenAlice's event bus. A shell script, a monitoring system, or another agent can POST to /api/events/ingest and have Alice react — complete with causal lineage and the same listener fan-out as any internal event.
Endpoint
POST /api/events/ingest
Body:
{
"type": "task.requested",
"payload": { "prompt": "Check BTC price and summarize" }
}
Auth headers (either works):
Authorization: Bearer <token>
X-OpenAlice-Token: <token>
X-OpenAlice-Token exists for webhook sources that don't support the Authorization header (some legacy platforms).
Responses:
| Status | Meaning |
|---|---|
201 | Accepted — returns the appended event entry { seq, ts, type, payload } |
400 | Invalid JSON or payload failed schema validation |
401 | Missing token |
403 | Invalid token, or event type not in external allowlist |
503 | No tokens configured — endpoint refuses by default (default-deny) |
Authentication
Webhook tokens live in data/config/webhook.json:
{
"tokens": [
{ "id": "grafana-alerts", "token": "<opaque-high-entropy-string>", "createdAt": 1710500000000 }
]
}
| Field | Description |
|---|---|
id | Non-secret label (shown in the UI for rotation management) |
token | The bearer secret. Treat as high-entropy — anyone with this can trigger Alice. |
createdAt | Epoch ms. Metadata only, used for rotation decisions. |
Default-deny. If the tokens array is empty, the endpoint returns 503 instead of accepting unauthenticated requests. You must configure at least one token before webhooks will fire.
Constant-time comparison. Token matching uses timingSafeEqual to prevent timing attacks on token guessing. Length-mismatch shortcuts are fine — token length is not sensitive.
The External Event Allowlist
Not every event type can be ingested externally. Only types marked external: true in the AgentEvents registry are accepted — everything else returns 403. This prevents outside callers from forging internal state transitions like cron.done.
Currently the external allowlist is:
| Event | Payload | Description |
|---|---|---|
task.requested | { prompt: string } | Ask Alice to run a one-shot task with the given prompt |
More external types will be added as needed. Each one needs an entry in the registry plus a listener that handles it.
task.requested — The First External Event
task.requested is the most general external trigger. When you POST it:
curl -X POST http://localhost:3002/api/events/ingest \
-H "Authorization: Bearer $OPENALICE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "task.requested",
"payload": { "prompt": "Check if any of my positions are down more than 5% today" }
}'
The following chain fires:
POST /api/events/ingest
↓ auth + schema validation
webhook-ingest producer emits task.requested
↓ (event seq 42)
task-router listener handles
↓ askWithSession(prompt, task/default)
Alice runs the full AI pipeline (tools, reasoning)
↓
ConnectorCenter.notify(reply) → last-interacted channel
↓
task-router emits task.done (causedBy: 42)
↓ OR task.error on failure
The reply is delivered to whichever channel you last interacted through (Web UI, Telegram). Nothing is returned in the HTTP response beyond the event seq — the endpoint fires the event and returns immediately. Outcomes surface as new events you can observe on /api/events/stream or in the Web UI Flow graph.
Session Isolation
External tasks run in a dedicated session (data/sessions/task/default.jsonl), separate from cron jobs, heartbeat, and user chats. Each external task sees prior external task history but not your conversations.
Serial Processing
Like cron-router and heartbeat, task-router processes one request at a time. If a new task.requested fires while another is in flight, the new one is skipped with a warning. Don't rely on webhooks for high-frequency ingestion.
fetch Example
await fetch('http://localhost:3002/api/events/ingest', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENALICE_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'task.requested',
payload: { prompt: 'Summarize overnight news and flag anything concerning' },
}),
})
Try It in the UI
The Webhook sub-tab on /automation has:
- Endpoint contract — method, body shape, status codes, auth note
- Accepted event types — dynamic list from
/api/topologyfiltered byexternal: true, each with payload fields, curl snippet, and fetch snippet (copy buttons included) - Try-it form — POSTs
task.requestedfrom the browser and shows the returned seq
Pair it with the Flow tab to watch the whole injection → listener → reply loop light up end-to-end.
Observability
Every webhook call produces an event chain you can observe:
/api/events— paginated history (disk)/api/events/recent— fast in-memory queries (ring buffer)/api/events/stream— SSE stream of new events in real-time/automationFlow tab — visual graph with live highlights
The causedBy field on task.done / task.error points back to the triggering task.requested seq, so you can trace any task outcome to its originator.
Security Notes
- Tokens in
webhook.jsonare stored in plaintext. The file is gitignored by default — keep it that way. - Rotate tokens by adding a new one alongside the old, updating callers, then removing the old one.
- The endpoint runs on the Web UI port (default
3002). If you expose it to the internet, front it with a reverse proxy that enforces TLS — OpenAlice itself does not terminate TLS. - Anyone with a valid token can request arbitrary AI work. Treat tokens as equivalent to an API key with full agent access.