Public API

Messages

Send typed notifications with project grouping, color, progress, URLs, media, and actions.

Messages

POST /v1/messages accepts a typed notification and queues it for delivery to the user's devices. New senders should use the first-class substance fields below so iOS/admin can render useful prompt, command, file, and subagent context without parsing boilerplate from message.

Production URL:

POST https://notifier.aicrew.in/api/v1/messages

Required auth:

X-API-Key: an_key_REPLACE_WITH_YOUR_KEY

Minimal request

{
  "type": "alert",
  "title": "Build finished",
  "message": "main passed in 4m 12s"
}

Full request shape

FieldTypeNotes
projectstringProject name or ID. Informational when API key auth already identifies the project.
session_idUUID stringExisting Agent Notifier session UUID. Do not send random local UUIDs.
client_event_idUUID stringOptional idempotency key for one logical event. Reusing the same value for the same project returns the original notification instead of creating a duplicate. Generate a fresh UUID per logical send.
typestringRequired. One of alert, progress, session_start, session_end, url, file.
titlestringRequired. Shown in push and feed cards. Max 256 chars.
messagestringSubstance-first body text. Do not repeat project, event, or status boilerplate. Max 4096 chars.
eventstringOptional hook/event name such as UserPromptSubmit, PreToolUse, PostToolUse, SubagentStart, SubagentStop, SessionStart, SessionEnd, AgentStop, or PreCompact. Max 64 chars.
tool_namestringOptional tool name for tool events, such as Bash, Read, or Write. Max 128 chars.
commandstringOptional redacted command or tool input. Max 4000 chars. Redact locally before sending.
promptstringOptional redacted user prompt text. Max 8000 chars. Redact locally before sending.
file_pathsstring arrayOptional file paths referenced by the event. Up to 12 paths, each capped by the server. Metadata/context only; this does not upload file bytes. Customer-facing CLIs should not send local upload paths by default; use POST /v1/media first for actual attachments.
parent_idUUID stringOptional parent notification ID for thread/subagent rendering. Must belong to the same project; foreign or invalid IDs are rejected.
subagent_idstringOptional opaque subagent/thread identifier supplied by the caller. Max 128 chars.
statusstringOptional short status word such as submitted, running, success, or failure. Max 64 chars.
priorityinteger-1 silent, 0 normal, 1 high, 2 critical/reserved entitlement path.
colorstringOptional fixed display color: red, orange, amber, lime, green, teal, cyan, blue, purple, or pink. Alerts default to orange when omitted. Clients render accessible light/dark variants; arbitrary hex is not accepted.
progressnumber0.0 to 1.0; useful for progress events and live sessions. Renderers show compact status text rather than determinate bars.
attachmentobjectLegacy primary structured attachment returned by POST /v1/media. Includes kind (image, audio, video, file), url, mime_type, size_bytes, and optional duration_ms, file_name, and thumbnail_url. New senders should also populate attachments.
attachmentsobject arrayPreferred multi-media field. Up to 12 structured attachments. The server keeps attachment as the first/primary item for older clients and rich-push compatibility.
image_urlURLDeprecated legacy image attachment URL. New senders should use attachments; image attachments still mirror into image_url for older clients.
as_audiobooleanOptional server-side TTS. When true, the API accepts the notification immediately with audio_status: "queued", generates Kokoro speech audio in the background, appends it as an audio attachment, and sends the push after audio is ready. Can be combined with attachments and image_url.
voicestringKokoro voice id for as_audio. Defaults to af_bella; accepted values must look like safe Kokoro ids such as af_bella or af_heart.
voice_enginestringTTS engine for as_audio. Defaults to kokoro; currently only kokoro is accepted.
speednumberTTS speech speed for as_audio. Defaults to 1.0; accepted range is 0.5 to 2.0.
rewrite_for_ttsbooleanReserved future flag for server-side text rewriting before TTS. Defaults to false; currently keep it false.
tts_textstringOptional explicit text to synthesize when as_audio is true. Use this when attaching a Markdown/text file and the audio should read the file content rather than the short notification message. Max 12,000 chars.
urlURLClickable link target. Use this with url_title for uploaded PDFs, Markdown files, logs, documents, and non-image blobs.
url_titlestringDisplay text for url.
buttonsstring arrayUp to 4 action button labels. Free tier may show fewer buttons.
soundstringPush sound name. Defaults to default.
ttlintegerTime-to-live in seconds.
devicestringOptional device-name target, such as Yash iPhone.
metadataobject of string valuesOptional safe, non-secret renderer metadata. Kept for back-compat and auxiliary chips; new senders should prefer the top-level substance fields for core rendering.

Substance-first rendering contract

The API still accepts title and message, but the current iOS/admin renderers prefer structured substance fields:

  • title should be a short verb phrase, for example Run tool: Bash, Prompt submitted, or Session ended.
  • message should be the useful content only: the prompt excerpt, command, file summary, output summary, or error detail.
  • Project name, event name, status, and session identity should be sent as fields/chips, not repeated as project: X · event: Y · status: Z prose.
  • metadata remains useful for safe hashes, lengths, durations, source labels, and back-compat hook context.
  • file_paths describes local context only. If a sender wants the app/user to open files, upload each file to /v1/media and send the returned structured objects in attachments. Images still backfill legacy image_url; non-image files can also populate url/url_title for older clients.

Idempotency and retries

Unattended agents and CLIs should generate a UUID client_event_id for each logical event and keep it stable across automatic retries. The backend enforces uniqueness per project: if the same project receives the same client_event_id again, it returns the original notification ID instead of inserting a duplicate.

Do not hard-code example UUIDs in long-lived scripts. Generate a new value per logical notification, then reuse that value only while retrying that notification after a timeout, 429, or transient 5xx response.

Rich example

AGENT_NOTIFIER_API_BASE="${AGENT_NOTIFIER_API_BASE:-https://notifier.aicrew.in/api/v1}"
CLIENT_EVENT_ID="$(uuidgen | tr '[:upper:]' '[:lower:]')"

curl -sS -X POST "$AGENT_NOTIFIER_API_BASE/messages" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $AGENT_NOTIFIER_API_KEY" \
  --data @- <<JSON
{
    "project": "Release Bot",
    "client_event_id": "$CLIENT_EVENT_ID",
    "type": "progress",
    "title": "Deploying 1.2.3",
    "message": "Smoke tests are running",
    "event": "PostToolUse",
    "tool_name": "Bash",
    "command": "npm run test:e2e",
    "file_paths": ["admin/e2e/launch.spec.ts"],
    "status": "running",
    "priority": 0,
    "color": "blue",
    "progress": 0.72,
    "url": "https://github.com/yashness/agent-notifier/actions",
    "url_title": "View workflow",
    "buttons": ["Approve", "Retry", "Rollback"],
    "sound": "cosmic",
    "ttl": 300
}
JSON

Server-side audio example

Use as_audio when an agent should send media plus generated speech without blocking the caller:

AGENT_NOTIFIER_API_BASE="${AGENT_NOTIFIER_API_BASE:-https://notifier.aicrew.in/api/v1}"
CLIENT_EVENT_ID="$(uuidgen | tr '[:upper:]' '[:lower:]')"

curl -sS -X POST "$AGENT_NOTIFIER_API_BASE/messages" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $AGENT_NOTIFIER_API_KEY" \
  --data @- <<JSON
{
    "project": "Release Bot",
    "client_event_id": "$CLIENT_EVENT_ID",
    "type": "alert",
    "title": "Deployment complete",
    "message": "The deployment finished successfully and smoke tests passed.",
    "priority": 0,
    "as_audio": true,
    "attachments": [
      {
        "kind": "file",
        "url": "https://notifier.aicrew.in/media/release-notes.md",
        "mime_type": "text/markdown",
        "size_bytes": 2048,
        "file_name": "release-notes.md"
      }
    ],
    "tts_text": "Release notes markdown content can be supplied here when the audio should read the attached file.",
    "voice_engine": "kokoro",
    "voice": "af_bella",
    "speed": 1.0,
    "rewrite_for_tts": false
}
JSON

The response includes the notification id and audio_status: "queued". The backend stores metadata.tts_status as queued, then running, then ready or failed. When audio is ready, the generated audio is prepended to attachments, exposed as the legacy primary attachment, and delivered in the rich push.

To check status without a user JWT, call GET /api/v1/messages/{id}/status with the same X-API-Key. The response includes audio_status, attachments_count, the primary attachment, all attachments, and safe metadata.

Hook metadata

Hooks should put core renderer context in the top-level substance fields and auxiliary/back-compat context in metadata. The API keeps /v1/messages compatible by accepting a flat object of string values, sanitizing secret-like keys/values, truncating long values, and returning the same safe metadata in feed/action-response payloads.

Stable hook.v1 keys include:

KeyMeaning
agent_notifier_schemaMetadata schema marker; use hook.v1 for hook events.
hook_source, hook_scope, hook_path, hook_scriptHook integration and config/script path that emitted the event.
hook_event, hook_event_type, hook_toolRaw lifecycle event, stable renderer category, and tool name.
hook_project, hook_project_id, hook_cwdProject label/ID when available and redacted current working directory.
hook_session_id, hook_session_hash, hook_api_session_idExisting server session UUIDs or a local session hash; do not send random UUIDs.
hook_subject_hashShort hash for dedupe/rendering without storing raw prompt/tool subjects in proof logs.
hook_file_path, hook_file_path_count, hook_tool_input_keysRedacted file/path and top-level tool parameter summaries.
hook_error_present, hook_status, hook_exit_code, hook_duration_msTool status, failure presence, exit code, and duration hints when available.
hook_prompt_length, hook_prompt_hash, hook_prompt_excerptRedacted prompt summary and optional bounded excerpt.
hook_command_length, hook_command_hash, hook_command_excerptRedacted command summary and optional bounded excerpt.
hook_tool_input_length, hook_tool_input_hash, hook_tool_input_excerptRedacted tool-input summary and optional bounded excerpt.
hook_tool_response_length, hook_tool_response_hash, hook_tool_response_excerptRedacted tool-response/output summary and optional bounded excerpt.
hook_thinking_length, hook_thinking_hash, hook_thinking_excerptRedacted visible thinking/reasoning summary and optional bounded excerpt when the hook payload exposes it.
hook_error_length, hook_error_hash, hook_error_excerptRedacted error summary and optional bounded excerpt.

Excerpt keys and top-level prompt/command are intended for UI rendering and must be locally redacted before send. Local proof logs omit excerpt fields and keep only hashes/lengths/status so API keys, bearer tokens, private keys, APNs material, SAS signatures, and prompt/command bodies do not leak into diagnostics.

Response

Successful sends return HTTP 202:

{
  "id": "00000000-0000-0000-0000-000000000000",
  "delivered_at": "2026-04-26T00:00:00Z",
  "project_name": "Release Bot",
  "user_hash": "a1b2c3d4e5f6"
}

The API response means the message was accepted for delivery. project_name and user_hash are diagnostics for account-scoped API keys and account mismatch troubleshooting; APNs delivery and user interaction happen asynchronously.