<!-- https://zerolog.ch/docs/webhooks · Docs / Connect -->
One boundary, two directions

# Webhooks

Let an outside system trigger work here, or be told when something finished.

Outside

ZeroLog

From *· Outside*

An outside system sends a request.

Inbound

Accepted

To *· ZeroLog*

The request triggers work here.

From *· ZeroLog*

Something here finished.

Outbound

To *· Outside*

The outside system is told.

## Only authorised requests are accepted

Unauthorised incoming requests are rejected before they can trigger work. The token in the URL is the credential. An unknown or revoked token gets a 404, so a guess reveals nothing.

The square on the inbound path above

## The three kinds

### Wake-up webhook · inbound

An outside system such as CI, GitHub Actions or Zapier sends `POST /api/hook/<token>`. ZeroLog starts a [/goal](https://zerolog.ch/docs/goal) job in the chat and container you attached to the webhook, and answers 202 with the job number.

- Model, objective, completion criteria and spend cap are frozen in the webhook. By default the caller cannot change them. If you allow it, the caller can append free text of up to 4,000 characters.
- A paused container is woken first, unless you turned that off.
- Limits per webhook: 6 triggers a minute and 100 a day by default, up to 60 and 1,000. Beyond that the answer is 429. With no prepaid credit left the answer is 402. Each account can have 3 active wake-up webhooks.
- The token is shown once when you create the webhook and is stored only as a hash. A browser visit (GET) is refused with 405.

### Event webhook · inbound

Real events from Stripe, GitHub or another service are posted to `POST /api/inject/<token>`. ZeroLog appends each one as a line of JSON to `/work/.webhooks/incoming.jsonl` in the container, so the agent can build and test against actual payloads.

- Each line holds the time, method, headers and body. Authorization, cookie and API-key headers are replaced before anything is written.
- Bodies up to 256 kB are accepted. The default limit is 60 events a minute, up to 120.
- This kind has no settings screen yet. You create and revoke it through the API (`/api/injects`).

### Decision webhook · outbound

You give ZeroLog a URL, an optional secret and the events you care about: `goal_set`, `turn_end`, `needs_approval`, `risk_check`, `goal_done` and `tool_call`. When one happens, ZeroLog sends a signed POST and acts on your reply: `continue`, `stop`, `approve`, `deny` or `ask`, with an optional instruction and reason.

- With a secret, each request carries an `X-ZeroLog-Signature` header with a timestamp and an HMAC-SHA256 of the body. Receivers should reject timestamps older than five minutes.
- The timeout is 5 seconds by default, between 1 and 30. ZeroLog retries once.
- If your service fails, times out or is not an allowed destination, ZeroLog's built-in evaluator decides instead. Redirects are not followed.

Manage the wake-up and decision kinds under Settings, Webhooks.

## The three kinds of webhook

Mermaid source of the diagram on this page:

```mermaid
%% Webhooks: the three kinds and what each checks. Facts from server/hooks.js, webhookinject.js, webhookai.js, routes/hook.js, routes/inject.js.
flowchart TB
  subgraph OUT["Decision webhook (outbound)"]
    direction TB
    o1["Event in your chat:<br/>approval, risk, tool call"]
    o2["Signed POST to<br/>your URL"]
    o3["Your reply: continue,<br/>stop, approve, deny or ask"]
    o4["No answer in time:<br/>built-in evaluator decides"]
    o1 --> o2
    o2 -->|answered| o3
    o2 -->|failed or blocked| o4
  end

  subgraph EVT["Event webhook (inbound)"]
    direction TB
    e1["Service sends<br/>POST /api/inject/token"]
    e2["Same token and<br/>pair checks, rate limit"]
    e3["Append one JSON line to<br/>incoming.jsonl in<br/>the container"]
    e1 --> e2 --> e3
  end

  subgraph WAKE["Wake-up webhook (inbound)"]
    direction TB
    w1["CI or Zapier sends<br/>POST /api/hook/token"]
    w2["Token known? Chat and<br/>container still yours?"]:::hot
    w3["Rate limits and<br/>prepaid credit"]
    w4["Wake container,<br/>start frozen /goal job"]
    w1 --> w2 --> w3 --> w4
  end

  OUT ~~~ EVT
  EVT ~~~ WAKE

  classDef hot stroke:#8f7ac9
```
