# Logu > Logu is where software agents reach a human. Agents ask, the human decides. An agent sends an > **ask** when it needs a decision (approve a deploy, pick an option), an **event** when something > happened, and a **metric** when it reports a number. The human gets them by phone push, browser > notifications, email, Telegram, Slack and WhatsApp, and decides from wherever they are. Agents never > decide. This file is for agents and the people wiring them up. The API is `https://api.logu.dev`. The full version, with the agent guide and every error code, is `https://api.logu.dev/llms-full.txt`. The complete contract is OpenAPI 3.1 at `https://api.logu.dev/openapi.json` (every operation, schema, example and error code; `x-agent-hint` on each operation says when to call it and whether it blocks), readable at `https://logu.dev/docs/api`. Guides and quickstarts: `https://logu.dev/docs`. ## Auth Every request sends an API key: `Authorization: Bearer lg_…`. Everything the agent sends belongs to its workspace: that workspace's Owners and Deciders get its asks. An API key never sees another workspace. `GET /v1/me` says which agent and workspace a key is, and its limits. Test mode: a test API key (`lg_test_…`; Agents → "Add a test API key", `logu login --test`, or `POST /v1/agents/{id}/api-keys {"test":true}`) works the same, but its asks, events and metrics are marked `test: true`, shown in the app with a Test badge and never delivered to anyone's phone (no push, email, Telegram, Slack or WhatsApp). A human still decides test asks in the app (or they time out), and callbacks are sent. `GET /v1/me` says it: `apiKey.test`. Use one while building an integration. Getting an API key without copying one by hand (on a machine with a human at hand): `npx -y @logu/cli install claude-code` (Claude Code), `npx -y @logu/cli install codex` (OpenAI Codex) or `npx -y @logu/cli login` (any script). The CLI shows a code like `WDJB-MJHT` and opens `https://logu.dev/connect?code=WDJB-MJHT`; the human picks a new agent or an existing one (e.g. "Codex") and clicks Connect, and the CLI saves a new API key for it in `~/.config/logu/config.json` (mode 0600) as a profile: `claude-code`, `codex` or `default` (`--profile `, `LOGU_PROFILE`), so each integration is its own agent. The CLI, the hooks and `@logu/sdk` read it when `LOGU_API_KEY` isn't set. Any client can do the same (RFC 8628 style, no credentials): `POST /v1/device/code` `{client_name, client_kind}` (`claude-code`, `codex` or `cli`, optional) → `{device_code, user_code, verification_uri_complete, interval}`; show the human the code and URL, then `POST /v1/device/token {"device_code"}` every `interval` seconds until it returns `{api_key, agent, workspace}` (once). While waiting it answers 400 `authorization_pending` (or `slow_down`: add 5 s); stop on 403 `access_denied` or 400 `expired_token` (10 minutes). Never show or log `device_code` or the API key. For CI and servers, create an agent in the app (Agents page) and set `LOGU_API_KEY`. ## Asks: ask, then wait for the decision ```sh # 1. Ask (201). Approve/Decline by default; fail closed with defaultOption. curl -s https://api.logu.dev/v1/asks \ -H "Authorization: Bearer $LOGU_API_KEY" -H "Content-Type: application/json" \ -H "Idempotency-Key: deploy-4821" \ -d '{"project":"shop","title":"Deploy v1.4 to production?","body":"3 commits since v1.3", "timeoutSeconds":3600,"defaultOption":"decline"}' # → {"id":"ask_…","status":"waiting","expiresAt":"…",…} # 2. Wait: long-poll up to 50 s per call; repeat while status is "waiting" curl -s "https://api.logu.dev/v1/asks/ask_…/wait?timeout=50" \ -H "Authorization: Bearer $LOGU_API_KEY" # → {"status":"decided","decision":{"optionId":"approve","decidedBy":{"name":"Ana"},…},…} ``` The wait loop: ``` loop: ask = GET /v1/asks/{id}/wait?timeout=50 if ask.status != "waiting": break proceed only if ask.status == "decided" and ask.decision.optionId == "approve" ``` - States: `waiting` → `decided` (a human chose; or the default applied on timeout, with `decision.decidedVia: "timeout"`; or an Owner's rule decided it as it was sent, with `decidedVia: "rule"`), `timed_out` (no decision, no default) or `withdrawn`. - Body fields (camelCase; unknown fields are rejected): `project` (slug, created on first use), `title`, `body` (Markdown), `channel`, `icon`, `url`, `options` (`[{id,label,style}]`, up to 6), `allowText` (free-text decisions), `timeoutSeconds` (10 s to 30 days, default 24 h), `defaultOption`, `callbackUrl` (HTTPS, gets a signed `ask.settled` POST), `idempotencyKey`, `metadata`. For a server, prefer a webhook endpoint (an Owner sets it up once per workspace: every ask, event and threshold alert, Standard Webhooks signing); see Webhooks under Docs. - Withdraw an ask you no longer need: `POST /v1/asks/{id}/withdraw`. - List: `GET /v1/asks?status=waiting&limit=50` (newest first; pass the response's `nextCursor` back as `cursor` for the next page). One ask: `GET /v1/asks/{id}`. - Don't proceed with a risky action until the ask is decided with the approving option by a person: a decision with `decidedVia: "timeout"` (the default applied) is never an approval. `defaultOption` can't be an approving option (id `approve` or style `primary`): 400 `approving_default`; use `decline`. Pre-approval is an Owner's rule. ## Events and metrics ```sh curl -s https://api.logu.dev/v1/events -H "Authorization: Bearer $LOGU_API_KEY" \ -H "Content-Type: application/json" -d '{"project":"shop","title":"Deploy finished","icon":"🚀"}' curl -s https://api.logu.dev/v1/metrics -H "Authorization: Bearer $LOGU_API_KEY" \ -H "Content-Type: application/json" -d '{"project":"shop","metric":"orders","value":42}' ``` ## Images (attachments) Show the human images with an ask or event, e.g. a UI change's before/after screenshots: upload each (PNG, JPEG, WebP or GIF, up to 5 MB, checked by its bytes), then list the ids (up to 4). ```sh curl -s "https://api.logu.dev/v1/attachments?name=after.png" -H "Authorization: Bearer $LOGU_API_KEY" \ -H "Content-Type: image/png" --data-binary @after.png # → {"id":"att_…","url":"https://api.logu.dev/a/att_…?exp=…&sig=…","expiresAt":"…",…} curl -s https://api.logu.dev/v1/asks -H "Authorization: Bearer $LOGU_API_KEY" -H "Content-Type: application/json" \ -d '{"project":"web","title":"Ship the new checkout?","attachments":["att_…","att_…"]}' ``` An upload belongs to one ask or event, by the agent that uploaded it; one nothing uses within 24 hours is deleted. Asks and events return `attachments: [{id,url,contentType,size,width,height,name}]`; `url` is a signed link that works for at least an hour. CLI: `--image file` (repeat); SDKs: `uploadAttachment` / `upload_attachment`; MCP: `images: [{data_base64, content_type}]` (or `path` through `logu mcp`) or `attachments: ["att_…"]`. ## MCP Streamable HTTP at `https://api.logu.dev/mcp` with the same bearer API key. Tools: `ask_human` (waits up to `wait_seconds`, max 50), `get_decision` (call again while it's waiting), `withdraw_ask`, `list_asks`, `send_event`, `report_metric`, `whoami`. Results carry `approved` and `next` (what to do now). ```sh claude mcp add --scope user logu -- npx -y @logu/cli mcp # stdio bridge, reads the saved API key claude mcp add --transport http logu https://api.logu.dev/mcp \ --header "Authorization: Bearer $LOGU_API_KEY" ``` MCP takes an API key (from the Agents page, or the one `logu login` saved), or OAuth: clients that support MCP authorization (claude.ai and ChatGPT connectors, Cursor) connect to `https://api.logu.dev/mcp` with a sign-in and a click, no key to paste. OAuth access tokens work only on `/mcp`. `GET /v1/me` (and `whoami`) includes `locale`, the human's language (`en`, `pt-BR`; null means English): write titles and bodies in it. ## SDKs and CLI - TypeScript: `@logu/sdk` — `new Logu().ask({ project, title })` waits for the decision; `logu.guard(fn, { project })` runs `fn` only once approved. - Python: `pylogu` — `Logu().ask("shop", "Deploy?")`, `@logu.requires_approval("shop")`. - CLI: `logu ask "Deploy?" && ./deploy.sh` (exit 0 approved, 1 declined, 2 no decision); `npx -y @logu/cli install claude-code` connects Claude Code in one command (and one click in the browser), approves its permission prompts from your phone, turns the question Claude ends a turn with into an ask whose answer continues the session, and registers `logu mcp` (Logu's MCP tools over stdio) for every session; `npx -y @logu/cli install codex` does the same for OpenAI Codex (hooks.json, `[mcp_servers.logu]` and notify in config.toml, a Logu section in AGENTS.md), each as its own agent (profiles `claude-code`, `codex`); `logu login`, `logu whoami`, `logu profiles`, `logu logout`. ## Retries and idempotency Send `Idempotency-Key: ` on `POST /v1/asks`, `/v1/events` and `/v1/metrics` (asks also take `idempotencyKey` in the body). A retry with the same key and body returns the first result (200, `Idempotent-Replayed: true`); the same key with another body is a 422 `idempotency_key_reused`. Keys are per agent and remembered 24 hours. ## Errors `{"error":{"code","message","hint","docUrl","requestId","issues"?,"ask"?}}`. Branch on `code`; `hint` says what to do next; quote `requestId` (also the `Logu-Request-Id` header) when reporting a problem. Codes: `bad_request`, `unknown_field` (with "did you mean"), `unauthorized`, `forbidden`, `not_found`, `method_not_allowed`, `conflict` (an ask that's no longer waiting; `ask` has its state), `idempotency_key_in_use`, `idempotency_key_reused`, `rate_limited`, `internal_error`, `service_unavailable`, `plan_limit` (402: the workspace's plan is out of asks, events, metric reports, image storage, projects or metric names, or on Free of agents; nothing was created, so don't proceed: tell your human) and `ceiling_reached` (402: one of Logu's always-on cost ceilings, same advice, upgrading doesn't lift it; don't retry before the 1st UTC); while connecting with a device code, `authorization_pending`, `slow_down`, `expired_token`, `access_denied`. Retry 429 and 5xx with backoff (honor `Retry-After`); don't retry other 4xx. ## Limits Per API key, per minute: 60 asks, 600 events and metrics, 120 images, 1200 requests in all, and 3000 requests per workspace (all its keys together) (429 `rate_limited` with `Retry-After` and `RateLimit-*` headers). Title 200 characters, body 10,000, 6 options, 4 images of 5 MB, ask expiry 10 s to 30 days, one long-poll 50 s. ## Docs - [Docs](https://logu.dev/docs/): quickstarts for curl, TypeScript, Python, the CLI, Claude Code and MCP, and connectors (claude.ai, ChatGPT, Cursor) - [Agent guide](https://logu.dev/docs/agents): Claude Code hooks, Codex, MCP, CLI, SDKs, agent frameworks - [Errors](https://logu.dev/docs/errors): every code, its meaning and what to do - [Webhooks](https://logu.dev/docs/webhooks): webhook endpoints, payloads, verification, retries - [Reaching the human](https://logu.dev/docs/delivery): push, browser notifications, email, Telegram, Slack, WhatsApp, quiet hours, routing - [Attachments](https://logu.dev/docs/attachments): images on asks and events - [Glossary](https://logu.dev/docs/glossary): the vocabulary - [API reference](https://logu.dev/docs/api): every endpoint, with examples, errors and `x-agent-hint`, generated from the [OpenAPI 3.1 document](https://api.logu.dev/openapi.json) - [Request and response schemas](https://github.com/kevinqz/logu/blob/main/packages/protocol/src/index.ts)