Asks · createAsk
Ask a human for a decision
POST /v1/asks
An agent's request for a human decision: a title, an optional body (Markdown) and context link, options (default Approve / Decline, or free text with allowText), an expiry (timeoutSeconds) and an optional default option applied when it expires. The default can never be an approving option (the id approve, or any option with style primary): that is refused with approving_default and nothing is asked. Use decline to fail closed; pre-approval is an Owner's rule (decidedVia: "rule"). The workspace's Owners and Deciders get it by push, browser notifications, email, Telegram and Slack. With a test API key (lg_test_…) the ask is test: true: it is shown and decided in the app but never delivered, and its callback is still sent. Returns at once with the ask waiting; then wait for the decision with GET /v1/asks/{id}/wait. States: waiting → decided (a human chose, or the default option applied on expiry with decision.decidedVia: "timeout"), timed_out (expired, no default) or withdrawn. A decision with decidedVia: "timeout" is never an approval: only a human or an Owner's rule (decidedVia: "rule") can approve.
- Who may call it
- Agents (API key)
- Server
- https://api.logu.dev
Parameters#
| Name | In | Type | Required | Description |
|---|---|---|---|---|
Idempotency-Key | header | string | optional | Send a unique value per operation to retry safely: a retry with the same key and body returns the first result (Idempotent-Replayed: true). Per agent, remembered 24 hours. · e.g. deploy-4821 |
Request body#
Content type application/json (required).
What an agent sends to ask. Strict: an unknown field is a 400 unknown_field.
| Field | Type | Required | Notes |
|---|---|---|---|
project | string | required | 1–64 chars · pattern ^[a-z0-9][a-z0-9._-]*$ |
channel | string | optional | 1–64 chars · pattern ^[a-z0-9][a-z0-9._-]*$ |
title | string | required | 1–200 chars |
body | string | optional | 0–10000 chars |
icon | string | optional | 1–16 chars |
url | string | optional | uri |
options | AskOptionInput[] | optional | 1–6 items |
options[].id | string | required | 1–64 chars · pattern ^[a-z0-9][a-z0-9._-]*$ |
options[].label | string | required | 1–64 chars |
options[].style | "primary" | "danger" | "default" | optional | default "default" |
allowText | boolean | optional | default false |
timeoutSeconds | integer | optional | 10–2592000 · default 86400 |
defaultOption | string | optional | 1–64 chars · pattern ^[a-z0-9][a-z0-9._-]*$ |
callbackUrl | string | optional | uri · 0–2048 chars |
idempotencyKey | string | optional | 1–200 chars |
metadata | object | optional | Free-form context stored with the item, up to 8,192 characters of JSON. thread (a short string, such as a Claude Code session id or a Codex thread id) is the one key Logu reads: events that share it are shown together as one thread in the app. |
attachments | string[] | optional | 0–4 items |
{
"project": "shop",
"title": "Deploy v1.4 to production?",
"body": "3 commits since v1.3. CI is green.",
"url": "https://github.com/acme/shop/compare/v1.3...v1.4",
"timeoutSeconds": 3600,
"defaultOption": "decline",
"attachments": [
"att_01K7DQ6C8E0G2J4L6N8Q0S2U4W"
]
}
Example#
curl -s -X POST "https://api.logu.dev/v1/asks" \
-H "Authorization: Bearer $LOGU_API_KEY" \
-H "Content-Type: application/json" \
-d '{"project":"shop","title":"Deploy v1.4 to production?","body":"3 commits since v1.3. CI is green.","url":"https://github.com/acme/shop/compare/v1.3...v1.4","timeoutSeconds":3600,"defaultOption":"decline","attachments":["att_01K7DQ6C8E0G2J4L6N8Q0S2U4W"]}'
Responses#
201
The ask, waiting. A replayed idempotent retry answers 200 with the original ask and Idempotent-Replayed: true.
{
"id": "ask_01K7DQ3M2V8H4N6R9T1W3Y5B7C",
"project": "shop",
"channel": null,
"title": "Deploy v1.4 to production?",
"body": "3 commits since v1.3. CI is green.",
"icon": null,
"url": "https://github.com/acme/shop/compare/v1.3...v1.4",
"options": [
{
"id": "approve",
"label": "Approve",
"style": "primary"
},
{
"id": "decline",
"label": "Decline",
"style": "danger"
}
],
"allowText": false,
"status": "waiting",
"decision": null,
"defaultOption": "decline",
"expiresAt": "2026-10-12T15:03:11.000Z",
"createdAt": "2026-10-12T14:03:11.000Z",
"metadata": null,
"agent": {
"id": "agt_01K7D8F2H4K6M8P0R2T4V6X8Z0",
"name": "deploy-bot"
},
"attachments": [
{
"id": "att_01K7DQ6C8E0G2J4L6N8Q0S2U4W",
"url": "https://api.logu.dev/a/att_01K7DQ6C8E0G2J4L6N8Q0S2U4W?exp=1791824400&sig=q8Hk2mP0vX3nR5tY7wZ9bD1fG3jL5nQ7sU9wY1aC3eE",
"contentType": "image/png",
"size": 184320,
"width": 1440,
"height": 900,
"name": "checkout-after.png"
}
],
"test": false
}
Response fields
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | required | |
project | string | required | |
channel | string | null | required | |
title | string | required | |
body | string | null | required | |
icon | string | null | required | |
url | string | null | required | |
options | AskOption[] | required | |
options[].id | string | required | 1–64 chars · pattern ^[a-z0-9][a-z0-9._-]*$ |
options[].label | string | required | 1–64 chars |
options[].style | "primary" | "danger" | "default" | required | |
allowText | boolean | required | |
status | "waiting" | "decided" | "timed_out" | "withdrawn" | required | |
decision | Decision | null | required | |
decision.optionId | string | null | required | |
decision.text | string | null | required | |
decision.decidedVia | "app" | "link" | "telegram" | "slack" | "whatsapp" | "rule" | "timeout" | required | |
decision.decidedAt | string | required | date-time |
decision.decidedBy | MemberRef | null | required | |
decision.decidedBy.id | string | required | |
decision.decidedBy.name | string | required | |
decision.ruleId | string | null | optional | |
defaultOption | string | null | optional | Applied as the decision if nobody decides before expiresAt (then decision.decidedVia is timeout: never an approval); null: it ends timed_out. Agents can't set an approving one (id approve or style primary) on new asks. |
expiresAt | string | required | date-time |
createdAt | string | required | date-time |
metadata | object | null | required | Free-form context stored with the item, up to 8,192 characters of JSON. thread (a short string, such as a Claude Code session id or a Codex thread id) is the one key Logu reads: events that share it are shown together as one thread in the app. |
agent | AgentRef | null | required | |
agent.id | string | required | |
agent.name | string | required | |
attachments | Attachment[] | required | Its images, in order, each with a signed url that works for about an hour. |
attachments[].id | string | required | |
attachments[].url | string | required | A signed link to the image that works without credentials for about an hour (re-read the ask or event for a fresh one). Members can always fetch it with GET /v1/attachments/{id}. |
attachments[].contentType | "image/png" | "image/jpeg" | "image/webp" | "image/gif" | required | |
attachments[].size | integer | required | Bytes. |
attachments[].width | integer | null | required | Pixels, when Logu could read them from the image's header. |
attachments[].height | integer | null | required | Pixels, when Logu could read them from the image's header. |
attachments[].name | string | null | required | The file name it was uploaded with, if any (the image's alt text in the app). |
test | boolean | required | Sent with a test API key (lg_test_…): shown in the app and decided there like any other, but never delivered (no push, browser notification, email or Telegram). Its callback is sent. |
escalation | object | null | optional | |
escalation.at | string | required | date-time |
escalation.to | MemberRef[] | required | |
escalation.to[].id | string | required | |
escalation.to[].name | string | required |
Errors#
Every error is {"error":{"code","message","hint","docUrl","requestId"}}: branch on code. See Errors.
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request | The request is malformed or a field is invalid (see issues). |
| 400 | unknown_field | The body has a field the endpoint doesn't know (often a typo or snake_case). |
| 400 | approving_default | The ask's defaultOption is an approving option (the id approve or a primary style): an agent can't pre-approve by default. |
| 401 | unauthorized | No credentials, or the API key or session is invalid or revoked. |
| 402 | plan_limit | This is over one of the workspace's plan quotas: asks, events or metric reports this month, stored images, projects or metric names (Free and Pro), or on Free its agents, members and webhook endpoints (only on servers that enforce plan limits), or a Free account that already owns its shared workspaces (always). |
| 402 | ceiling_reached | The workspace reached one of Logu's cost ceilings (asks, events, metric reports, test traffic or images this month, stored image bytes, projects, metric names or API keys). Unlike plan_limit, upgrading doesn't lift it. An account that already owns the most shared workspaces an account may own gets it too. |
| 403 | forbidden | The caller is authenticated but may not do this (wrong role, or an API key on a human-only endpoint). |
| 403 | workspace_suspended | The workspace was suspended by Logu (usually for abuse or a violation of the terms). Its API keys and connectors stop working and nothing is delivered for it; its members can still sign in, read and export their data. |
| 409 | idempotency_key_in_use | A request with the same idempotency key is still being processed. |
| 422 | idempotency_key_reused | The idempotency key was already used with a different request body or endpoint. |
| 429 | rate_limited | This API key sent too many requests, or a human did too much too fast: sign-in attempts, workspaces created in a day, or a global daily cap on new accounts was reached. |
| 500 | internal_error | Something went wrong on Logu's side. |
| 503 | service_unavailable | A dependency is down or a feature isn't configured on this server. |