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
| Field | Type | Notes |
|---|---|---|
project | string | Project name or ID. Informational when API key auth already identifies the project. |
session_id | UUID string | Existing Agent Notifier session UUID. Do not send random local UUIDs. |
client_event_id | UUID string | Optional 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. |
type | string | Required. One of alert, progress, session_start, session_end, url, file. |
title | string | Required. Shown in push and feed cards. Max 256 chars. |
message | string | Substance-first body text. Do not repeat project, event, or status boilerplate. Max 4096 chars. |
event | string | Optional hook/event name such as UserPromptSubmit, PreToolUse, PostToolUse, SubagentStart, SubagentStop, SessionStart, SessionEnd, AgentStop, or PreCompact. Max 64 chars. |
tool_name | string | Optional tool name for tool events, such as Bash, Read, or Write. Max 128 chars. |
command | string | Optional redacted command or tool input. Max 4000 chars. Redact locally before sending. |
prompt | string | Optional redacted user prompt text. Max 8000 chars. Redact locally before sending. |
file_paths | string array | Optional 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_id | UUID string | Optional parent notification ID for thread/subagent rendering. Must belong to the same project; foreign or invalid IDs are rejected. |
subagent_id | string | Optional opaque subagent/thread identifier supplied by the caller. Max 128 chars. |
status | string | Optional short status word such as submitted, running, success, or failure. Max 64 chars. |
priority | integer | -1 silent, 0 normal, 1 high, 2 critical/reserved entitlement path. |
color | string | Optional 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. |
progress | number | 0.0 to 1.0; useful for progress events and live sessions. Renderers show compact status text rather than determinate bars. |
attachment | object | Legacy 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. |
attachments | object array | Preferred 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_url | URL | Deprecated legacy image attachment URL. New senders should use attachments; image attachments still mirror into image_url for older clients. |
as_audio | boolean | Optional 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. |
voice | string | Kokoro 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_engine | string | TTS engine for as_audio. Defaults to kokoro; currently only kokoro is accepted. |
speed | number | TTS speech speed for as_audio. Defaults to 1.0; accepted range is 0.5 to 2.0. |
rewrite_for_tts | boolean | Reserved future flag for server-side text rewriting before TTS. Defaults to false; currently keep it false. |
tts_text | string | Optional 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. |
url | URL | Clickable link target. Use this with url_title for uploaded PDFs, Markdown files, logs, documents, and non-image blobs. |
url_title | string | Display text for url. |
buttons | string array | Up to 4 action button labels. Free tier may show fewer buttons. |
sound | string | Push sound name. Defaults to default. |
ttl | integer | Time-to-live in seconds. |
device | string | Optional device-name target, such as Yash iPhone. |
metadata | object of string values | Optional 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:
titleshould be a short verb phrase, for exampleRun tool: Bash,Prompt submitted, orSession ended.messageshould 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: Zprose. metadataremains useful for safe hashes, lengths, durations, source labels, and back-compat hook context.file_pathsdescribes local context only. If a sender wants the app/user to open files, upload each file to/v1/mediaand send the returned structured objects inattachments. Images still backfill legacyimage_url; non-image files can also populateurl/url_titlefor 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:
| Key | Meaning |
|---|---|
agent_notifier_schema | Metadata schema marker; use hook.v1 for hook events. |
hook_source, hook_scope, hook_path, hook_script | Hook integration and config/script path that emitted the event. |
hook_event, hook_event_type, hook_tool | Raw lifecycle event, stable renderer category, and tool name. |
hook_project, hook_project_id, hook_cwd | Project label/ID when available and redacted current working directory. |
hook_session_id, hook_session_hash, hook_api_session_id | Existing server session UUIDs or a local session hash; do not send random UUIDs. |
hook_subject_hash | Short hash for dedupe/rendering without storing raw prompt/tool subjects in proof logs. |
hook_file_path, hook_file_path_count, hook_tool_input_keys | Redacted file/path and top-level tool parameter summaries. |
hook_error_present, hook_status, hook_exit_code, hook_duration_ms | Tool status, failure presence, exit code, and duration hints when available. |
hook_prompt_length, hook_prompt_hash, hook_prompt_excerpt | Redacted prompt summary and optional bounded excerpt. |
hook_command_length, hook_command_hash, hook_command_excerpt | Redacted command summary and optional bounded excerpt. |
hook_tool_input_length, hook_tool_input_hash, hook_tool_input_excerpt | Redacted tool-input summary and optional bounded excerpt. |
hook_tool_response_length, hook_tool_response_hash, hook_tool_response_excerpt | Redacted tool-response/output summary and optional bounded excerpt. |
hook_thinking_length, hook_thinking_hash, hook_thinking_excerpt | Redacted visible thinking/reasoning summary and optional bounded excerpt when the hook payload exposes it. |
hook_error_length, hook_error_hash, hook_error_excerpt | Redacted 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.