Skip to content

Letting outside systems poke your box (webhooks)

Most agent work starts because a person typed something. A webhook lets a machine start it instead. When some other system has news — a payment cleared, a monitor tripped, a form was submitted — it can send an HTTP request to your box, and Lowkey turns that request into a message to an agent. The external event kicks off agent work without anyone sitting at the keyboard.

It’s the difference between “an agent works when I ask it to” and “an agent works the moment something happens out there.”

Your box exposes one route for this:

POST /api/webhook/<source>

<source> is a name you choose for each system that’s allowed to call in — say stripe or uptime. When a request arrives, Lowkey looks that source up in your config. If it’s known, the request body is folded into a message and handed to an agent in the project you picked; if it’s not configured, the request is rejected with a 404 (packages/daemon/src/server.ts, the /api/webhook/ route).

The incoming JSON body rides along in the message — Lowkey prefixes it with Webhook from <source>: and includes the payload (trimmed to the first 2000 characters), so the agent can see what actually happened and act on it.

Webhook sources live in your box config at ~/.lowkey/config.json, under a webhooks key. Each entry is named by its source and points at where the work should land (packages/shared/src/config.ts):

{
"webhooks": {
"stripe": {
"project": "/home/you/projects/billing",
"agent": "lucy",
"target": "new_session"
}
}
}
  • project (required) — the project the message goes into.
  • agent (optional) — which agent answers; defaults to your box’s default agent.
  • target (optional) — new_session (the default) starts a fresh conversation for each event, or you can pass an existing session id to keep landing events in the same thread.

A concrete run: Stripe is set to call POST /api/webhook/stripe on a successful charge. The event arrives, Lowkey opens a new session in your billing project, and the agent gets a message describing the charge — ready to log it, flag a large one, or message you. You did nothing; the event did it.

A route that lets the outside world start agent work has to be locked down, so it is:

  • Every call needs your box’s API key. The request must carry Authorization: Bearer <apiKey> matching the key in your config — anything else gets a 401 (packages/daemon/src/auth-gate.ts). Only systems you’ve handed that key to can reach in.
  • Unknown sources are refused. A source has to exist in your config before its requests do anything; everything else 404s.
  • Nothing is hidden. Each webhook lands as a real session you can open and read, so you always see what an external event set in motion.

Note: the config also accepts a per-source secret field, but I could not confirm it’s enforced by the current route — treat the API key as the gate.

  • Defining the agents that answer → /operator/agents-config/
  • Running work on a schedule instead of on an event → ../automations.md
  • The box’s keys and trust model → /operator/provisioning/