GitHubBlog

Search Documentation

Search for a page in the docs

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:

StatusMeaning
201Accepted — returns the appended event entry { seq, ts, type, payload }
400Invalid JSON or payload failed schema validation
401Missing token
403Invalid token, or event type not in external allowlist
503No 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 }
  ]
}
FieldDescription
idNon-secret label (shown in the UI for rotation management)
tokenThe bearer secret. Treat as high-entropy — anyone with this can trigger Alice.
createdAtEpoch 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:

EventPayloadDescription
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/topology filtered by external: true, each with payload fields, curl snippet, and fetch snippet (copy buttons included)
  • Try-it form — POSTs task.requested from 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
  • /automation Flow 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.json are 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.