AgentNotify
Browse documentation

AgentNotify API

Base URL: http://127.0.0.1:47821 (configurable via config.json port or AGENTNOTIFY_PORT). All /v1/* routes require Authorization: Bearer <token>. The token is generated on first launch and stored as authToken in the platform data directory's config.json (%LOCALAPPDATA%\AgentNotify on Windows; $XDG_DATA_HOME/AgentNotify or ~/.local/share/AgentNotify on macOS/Linux). It can be overridden by AGENTNOTIFY_TOKEN or --token on the CLI.

Priority/status enum strings and built-in type IDs are snake_case (input_required, permission_required, etc.). Type IDs may also be user-defined: 1–64 lowercase letters, numbers, or underscores, starting with a letter. CLI input accepts hyphens and normalizes them to underscores.

Content-Type: application/json. Max request body: 64 KiB (configurable maxRequestBodyBytes).


Endpoints

GET /health

Unauthenticated liveness probe.

{ "status": "ok" }

GET /v1/health

Authenticated. Returns broker health.

Field Type Notes
status string "ok"
version string Product informational version, e.g. "0.2.0-alpha.3"
pid int Broker process id
uptimeSeconds number Seconds since start
activeCount int Count of status=active notifications
apiVersion string Always "v1"
serverTimeUtc string (ISO 8601) DateTimeOffset.UtcNow at handle time

POST /v1/notifications

Create or — when key matches an active notification — update in place (dedup).

Request (CreateNotificationRequest):

Field Type Required Notes
title string yes 1–200 chars
message string yes 1–4000 chars
type identifier no A built-in or user-defined type ID (default info)
priority enum no low/normal/high/critical; omitted values use the custom definition default or normal
agent string no Default "unknown"; 1–100 chars
agentInstance string no 1–100 chars
project string no 1–200 chars
key string no 1–100 chars dedup key
cwd string no 1–1024 chars
pid int no Non-negative
metadata object no Arbitrary JSON map; serialized size ≤ 8192 bytes

Validation failures return 400 with { "error": "<message>" }.

Response 201NotificationDto; on dedup the same id is returned with updated updatedAt.

POST /v1/events

Accepts ARC 0.2 request.created, request.updated, response.submitted, and request.resolved events. The sender, execution context, semantic request kind, title, message, priority, and stable condition key project into the native notification lifecycle. extensions.x-agentnotify.notification_type may select a configured local type ID; other correctly namespaced vendor extensions are ignored. arc_version must be exactly "0.2"; 0.1 is rejected.

An unkeyed created event derives a stable key from sender.id and event_id, so a delivery retry returns the original notification even after resolution. An explicit request.key opts into the condition lifecycle. Updates require an existing active condition and never create a missing one; resolutions are idempotent.

Every accepted event returns the same envelope:

{ "notification": { }, "interaction": { } }

notification is the local record; interaction is the durable question the producer waits on, and is null unless the event was answerable. A request carrying request.response opens both under one key — see Interactions for the model and its own routes. response.submitted delivers one answer and resolves the notification half with it; a resolution withdraws any question still open under the key.

A newly persisted request returns 201. An update, answer, resolution, keyed in-place refresh, or immutable replay returns 200. A missing update/answer/resolution target returns 404, a second answer to an already answered condition returns 409, and invalid or unsupported envelopes return 400. An answerable event whose question is invalid is rejected before anything is stored. The endpoint uses the same validation, persistence, callbacks, routing, and durable outbox boundary as native creation.

See ARC.md for the complete contract, lifecycle, schema, security rules, and examples.

GET /v1/notifications

List notifications (newest first). Optional query params:

Param Type Notes
unresolved bool When true, only status=active
type identifier Filter by exact normalized type ID
status enum Filter by exact status
project string Exact match, trimmed
agent string Exact match, trimmed
limit int 1–500 (default 100)

Response 200NotificationDto[].

GET /v1/notifications/{id}

Response: 200NotificationDto; 404 when missing.

PATCH /v1/notifications/{id}

Update status.

Request (UpdateNotificationRequest): { "status": "dismissed" | "resolved" | "active" } — required. Transition rules (see StatusTransitions): active → {dismissed,resolved} always allowed; a dismissed/resolved notification can only be reopened (→ active).

Response: 200 → updated NotificationDto; 400 on invalid transition or missing status; 404 when missing.

POST /v1/notifications/{id}/dismiss

Convenience: sets status=dismissed. Same responses as PATCH.

POST /v1/interactions/request

Open a durable waiting question or permission. A pending interaction with the same key is returned instead of duplicated; if the request bound to that key changed, the previous one is superseded and a new interaction is created.

Request (CreateInteractionRequest, snake_case):

Field Type Required Notes
key string no Logical deduplication key
agent string no Host agent ID; default unknown
agent_instance string no Per-run agent identity
project string no Project/repository name
session_id string no Opaque native session ID
turn_id string no Opaque native turn/generation ID
native_request_id string no Opaque host request ID needed to return the answer
kind enum no permission (default), single_choice, or text
prompt string yes 1–2000 characters
choices array for choice kinds 2–12 { "id", "label", "detail"? } entries
text_max_length int no Text-answer limit, 1–2000; default 500
ttl_seconds int no Expiry window, 30–3600; default 600

Response: 201 with InteractionDto when created; 200 when a pending keyed request is reused; 400 on validation failure.

GET /v1/interactions

List interactions newest first. Optional query parameters are pending (bool), status, agent, project, session, and limit (1–500, default 100). Returns InteractionDto[].

GET /v1/interactions/{id}

Return one InteractionDto, or 404 when it does not exist. Reads also sweep an overdue pending interaction to expired.

GET /v1/interactions/{id}/wait

Long-poll one interaction until it reaches a terminal state or the timeout elapses. timeout is clamped to 1–300 seconds and defaults to 60. A timeout returns 200 with the current pending InteractionDto; callers may wait again. Unknown IDs return 404.

POST /v1/interactions/{id}/respond

Submit the first valid answer.

Request (RespondInteractionRequest, snake_case):

Field Type Required Notes
response_id string yes Client idempotency ID, 1–128 characters
request_digest string yes Must equal the current interaction digest
nonce string yes Single-use nonce returned in the current InteractionDto
choice_id string choice kinds Exactly one of choice_id or text
text string text kind Bounded by text_max_length
source string no desktop, cli, relay, or a host ID
device_id string no Responding device identity when known

Returns the answered InteractionDto. An identical response_id replays the accepted 200; a different second answer returns 409. Stale digests/nonces, wrong answer shapes, terminal states, and expired requests return 400; unknown IDs return 404.

POST /v1/interactions/{id}/cancel

Cancel a pending interaction and wake any waiter. Returns the current InteractionDto; unknown IDs return 404. Cancelling an already-terminal interaction is idempotent.

POST /v1/interactions/{id}/publish

Republish the interaction through every matching enabled AgentNotify Relay route whose payload policy allows message content. The response is { "id": "...", "published": <count> }. Returns 400 when Relay publishing is not configured and 404 for an unknown interaction. Publishing is idempotent per interaction and Relay provider.

See INTERACTIONS.md for lifecycle, first-response-wins, digest/nonce, expiry, host capture, and Relay synchronization rules.


DTOs

NotificationDto

id, key?, agent, agentInstance?, project?, type, priority, title, message,
cwd?, pid?, status, createdAt, updatedAt, resolvedAt?, metadata?

Timestamps are ISO 8601 (DateTimeOffset). metadata is Record<string, JsonElement>? (arbitrary JSON values, erased to null when absent).

Custom type definitions are local presentation policy stored in config.json; notification rows persist only the stable identifier. Missing, disabled, or deleted definitions safely fall back to generic info styling and a seven-second lifetime.

InteractionDto

id, key?, agent, agent_instance?, project?, session_id?, turn_id?, native_request_id?,
kind, prompt, choices, text_max_length, status, request_digest, nonce,
created_at, updated_at, expires_at, answered_at?, response?

status is pending, answered, expired, cancelled, or superseded. response, when present, contains response_id, choice_id?, text?, source, device_id?, and created_at. This DTO contains the response-binding nonce and is loopback/bearer protected; do not forward it to unrelated services or logs.

HealthResponse — see GET /v1/health.


Web interface

Everything under /ui belongs to the web interface and is not part of the stable API. /ui/api/* takes no bearer token, requires X-AgentNotify-UI: 1 and a loopback Host on every state change, and may change between releases without notice. Automate against /v1. The trust model is in WEB_UI.md.


Auth

Authorization: Bearer <token>

The supplied and expected values are hashed with SHA-256 and compared with a fixed-time byte comparison. Missing/invalid token → 401 { "error": "unauthorized" }. The bare /health probe is not authenticated.


Rate limiting

Every POST under /v1/notifications or /v1/interactions, plus POST /v1/events, is guarded by one sliding fixed-window counter per token: default 30 requests/second (rateLimitPerSecond). GET and PATCH are not rate limited. When exceeded: 429 { "error": "rate limit exceeded" } with Retry-After: 1.


Errors

Status Shape When
400 { "error": "<message>" } Validation failure, invalid JSON, illegal status transition
401 { "error": "unauthorized" } Missing/invalid bearer token
404 { "error": "notification not found" } or { "error": "interaction not found" } Unknown id
429 { "error": "rate limit exceeded" } POST rate limit
413 Request body exceeds maxRequestBodyBytes (Kestrel rejects before routing)

Malformed payloads (invalid JSON) are handled gracefully — never crash the broker. Validation never logs the token.


Examples

agentnotify.exe send --type completed --title "Build complete" --message "All tests passed."

PowerShell

$config = Get-Content "$env:LOCALAPPDATA\AgentNotify\config.json" | ConvertFrom-Json
$headers = @{ Authorization = "Bearer $($config.authToken)" }
$body = @{
  agent = "codex"
  project = "AgentNotify"
  type = "input_required"
  priority = "high"
  title = "Need input"
  message = "Choose option A or B."
} | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:$($config.port)/v1/notifications" `
  -Headers $headers -ContentType "application/json" -Body $body

curl from WSL

Prefer agentnotify.exe; if direct HTTP debugging is explicitly required:

TOKEN="$(agentnotify.exe token)"
curl --fail-with-body -sS -X POST http://127.0.0.1:47821/v1/notifications \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"type":"blocked","priority":"high","title":"Blocked","message":"SDK missing"}'
unset TOKEN

ARC event

TOKEN="$(agentnotify.exe token)"
curl --fail-with-body -sS -X POST http://127.0.0.1:47821/v1/events \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "arc_version":"0.2",
    "event_id":"evt_build_42",
    "event_type":"request.created",
    "occurred_at":"2026-08-26T01:15:00Z",
    "sender":{"id":"codex","name":"Codex"},
    "context":{"project":"agent-notify"},
    "request":{
      "kind":"completion",
      "title":"Build complete",
      "message":"All tests passed."
    }
  }'
unset TOKEN