---
title: Messages
slug: messages
description: Send typed notifications with project grouping, color, progress, URLs, media, and actions.
openapi_tags: [Messages]
---

# 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:

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

Required auth:

```http
X-API-Key: an_key_REPLACE_WITH_YOUR_KEY
```

## Minimal request

```json
{
  "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:

- `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

```bash
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:

```bash
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`:

```json
{
  "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.
