Public API

Actions and webhooks

Action buttons, durable response polling, acknowledgements, and outbound webhook callbacks.

Actions and webhooks

Agent Notifier notifications can include action buttons and text replies. When the user taps a button or sends a reply, the app records the response with the API. Local agents can then poll or stream a durable inbox and acknowledge each response after handling it; projects can also receive signed outbound webhook callbacks.

For the common "pause the agent until a human decides" workflow, prefer POST /v1/approvals. It wraps message send, decision waiting, auto-ack, timeout defaults, and per-agent response shaping into one API call.

Button labels

Send action buttons as part of POST /v1/messages:

{
  "type": "alert",
  "title": "Deploy approval needed",
  "message": "Promote build 1.2.3 to production?",
  "buttons": ["Approve", "Retry", "Rollback", "Cancel"]
}

Free-tier notifications may expose fewer lock-screen buttons; Pro is planned for up to four.

First-class approvals

Agents can send a blocking approval request with:

POST /v1/approvals

Minimal request:

{
  "project": "agent-notifier",
  "session_id": "00000000-0000-0000-0000-000000000000",
  "title": "Run tool: Bash",
  "message": "rm -rf node_modules",
  "event": "PreToolUse",
  "tool_name": "Bash",
  "command": "rm -rf node_modules",
  "decisions": [
    {"key": "allow", "label": "Allow"},
    {"key": "deny", "label": "Deny", "is_default_on_timeout": true}
  ],
  "timeout_seconds": 300,
  "response_format": "claude_code.pretooluse",
  "wait": "block"
}

Wait modes:

waitBehavior
noneReturns 202 with approval_id and notification_id; caller can poll/stream.
blockHolds the request until a decision or timeout, then auto-acks and returns response_payload.
sseStreams sent then decided events over text/event-stream.

Initial response_format renderers are raw, claude_code.pretooluse, claude_code.userpromptsubmit, copilot_cli.pretooluse, copilot_cli.permissionrequest, codex.permission, and cursor.beforeshellexecution.

On timeout, the first decision marked is_default_on_timeout is returned with is_timeout_default: true; if none is marked, the first decision is used.

Sessions

API-key clients can create explicit sessions before sending messages or approvals:

POST /v1/sessions
{
  "project": "agent-notifier",
  "label": "Tuesday morning run",
  "agent": "claude-code",
  "agent_version": "2.1.119",
  "metadata": {"repo": "agent-notifier"}
}

Useful session endpoints:

EndpointPurpose
GET /v1/sessionsList sessions for the API-key project.
GET /v1/sessions/{id}Fetch one session.
GET /v1/sessions/{id}/treeFetch parent/child notification tree for debugging subagents.
GET /v1/sessions/{id}/responsesSession-scoped action response inbox.
POST /v1/sessions/{id}/endMark a session complete or error.

Implicit session creation on first message still works for backward compatibility, but explicit creation is preferred for new agents.

Recording an action

The iOS app records taps through:

POST /v1/actions

Request body:

{
  "notification_id": "00000000-0000-0000-0000-000000000000",
  "action": "button_pressed",
  "button_index": 0,
  "session_id": "00000000-0000-0000-0000-000000000000"
}

This endpoint uses Bearer JWT auth because it is called by the app for an authenticated user.

Polling action responses

Local agents and hooks poll durable user responses with an API key:

GET /v1/action-responses

Use the X-API-Key header. The endpoint returns a project-scoped inbox of user button taps and text replies.

Project selection depends on the API key scope:

Key scopeSelector behavior
Account-scoped API keyMust pass either project_id=<uuid> or project=<name>. project_id takes precedence. The selected project must already belong to the API-key owner.
Legacy project-scoped API keyThe backend infers the project from the key and ignores project_id / project.

Useful query parameters:

QueryDefaultNotes
project_idProject UUID selector for account-scoped keys.
projectProject name selector for account-scoped keys when the UUID is not available.
session_idOptional session UUID filter.
notification_idFind responses for one notification.
parent_idFind responses under a parent/subagent root.
kindbutton or reply.
client_event_idMatch action or notification correlation IDs.
approval_idMatch a first-class approval created by POST /v1/approvals.
decision_keyMatch structured approval decision key, action text, or button label.
afterOptional RFC3339 lower bound on recorded_at.
decided_afterOptional RFC3339 lower bound on the user's tap/reply timestamp.
cursorCursor from a previous next_cursor for deterministic keyset pagination.
limit50Page size from 1 to 100.
unacked_onlytruePending-only by default. Set false to include processed, ignored, and failed responses.

Example response:

{
  "items": [
    {
      "id": "00000000-0000-0000-0000-000000000001",
      "notification_id": "00000000-0000-0000-0000-000000000000",
      "project_id": "00000000-0000-0000-0000-000000000002",
      "session_id": "00000000-0000-0000-0000-000000000003",
      "kind": "button",
      "action": "button_pressed",
      "button_index": 0,
      "button_label": "Approve",
      "ack_status": "pending",
      "recorded_at": "2026-04-28T12:00:00Z",
      "notification_title": "Deploy approval needed",
      "notification_message": "Promote build 1.2.3 to production?"
    }
  ],
  "next_cursor": "00000000-0000-0000-0000-000000000004"
}

If next_cursor is present, poll the next page with cursor=<next_cursor> until the response omits next_cursor. This prevents large pending batches or same-timestamp replies from being dropped.

Streaming action responses

For lower-latency local agents, use Server-Sent Events instead of polling:

GET /v1/action-responses/stream?session_id=00000000-0000-0000-0000-000000000000
Accept: text/event-stream

The stream emits action_response events with the same item shape as the paginated endpoint, plus : heartbeat comments roughly every 30 seconds. Resume with Last-Event-ID or the cursor query parameter.

Acknowledging action responses

After the local handler accepts a response, acknowledge it:

POST /v1/action-responses/{id}/ack

For account-scoped API keys, pass the same project_id or project selector used for polling. This keeps guessed cross-project IDs returning 404. Legacy project-scoped keys infer their project from the key.

Request body is optional. When omitted, the status defaults to processed:

{
  "status": "processed"
}

Allowed acknowledgement statuses are:

StatusUse when
processedThe handler completed successfully.
ignoredThe handler intentionally took no action.
failedThe handler tried and failed; include a short redacted error string if useful.

Acknowledgement is idempotent after a response leaves pending: repeat acks return the existing terminal status instead of overwriting it. Ack after handler success, not before; leaving a response pending is the retry signal for the next poll.

Simulated decisions for tests

E2E harnesses can inject a button decision without a real phone tap:

POST /v1/test/simulate-decision

This endpoint requires an API key created with test_mode: true.

{
  "notification_id": "00000000-0000-0000-0000-000000000000",
  "decision_key": "allow",
  "device": "test-runner"
}

Normal production API keys receive 403.

Idempotency and redaction

  • POST /v1/actions accepts client_event_id; duplicate client event IDs return the original response instead of recording another tap/reply.
  • Treat response_text as untrusted user input. Do not evaluate it as shell, SQL, JSONPath, or code.
  • Log response IDs, notification IDs, project IDs, ack_status, and redacted handler status. Avoid logging API keys, webhook secrets, raw APNs tokens, full reply text, or secret-shaped metadata.
  • If a handler writes proof logs, prefer boolean summaries such as has_response_text over storing the text itself.

Outbound webhook payload

When a project has a webhook_url, the backend sends a signed payload similar to:

{
  "notification_id": "00000000-0000-0000-0000-000000000000",
  "action": "button_pressed",
  "button_index": 0,
  "session_id": "00000000-0000-0000-0000-000000000000",
  "timestamp": 1777248000,
  "nonce": "b7c1f9d0"
}

Webhook URLs must be public http or https URLs with a valid hostname. The backend rejects empty/invalid URLs, embedded credentials, invalid ports, localhost and Docker-internal hosts, private/link-local/loopback/multicast/unspecified IPs, local-only suffixes such as .localhost, .local, .internal, .lan, .home, and single-label or all-numeric hostnames. Validation runs when projects are created or updated and again before delivery.

Expected security headers:

HeaderPurpose
X-Signature-256HMAC-SHA256 signature over the payload.
X-TimestampUnix timestamp used for replay-window checks.
X-NonceOne-time nonce used to prevent replay.

The current decisions log requires a 5-minute replay window for webhook security. Agents receiving webhooks should reject stale timestamps, reused nonces, and invalid signatures.

Agent behavior

  • Treat action callbacks as asynchronous; the original send response only confirms message acceptance.
  • Poll GET /v1/action-responses until next_cursor is exhausted, then ack each handled response.
  • Ack only after local handler success; use ignored or failed deliberately and keep errors short/redacted.
  • Make callbacks idempotent by id, client_event_id, or notification_id + button_index.
  • Log action IDs and statuses, not webhook secrets, API keys, or raw reply text.