# Notify — Make Notify part of your workflow

Notify documentation. HTML version: https://notify.belweave.ai/docs

## Get connected

Send a decision to your phone and see the answer return. Then connect an agent, script, or service to ask when it needs you.

### Choose your connection

Use Notify when a script, service, or agent needs to reach a person away from a terminal. An update goes to the inbox and eligible iPhone or Android devices; a question waits for a human answer; an activity keeps a running task visible.

Choose a webhook for an existing system that can POST JSON. Choose [MCP](/docs/quickstart#mcp-connect) for a compatible agent client, or the [CLI](/docs/quickstart#cli-install) for terminal workflows. Webhooks use a service secret; MCP and CLI connections have revocable permissions. Personal and Family plans are free.

### Connect your phone

Install the iOS beta through the [public TestFlight link](https://testflight.apple.com/join/1MDYPc7a). No developer-issued invitation is needed; a public App Store listing is not yet available. Android has a native client and APK build workflow in the [repository guide](https://github.com/Belweave/notify/tree/main/apps/android); this is not a public Google Play release. Packaged Mini Apps are a separate developer preview and are not required for notifications or decisions.

1. Install your invited iOS build or your locally built Android APK. Open Notify, sign in with Apple or Google, and allow notifications.
2. Sign in to the website with the same account. Open [Ask your phone](/dashboard#decisions) and select Check devices. Registration and permission are prerequisites for a push banner; a registered device alone does not prove delivery.

### Set up as an agent

An agent with an AgentID can [sign in or create its own Notify account](/login/agentid) without borrowing a human’s Apple or Google credentials. The same AgentID sign-in is available in the iOS app, CLI authorization, MCP consent, and team invitations. Read [authentication instructions](/auth.md) for the complete unattended setup path.

1. Open `/login/agentid`. AgentID uses your existing browser enrollment or shows the AgentMail command to enroll this browser. Run that command in your own environment using your own AgentMail credentials. Notify requests only `openid email profile`; owner-sharing approval is not required.
2. After sign-in, open [API keys](/dashboard#api-tokens) and create a named credential with only the scopes your work needs. Store the secret privately when it is shown once. You can instead complete `notify auth login` or your MCP client’s OAuth flow in the same AgentID browser; the agent can review and approve access to its own account.
3. Use the [OpenAPI specification](/openapi.json) or connect to `https://notify.belweave.ai/mcp`. Check your account and devices, then create services, configure reply webhooks, register apps, manage teams and routing, or publish Mini Apps within your permissions. A scoped API key cannot mint new credentials or widen its own scopes.
4. To reach a human who uses a separate Notify account, create a family or use an existing workplace team, invite them, and share the relevant services and apps. New workplace-team creation is not currently available. The human accepts the invitation, installs the app, signs in, and grants device permissions. An agent account has its own inbox; knowing the human’s email does not link accounts or grant access to their phone.

> AgentID proves the agent’s identity. It does not approve an unrelated human’s decision, grant device permissions, or prove delivery. Use existing team roles and explicitly granted scopes. Link X from [Connected accounts](/account/connections) after signing in with Apple, Google, or AgentID; linking requires proof of the X account and never merges by email.

### Try a decision

In [Ask your phone](/dashboard#decisions), start with the connection check or write a personal request. Choose approve/deny, yes/no, or a written reply, then select your own devices. Send to my phone creates a real request without creating an API key. Dashboard requests never run a job, share with a family or team, or call reply webhooks.

1. Send the connection check. Answer from Notify’s Inbox or its notification actions. If no banner appears, open the Inbox and check notification permissions and device registration.
2. Watch Your recent requests for the confirmed result. Push accepted means the provider accepted a delivery request; queued means delivery will retry. Neither means a person saw or answered it.
3. If sending loses its connection, use Retry same request. The pending send is kept in this browser tab and reuses the same idempotency key after a reload. If checking for the answer fails, keep the last result and use Refresh results when connected.
4. Requests expire after ten minutes and can be canceled before an answer. Expiry, cancellation, or a wait timeout never counts as approval. A confirmed phone response proves the round trip, not that an external job finished.

For a real agent or script workflow, [install the CLI](/docs/quickstart#cli-install), sign in, and send a request with --wait. Require an explicit approved result before taking action; handle denial, expiry, and timeout separately. [Connect MCP](/docs/quickstart#mcp-connect) for clients that support the remote server. Credentials are scoped and revocable.

```bash
notify auth login
notify send ask "Tests passed. Proceed with the release?" --approval --title "Release check" --wait --timeout 10m
```

### Account, device, and service

1. Sign in to [Notify](https://notify.belweave.ai) with Apple, Google, or AgentID. Use the same Notify account on the devices that should receive your updates.
2. Open Notify on your iPhone or Android device, sign in, and allow notifications to register your device.
3. In the dashboard, add a service for one source, such as Warehouse scanner. Set its sender title, optional image, and destination URL.
4. Save the returned webhook URL in the sending system’s secret store. You can now [send an update](/docs/send-update#first-notification).

For help with your account or setup, contact [info@belweave.com](mailto:info@belweave.com).

### Save the service credential

The URL includes a bearer credential for this service. A holder can send to its eligible devices, so keep it out of source control and public logs. Agent service reads redact the URL; agent creation and rotation return it once. The signed-in dashboard can retrieve an existing encrypted webhook URL.

Webhook URL:

```text
https://notify.belweave.ai/hooks/whk_your_token
```

Rotation invalidates the previous URL. An unknown token is reported as `404` with `{ "ok": false, "error": "Unknown webhook" }`; replace the sender’s stored credential after rotating.

### Connect an MCP client

Configure Notify as a remote Streamable HTTP MCP server. Prefer OAuth discovery and browser consent when your client supports them. Clients that accept a Bearer header can instead use a scoped Notify API key; both connect to the same endpoint below. Request only the permissions your workflow needs.

MCP server URL:

```text
https://notify.belweave.ai/mcp
```

In [Poke](https://poke.com/docs/mcp-servers), add this URL as a custom MCP integration and use its OAuth connection flow. For an API-key connection, enter a scoped Notify key in Poke’s API key field; Poke sends it as a Bearer header. Keep credentials out of the server URL. Other remote MCP clients use the same URL and authentication choices when supported.

OpenCode configuration belongs in `opencode.json`:

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "notify": { "type": "remote", "url": "https://notify.belweave.ai/mcp" }
  }
}
```

For Claude Code, use the HTTP add command below. Cursor’s configuration goes in `~/.cursor/mcp.json`:

```bash
claude mcp add --transport http notify https://notify.belweave.ai/mcp
```

```json
{
  "mcpServers": {
    "notify": { "url": "https://notify.belweave.ai/mcp" }
  }
}
```

### Set up the CLI

Download the versioned CLI bundle from [Notify Tools 0.13.0](/downloads/notify-tools/v0.13.0/notify-tools-0.13.0.tar.gz). Requires Node.js 22.13 or newer; no npm package installation is needed.

Verify the download, inspect the installer, and install the exact release:

```bash
curl -fLO https://notify.belweave.ai/downloads/notify-tools/v0.13.0/notify-tools-0.13.0.tar.gz
curl -fLO https://notify.belweave.ai/downloads/notify-tools/v0.13.0/SHA256SUMS
shasum -a 256 -c SHA256SUMS
mkdir notify-tools-0.13.0
tar -xzf notify-tools-0.13.0.tar.gz -C notify-tools-0.13.0
cd notify-tools-0.13.0
node install.mjs --dry-run
node install.mjs
export PATH="$HOME/.local/bin:$PATH"
notify auth login
notify --help
```

1. Run `notify auth login` to obtain a short device-authorization code and open Notify in your browser.
2. Sign in, inspect the requested scopes, and authorize the connection.
3. The CLI polls for that authorization and saves credentials in its OS configuration file with mode `0600`.

Check your connection with `notify auth status`. Dashboard agent connections show its scopes and usage; revoking a connection invalidates it immediately. `notify auth logout` revokes the connection and removes its local credentials.

Repeat `--scope` to request only needed permissions. `--client-name` labels the connection and `--expires-in` requests its lifetime (default `90d`); review the resulting expiry. `NOTIFY_TOKEN` and `NOTIFY_API_URL` override saved credentials and the API base for authorized CI use. Current tools accept existing `ntf_` and Clerk-managed `ak_` user API keys in the same saved configuration `token` field and SDK `token` option. Browser login saves credentials with mode `0600`; provision CI credentials through your secret store as `NOTIFY_TOKEN`, never as a command argument. Clerk application publishable and secret keys are not Notify user API credentials.

## Send an update

Send a useful message, file it into the inbox, and give the recipient a clear next step. Webhook and terminal examples below describe the same package-pickup workflow.

### Send from a webhook

For a package-pickup alert, send a JSON object with `body`. The service supplies the sender title and other defaults. This example uses a placeholder token; substitute the URL saved during [connection setup](/docs/quickstart#webhook-url).

```bash
curl -X POST https://notify.belweave.ai/hooks/whk_your_token -H 'Content-Type: application/json' -d '{"body":"Package PKG-42 is ready at the front desk."}'
```

### Send from a terminal

Use `notify send <body>` for an update from a terminal. It targets active supported devices unless you specify routing. Set the sender title per call; sends with the same title are threaded together.

```bash
notify send "Package PKG-42 is ready" \
  --title "Warehouse desk" \
  --image https://warehouse.example.com/icon.png \
  --url https://warehouse.example.com/packages/PKG-42
```

| Flag | Type | Description |
| --- | --- | --- |
| `--title` | string | Sender name. Defaults to `Notify`. |
| `--image` | url | Public HTTPS avatar, same rules as the webhook `imageUrl`. |
| `--url` | url | Web URL, app deep link, or Shortcuts URL opened when tapped. |
| `--device` | id, repeatable | Target eligible devices by stable ID. Included for free. |
| `--project` | string | File the notification into a named project in the Notify app inbox. |
| `--summary` | string | Short push/preview text for a long body. Bodies can hold up to 8,000 characters. |
| `--markdown` | boolean | Record the body as Markdown (same as `--body-format markdown`); V1 renders plain text. |
| `--app` | app id | Open this [web app](https://notify.belweave.ai/docs#web-apps) in Notify when tapped; `--url` must stay on its origin. |
| `--idempotency-key` | string | Safe retries: replays return the original result without a second push. |
| `--stdin` | boolean | Merge a JSON object from stdin under the explicit flags. |

> A connection’s sends use the same rate/usage accounting as webhook sends: a requester window and an account-wide window. Notifications have no monthly cap.

### Organize longer messages

Use `project` to keep recurring updates together, for example Warehouse maintenance. Names are matched within an account after Unicode normalization and case folding: `Warehouse maintenance` and `warehouse maintenance` resolve to one project, with the first spelling kept for display. Unfiled messages appear under Other.

Put the full report in `body` and a short actionable digest in `summary`. The digest supplies banners and list previews; the complete body remains in the inbox detail. Clients currently show plain text and tappable links. `bodyFormat: "markdown"` stores the format metadata but does not enable Markdown rendering in V1.

> The account limit is 500 projects. A send that introduces a name beyond this limit can still create the notification, unfiled, with a `message` explaining the fallback. Existing names continue to resolve.

### Give the tap a destination

A destination belongs in `url`, either on the service or in the individual send. The recipient must tap to open it. A received push alone cannot launch a site, another app, or a shortcut.

- Prefer a universal `https://` link when the destination supports app and browser routes. On iOS, an installed associated app can handle it; otherwise the website opens.
- A documented custom scheme can address an installed app directly, such as `warehouse://packages/PKG-42`. Without a handler, the destination cannot open.
- URL-encode spaces and reserved characters in route names and query values. Shortcuts are an iOS-specific destination.

```json
{
  "body": "Package PKG-42 is ready for collection.",
  "url": "warehouse://packages/PKG-42"
}
```

For an iPhone shortcut, follow Apple’s [run-shortcut URL format](https://support.apple.com/guide/shortcuts/run-a-shortcut-from-a-url-apd624386f42/ios). Match the saved shortcut’s name and encode its query values. `input=text` passes the `text` value; `input=clipboard` uses the device clipboard.

```json
{
  "body": "Warehouse pickuped. Tap to run the follow-up.",
  "url": "shortcuts://run-shortcut?name=Package%20Pickup&input=text&text=collect%20PKG-42"
}
```

> The device may ask the person to unlock or grant shortcut permissions. Notify cannot run a shortcut merely because a notification arrived. The server rejects executable or local schemes including `javascript:`, `data:`, `file:`, `blob:`, and `about:`.

## Ask for a decision

Pause work for a person’s approval, choice, or reply. Creating a prompt is the beginning of that exchange; use its confirmed server outcome to decide what happens next.

### Ask and wait in the terminal

Use `notify send ask <prompt>` when later work depends on an answer. Select one of `--approval`, `--yes-no`, or `--text`; ordinary send appearance flags also apply. The example asks whether a package should be held for collection.

```bash
notify send ask "Hold package PKG-42 until tomorrow?" \
  --approval --title "Warehouse desk" --wait --timeout 15m
```

For approval or yes/no, `--live-activity` requests an iOS interactive card (iOS 17+) with `approval`, `shell`, `verdict`, or `signal` style. Native Android receives an ordinary actionable alert for this presentation. The prompt is limited to 240 characters, labels to 1–24 characters, and expiry to eight hours. This presentation accepts no text replies, images, or URLs. Custom primary/secondary labels change displayed verbs; returned actions remain canonical.

```bash
notify send ask "Send the pickup reminder?" \
  --approval --live-activity --style signal \
  --primary-label Send --secondary-label Deny \
  --wait --timeout 15m
```

`--wait` waits for a terminal answer, subject to `--timeout` (by default, the requested prompt expiry: 15 minutes unless changed). `--poll` waits up to 20 seconds. Either wait may time out while the prompt stays pending; neither cancels it. Prompt expiry defaults to 15 minutes and is set separately with `--expires-in`. Resume with `notify interaction wait <id>`; that standalone command defaults to a 60-second wait. A pending `interaction get` can exit successfully, so do not treat a successful read as approval.

```json
{
  "interaction": {
    "id": "int_7MFuml-SqoUmpPLo",
    "kind": "reply",
    "status": "replied",
    "response": "Leave it at the desk",
    "respondedAt": "2026-10-12T11:54:17.927Z"
  },
  "accepted": 1,
  "timedOut": false
}
```

### Ask from a webhook

For a webhook question, include `response` next to the message. Choose `approval` for Approve/Deny, `yes_no` for Yes/No, or `text` for a typed reply. The warehouse example asks whether a delayed parcel should be held.

```json
{
  "title": "Warehouse pickup",
  "body": "Hold package PKG-42 for collection?",
  "response": {
    "type": "approval",
    "expiresInSeconds": 900,
    "correlationId": "pickup-42",
    "callback": {
      "url": "https://warehouse.example.com/notify-answer",
      "token": "callback_token_placeholder_32_chars"
    }
  }
}
```

| Field | Type | Description |
| --- | --- | --- |
| `type` | string, required | Required choice of `approval`, `yes_no`, or `text`. |
| `expiresInSeconds` | integer | Answer window: integer from 30 to 86,400 seconds; default 900. |
| `correlationId` | string | Optional sender-defined correlation value, maximum 100 characters, echoed in the callback. |
| `callback.url` | string | Classic answered-callback destination. Requires public HTTPS and a paired callback token. |
| `callback.token` | string | Classic callback bearer credential and HMAC key, 16–512 characters. |
| `callback.target` | object | Connector destination `{ connectorId, route, path }`; use instead of callback.url/token. See the connector receiver guide. |
| `webhookId` | string | Account-owned saved reply webhook ID (`whk_…`) selected for this interaction, in addition to applicable service/account defaults. |

A successful question send adds pending-response metadata. Store the event ID and deadline; acceptance of the push does not authorize the requested action.

```json
{
  "ok": true,
  "eventId": "evt_example_pickup",
  "delivered": 1,
  "response": { "status": "pending", "expiresAt": "2026-10-11T18:19:04.000Z" }
}
```

### Read or cancel the outcome

Read the saved event ID to find a webhook question’s current outcome. To stop accepting answers before its deadline, POST to `/cancel`. Canceling the question and withdrawing the displayed notification are separate operations.

```bash
curl https://notify.belweave.ai/hooks/whk_your_token/events/evt_example_pickup

curl -X POST https://notify.belweave.ai/hooks/whk_your_token/events/evt_example_pickup/cancel
```

```json
{
  "ok": true,
  "event": {
    "id": "evt_example_pickup",
    "response": {
      "status": "approved",
      "action": "approve",
      "text": null,
      "correlationId": "pickup-42",
      "respondedAt": "2026-10-11T18:06:52.000Z",
      "expiresAt": "2026-10-11T18:19:04.000Z"
    }
  }
}
```

- `pending` means no settled answer. Terminal statuses are `approved`, `denied`, `yes`, `no`, `replied`, `expired`, and `canceled`.
- A text answer has `action: "reply"` and a populated `text`; choice answers carry the selected canonical action and `text: null`.
- Reading an overdue pending question settles it as `expired`.
- Only pending responses can be canceled; other states return `404` from cancel. The webhook can only inspect events belonging to its service.

> An offline client may queue an answer locally. Queued is not server-confirmed: another valid answer, expiry, cancellation, or removal of access can win before synchronization. Continue automation only after the server reports the required terminal outcome.

### Route agent permission requests

Permission hooks let Claude Code, Codex, and OpenCode V1/V2 pause for a person’s decision. A grant requires an explicit, valid approval and applies once. Denial, timeout, invalid input, authentication or network errors, and no accepted or queued device delivery all deny the request. Queued delivery keeps waiting; only a server-confirmed explicit approval grants permission.

```bash
notify permissions setup all
notify permissions doctor
```

Keep `notifications:send`, `interactions:create`, and `interactions:read` on the connection. The default login includes them; narrowed logins must request them. Setup and `permissions doctor` check scopes before installing hooks.

Install one integration with `permissions setup claude`, `codex`, or `opencode`. Codex also requires review/trust in `/hooks`. On macOS, OpenCode setup installs its V1 plugin and V2 background connector. `notify permissions uninstall all` removes Notify-owned hooks and services.

The phone receives a limited request summary: agent name, permission/tool name, project-directory basename, and resource count. It does not receive raw commands, patches, prompts, file contents, URLs, environment values, transcript paths, or absolute paths.

## Track work

Keep one task’s current stage visible as work progresses. iOS uses ActivityKit on capable builds; native Android uses an ordinary ongoing progress notification through the same API.

### Begin a task

| Route | Purpose |
| --- | --- |
| `POST /hooks/:token/live-activities` | Start an activity. Returns 201. |
| `GET /hooks/:token/live-activities/:id` | Read the current state. |
| `PATCH /hooks/:token/live-activities/:id` | Apply a partial update. |
| `POST /hooks/:token/live-activities/:id/end` | Settle and dismiss the activity. |

Start one activity for a task that has several meaningful stages. `title` identifies the task and `status` describes its current stage; both are required. Here a warehouse pickup begins preparation.

```bash
curl -X POST https://notify.belweave.ai/hooks/whk_your_token/live-activities \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: pickup-42-start' \
  -d '{
    "title": "Pickup PKG-42",
    "status": "Preparing",
    "subtitle": "Pick and pack",
    "detail": "Warehouse team is gathering the parcel",
    "progress": 0,
    "symbol": "build",
    "accentColor": "#FF9F0A"
  }'
```

```json
{
  "ok": true,
  "activityId": "act_example_pickup",
  "sequence": 0,
  "status": "active",
  "accepted": 1,
  "failed": 0,
  "state": { "title": "Pickup PKG-42", "status": "Preparing", "progress": 0, "symbol": "build" },
  "expiresAt": "2026-10-12T02:04:11.000Z",
  "staleAt": "2026-10-11T22:04:11.000Z",
  "endedAt": null
}
```

Save `activityId` for subsequent writes. Alternatively, set a start `key` and use that alias in the URL. Start, update, and end accept service-scoped `Idempotency-Key` headers: a matching replay returns `idempotent: true`; a changed payload with the same key returns `409`.

> On iOS, a capable Notify build must register a push-to-start token. Native Android uses an ordinary ongoing progress notification through FCM on these same routes. A `201` with `accepted: 0` and a `message` means no eligible target accepted the start. Neither provider acceptance nor a successful response proves that a card is visible.

### Report the next stage

Update the existing task with `PATCH` instead of starting another one for each stage. Omitted fields keep their current values, and a committed update increments `sequence`. Include at least one changed field besides `ifSequence`.

```bash
curl -X PATCH https://notify.belweave.ai/hooks/whk_your_token/live-activities/act_example_pickup \
  -H 'Content-Type: application/json' \
  -d '{ "status": "Packing", "progress": 0.6, "accentColor": "#64D2FF" }'
```

- Clear an optional `subtitle`, `detail`, or `progress` value by sending `null`.
- For concurrent writers, send the last observed `ifSequence`. `409 Sequence conflict` includes the current state so the writer can reconcile.
- An ended or expired activity rejects updates with `409 Live Activity is already terminal`.

### Finish the task

```bash
curl -X POST https://notify.belweave.ai/hooks/whk_your_token/live-activities/act_example_pickup/end \
  -H 'Content-Type: application/json' \
  -d '{ "status": "Collected", "progress": 1, "symbol": "success" }'
```

End the activity when the pickup completes or stops. You may omit the body. On iOS, `dismissAfterSeconds` sets the finished card’s remaining visibility: 0–14,400 seconds, default 0.

An iOS start reports its update token asynchronously. A provider-accepted start remains `starting` until registration. Fresh starts share a durable 60-second interval per device across all agents, services, and interactive prompts; additional starts return `queued`. This precaution does not guarantee display. Updates and ends also queue while a token is missing. When delivery becomes possible, only the newest contiguous routine progress snapshot is sent; starts, interaction changes, and endings are preserved. `superseded` counts skipped snapshots without claiming delivery. End marks the logical activity ended immediately. Recovery runs every 15 seconds and work expires at its original deadline. `accepted`, `failed`, `queued`, and `superseded` are disjoint operation outcomes. Transient rejection stays queued. None of these counts alone confirms device display.

> The end route supplies `status: "Complete"` and `symbol: "success"` when omitted, replacing the previous values. Set them explicitly for a different final message.

### Track from a terminal

The CLI can start, update, inspect, and end the same activity lifecycle without a webhook. Use the returned ID or a start `--key`; select an iOS progress layout with `--style`. The example uses one key for a warehouse preparation task.

```bash
notify activity start --key pickup --replace --style ring \
  --title "Pickup PKG-42" --status "Preparing" --progress 0.1

notify activity update pickup --status "Packing" --progress 0.6 --if-sequence 0

notify activity end pickup --status "Collected" --progress 1 --dismiss-after 45s
```

- `--replace` deliberately ends activities occupying the target slot or key before starting this one.
- `--if-sequence` applies the write only at the expected sequence; reconcile a conflict before retrying.
- Progress styles are `standard`, `ring`, `hero`, `terminal`, and `steps`. An update can change the style.
- Inspect one activity with `activity get <id|key>`, or recent activity records with `activity list`.

## Bring a web app

Make your warehouse board or household planner available inside Notify. Register its origin and send updates that open the relevant page. Add optional microAuth() when your site needs sign-in.

### Add your site

Bring an HTTPS site you control into Notify using `notify apps create`. A repeated registration of the same URL updates its name, icon, or project. Ordinary websites open without sign-in. An integrated site can explicitly request microAuth(); the viewer then approves sign-in and chooses any profile details to share. New sharing choices start off. Manage the profile in Settings → microAuth profile and per-app grants in Access & details.

```bash
notify apps create --name "Warehouse board"   --url https://warehouse.example.com --icon https://warehouse.example.com/icon.png

notify apps list
notify apps remove app_example_warehouse
```

Use an HTTPS launch URL. Only `localhost` development URLs may use plain HTTP. Each account can register up to 100 apps. Agent management requires `apps:read`/`apps:write`; reauthorize if these scopes are missing.

The iOS hosted view requires iOS 18.4+. It supports standard web inputs for camera, microphone, location, motion, and files. iOS builds with device features additionally expose Promise-based `window.notify.haptics(style)`, `authenticate(reason)`, `authenticateChallenge(challenge)`, `speech.start()`/`speech.stop()`, `share(data)`, `openSettings()`, and `capabilities()`. Pocket-friendly optional helpers on both platforms include `haptic(style)` with light, success, or warning, no-argument `authenticate(): Promise<boolean>` for local biometrics without passcode fallback, and `dictate(): Promise<string>` for native speech. Passing a reason to `authenticate(reason)` retains the detailed result object. Keep keyboard input available and handle permission denial or recognition errors. Local Face ID does not require passkey domain association. Native Android builds with device features expose `getToken()`, `close()`, `capabilities()`, `haptics()` (also `haptic()`), `authenticate()`, `speech.start()`/`speech.stop()`, `share()`, and `openSettings()`. Android authentication and speech use an app-name and exact-origin Allow once dialog for each request. Android does not provide signed `authenticateChallenge` proofs, file/download, location, or inline-media integrations. Android passkeys are limited to `https://pocket.belweave.ai`, API 28+, and supported WebKit credential integration; they require Pocket Digital Asset Links association with the actual signed app certificate. A true capability flag does not verify that association; iOS passkeys require AASA and Associated Domains. Neither association follows automatically from registering a hosted app. Feature-detect for platform/build/browser differences. Review the app-level grant and OS permission separately. On iOS before 27, blocking the Notify location wrapper cannot fully gate raw WebKit geolocation; set Location to Never in system settings for a complete OS-level revocation. Local biometric authentication is an owner check, not server identity proof; a signed challenge requires your service to bind the key and validate a fresh nonce.

> On iOS, Settings → App device permissions stores Ask/Allow/Block per account, app, and origin. Allow does not bypass OS denial, and Ask again does not reset OS/WebKit grants. On iOS versions before 27, raw WebKit location calls can bypass the wrapper’s revocation; disable Notify’s global Location permission to fully stop that access. A local biometric check is not server identity. Server authorization needs a verified pass, verified WebAuthn assertion, or an enrolled and verified challenge proof.

### Establish the viewer’s identity

For new integrations, download the SDK from [Notify Tools](/docs/quickstart#cli-install) and copy the release’s complete `sdk` directory into your project as `vendor/notify`. Import browser helpers from `./vendor/notify/browser.mjs`. Feature-detect `supportsMicroAuth()` and call `connectMicroAuth({ challengePath, sessionPath, fields })` from your site’s sign-in control. Your server uses `createMicroAuthServer` from `./vendor/notify/server.mjs` with a durable challenge store. The SDK verifies the signed pass against a fresh, single-use challenge bound to an HttpOnly browser cookie, then your server creates its own session. Current microAuth support targets iOS bridge version 3; keep ordinary browser and older-client sign-in available.

Optional profile fields are name, email, phone, timeZone, pronouns, and custom text. The viewer sees actual values and chooses each custom detail separately. The app receives only requested and approved values. Profile values are self-described; email is the Notify account email, not an independent verification claim. Revoking access stops future passes but does not end the website’s existing sessions or erase copies it already received.

1. For legacy integrations, feature-detect the hosted bridge, then call `await window.notify.getToken()` to obtain the short-lived pass. `window.notify.close()` exits the hosted view.
2. Send the pass to your backend. Verify the ES256 JWT against [Notify’s JWKS](https://notify.belweave.ai/.well-known/jwks.json), using a JWT verifier such as `jose.jwtVerify`.
3. Require issuer `https://notify.belweave.ai`, your exact app origin as audience, protected header `typ: "notify-pass+jwt"`, and algorithm `ES256`. Check expiry and reject replayed `jti` values before creating your own session.

| Field | Type | Description |
| --- | --- | --- |
| `sub` | string | Pairwise user ID (`hk_…`) scoped to your origin. Different origins receive different subjects; apps registered on the same origin share this subject. |
| `aud` | string | Your app's origin, e.g. `https://app.example.com`. |
| `iat / exp` | number | Passes expire two minutes after issue. |
| `jti` | string | Unique per pass; reject reused values to block replay. |
| `app_id` | string | The Notify app ID the pass was issued for. |
| `name / email` | string | Present only when the owner shares them. New sharing choices start off. microAuth() also limits the pass to the fields the site requested. |
| `team_id / team_role` | string | Present for [team apps](/docs/teams#teams-apps): the viewer's team and their current role (`owner`, `admin`, or `member`). |

> Exchange the verified pass for your server’s session; do not store the two-minute pass as a permanent login credential. microAuth remains Notify’s phone-approved, app-scoped authentication and profile-sharing flow. It does not turn hosted apps into Clerk Apple/Google clients, and the Clerk migration does not change the app’s identity or consent boundary. Native device sign-in and human social sign-in are separate from this hosted-site pass.

### Open the right page

When an update belongs to a hosted app, attach its `appId` (CLI: `--app`). Tapping opens that app. An optional `url` points to a page within the registered origin; cross-origin destinations are rejected.

```json
{
  "body": "The pickup schedule is ready",
  "appId": "app_example_warehouse",
  "url": "https://warehouse.example.com/reports/latest"
}
```

## Share with family

Coordinate household alerts without sharing an account or device. A free family has one owner and up to three additional members, with shared services and unlimited notifications.

### Invite the household

A household can share a Family account group while each person keeps their own sign-in. Create it through the signed-in `POST /api/teams` route, or `POST /api/agent/teams` with the required scope, using `kind: "family"`. An account can join only one family.

```json
{"name":"Our family","kind":"family"}
```

```bash
notify families create "Our family"
notify families invite family_id --email person@example.com
notify families list
```

Replace `family_id` with the returned ID. IDs retain the `team_…` prefix; check `kind` to identify a family. Inspect people with `notify families get`, `members`, and `invites`. Owners may `rename`, `remove-member`, or `revoke-invite`; members may `leave`.

The owner has three additional places. Pending invitations reserve a place until they expire or are revoked; trying to exceed capacity returns `409`. Invitees join as `member`. Families have no admin role or domain-restricted invitations.

The recipient opens the invite link and signs in on the website. `POST /api/team-invites/:code/accept` requires a signed-in account session (Apple, Google, or AgentID); an email-addressed invitation is restricted to that account.

### Share services and apps

A shared service can collect a household answer. Any accepted family member using a supported interactive client may respond, and the first valid server-confirmed answer settles the prompt. `respondingUserId` identifies that person. Unaccepted invitees, removed members, and outsiders cannot respond. The service creator retains its callback configuration and webhook credential.

To share your service, POST `{ "familyId": "team_…" }` to `/api/services/:id/share` or `/api/agent/services/:id/share`. POST `{ "familyId": null }` to make it personal. The agent route needs `services:write`.

```bash
notify services share service_id --family family_id
notify services share service_id --personal
```

Members receive shared alerts and have their own read state for shared inbox history. The original creator controls sharing, token rotation, and webhook URL access. The family owner may rename or delete a shared service. Leaving the family returns the creator’s services to personal use.

A family’s accepted members have unlimited notifications. Device targeting, interactive answers, callbacks, activities, and shared web apps remain included. Each member registers and manages their own devices.

> Workplace-only routes return `403` for families: rotations, paging, device groups, member-device administration, domain verification, audit logs, retention, and team billing. For a family web app, POST `{ "teamId": "team_…" }` to `/api/apps/:appId/share` or `/api/agent/apps/:appId/share`. The app’s original contributor can also use `notify apps share app_id --team team_id`. List shared apps with `GET /api/teams/:familyId/apps`; every viewer approves app sign-in individually.

## Try Mini Apps

A little app, ready when you need it. Personal Mini Apps package a small web experience for installation in Notify, with local drafts that can work offline after installation.

### Availability and ownership

> TestFlight 1.5.3 build 22 can show Mini Apps in Apps, but packaged installation and launch are disabled on physical iPhones. No Apple permission for this runtime has been established. A supported iOS simulator build enables it for local testing; Expo Go, web, and Android do not run these packages.

Today’s package format is free HTML/CSS/JavaScript: `web`, `notify-web-v1`, and `commerce: none`. It can request isolated local storage, but not network access, the hosted-app sign-in bridge, or native device features. React Native bundles and paid packages are unavailable.

Packages belong to the publishing Notify account. Sign in to that same account on the CLI and simulator, against the same Notify server. An install link does not give another account access. This personal catalog has no public marketplace or cross-account package sharing.

### Build your first package

Use Node.js 22.13+ and [Notify Tools 0.13.0](/downloads/notify-tools/v0.13.0/notify-tools-0.13.0.tar.gz). Follow [CLI setup](/docs/quickstart#cli-install) to verify and install the archive. The versioned download and checksum are hosted on Notify; no GitHub access is needed.

Download the [Offline notebook starter](/downloads/offline-notebook.zip) and unzip it into a working folder. It contains `examples/offline-notebook` with its manifest and three assets. The starter is a separate download; it is not included in the Notify Tools 0.13.0 archive. Keep those four files together.

```bash
notify auth login
notify auth status
```

In `examples/offline-notebook/manifest.json`, replace `publisher.id` with the returned `userId`. Run the following from the directory containing `examples`. The output must be outside the example directory and must not already exist; choose a fresh filename for another build.

```bash
notify mini-apps build examples/offline-notebook /tmp/offline-notebook.package.json
notify mini-apps validate /tmp/offline-notebook.package.json
```

Build and validate are local and need no login. Publish uploads the validated package to your account and checks publisher ownership; it neither installs it nor sends an alert. The server must support the Mini Apps API, and your login needs `apps:read` and `apps:write`.

```bash
notify mini-apps publish /tmp/offline-notebook.package.json
notify mini-apps list
```

Save the returned `packageId`, `installUrl`, and, on HTTPS servers, `universalUrl`. Keep the original archive. Changed content needs a new manifest version; different bytes cannot replace an existing account/app/version.

> Optional: `notify mini-apps offer <packageId>` deliberately sends an availability notification to your own eligible devices and needs `notifications:send`. It does not install the app or bypass the runtime gate. Skip it when testing without push.

### Test locally and on your phone

1. Use a supported local iOS simulator build of Notify, signed in to the publishing account and configured for the same server. Open Apps → Mini apps · Yours, then choose Offline notebook · Install 1.0.0. No offer or push is required.
2. Review the publisher, version, age rating, and capabilities, then tap Install. Installation needs connectivity and age eligibility. Open the installed notebook, type a draft, and wait for “Saved on this device.”
3. Close and reopen the notebook with networking off. The draft should remain. Restore connectivity for catalog updates and account-status checks; offline devices cannot receive revocations immediately. Removing the app and local drafts clears its saved content.

Simulator testing requires a compatible native Notify build; Expo Go cannot load the package runtime. Contact [info@belweave.com](mailto:info@belweave.com) for developer-preview access. You can build and validate the starter locally with Notify Tools while waiting for runtime access.

Want something on your phone today? Open an existing [hosted app](/docs/web-apps) from Apps, or register an HTTPS site you control with `notify apps create --name "My demo" --url https://your-site.example`. Replace the example URL with your real site. That demonstrates opening a web tool in Notify, not signed package installation or packaged offline storage. The notebook’s `window.notifyMini.storage` is absent in a hosted view.

Physical Mini Apps testing is deferred until the required Apple permission and release review are completed and an explicitly enabled compatible binary is distributed. TestFlight acceptance, a visible catalog, and publication do not establish that approval.

## Meet Notify Network

Notify Network connects agents, apps, and people through Notify. A tool brings the update, Notify reaches the right person, and their answer can keep the work moving.

### Connect agents, apps, and people

Start with the connections you already control: a service webhook for updates, a scoped CLI or MCP connection for an agent, and an app that gives the message somewhere useful to open. For example, a household planner can ask about a pickup and open the schedule when tapped.

- [Services](/docs/quickstart#create-a-service) name the source and receive webhook updates. Each webhook credential belongs to its service; keep it private.
- [Agents](/docs/quickstart#mcp-connect) connect with revocable permissions to send updates, ask questions, or track work. A human answer is required for approval.
- [Hosted apps](/docs/web-apps) put your web tools in Apps. Attach an app ID to an update to open the relevant page within its registered origin. Each viewer chooses sign-in and profile sharing.
- [Reply webhook subscriptions](/docs/notification-api#reply-webhooks) send answered, expired, or canceled interaction events to an enabled endpoint you configure. They are event subscriptions, not paid subscriptions or a public feed.

Notify Network describes these authorized connections. It does not provide public discovery, automatic access to strangers’ tools, or a cross-account Mini Apps package catalog. [Personal Mini Apps](/docs/mini-apps) remain owned by one publishing account.

### Share with the right people

A [free family](/docs/family) brings up to four people together while each keeps their own sign-in and devices. Share a service to bring its alerts into the household; the first valid answer from an accepted member settles a shared question. Members keep their own inbox read state, and the service creator retains the webhook credential and callback configuration.

Share a hosted tool with your family using `notify apps share app_id --team family_id`. Replace both IDs with your actual app and family IDs; family IDs use the `team_…` prefix. Each viewer approves app sign-in independently. Sharing a hosted app does not share a personal package.

Existing workplace teams also support shared apps and responder workflows. New workplace team signup is unavailable; [team administration](/docs/teams) documents the capabilities retained by existing teams. Families have their own smaller feature set, without workplace rotations, paging, or device groups.

Begin small: connect one tool, give its updates a useful destination, and share it only with the people involved. You can add more connections as the routine earns its place.

## Delivery and reliability

Understand what a send proves, how retries behave, and how device availability affects an update. Use these rules when turning the guides into unattended workflows.

### Acceptance is not a receipt

```json
{
  "ok": true,
  "eventId": "evt_example_pickup",
  "delivered": 1
}
```

Save `eventId` if you will inspect an answer or request withdrawal. `delivered` counts push requests accepted across the configured providers: Expo for iOS and FCM for native Android. It is not a delivery receipt, proof of display, read state, or a human answer. Notification detail responses now include `pushReceipts` for the reader’s iOS devices: `pending`, `accepted` by Apple, `error` with a safe provider code, or `unknown` when no receipt arrives within 24 hours. Checks begin after one minute and retry every five minutes; diagnostics are retained for 30 days. Older sends have no receipts. A receipt still cannot prove on-screen display. `DeviceNotRegistered` retires only the matching Expo token; a newer registration and Live Activity credentials are preserved.

Interactive service questions persist their original push targets with the question before sending. While retries remain, HTTP `202` includes `queued: N` alongside the persisted `delivered` count. Repeat the identical payload and idempotency key to read that same event’s current counts. Retries preserve the interaction and response credential, recheck authorization and expiry, and never add newly registered devices. Exhausted delivery with no provider acceptance returns `502`. A provider request can repeat after an ambiguous network result; deduplicate by interaction ID.

> With no eligible devices, the event can still be created with `delivered: 0` and an explanatory `message`. Decide explicitly whether your workflow requires an available device. For questions, proceed only after an approved or otherwise appropriate terminal answer.

### Retry without duplicate sends

Give each logical send a stable `Idempotency-Key` when a network retry is possible. The header accepts 1–200 characters, and its namespace belongs to the service. Reuse a key only for an identical payload.

- An already completed matching request returns its original event and `idempotent: true`.
- A matching request still being processed returns `202 Accepted`; wait before checking again.
- A key associated with a different payload returns `409 Conflict`.
- An empty key or a key longer than 200 characters returns `400`.

### Choose recipients

Omit targeting to use the account’s active supported iOS and native Android devices, ordered by most recent activity. Personal accounts support unlimited devices.

Set `deviceIds` to 1–50 stable IDs from the dashboard when only selected devices should receive the update. A foreign or unknown selection returns `400 Invalid device selection`. Owned devices that are inactive or unsupported are skipped. Workplace `group` targeting uses a separate permission check.

```json
{
  "body": "Package PKG-42 needs a pickup decision.",
  "deviceIds": ["dev_your_iphone_id"]
}
```

Targeting has no paid personal-plan gate. Availability still depends on the device’s registration, platform capabilities, and operating-system notification settings.

### Remove an obsolete alert

If a package alert becomes obsolete, use its saved `eventId` to request removal from the service owner’s active supported devices. Withdrawal also cancels an associated pending question. To stop answers without requesting alert removal, use [question cancellation](/docs/ask-decision#response-status).

```bash
curl -X POST \
  'https://notify.belweave.ai/hooks/whk_your_token/events/evt_example_pickup/withdraw'
```

```json
{
  "ok": true,
  "eventId": "evt_example_pickup",
  "status": "withdrawn",
  "accepted": 1
}
```

> `accepted` counts provider acceptance of the removal command, not confirmed device removal. Background work may run late or never run, including after an iOS force-quit. Use the webhook belonging to the original event. Withdrawal commands target the owner’s devices, not every original family/group recipient; those recipients may retain their alert. A settled repeat returns the status with `accepted: 0` and `idempotent: true`, without another command.

### Rate limits and usage

| Limit | Personal · free | Family |
| --- | --- | --- |
| Requests per minute, per service | 300 | 300 |
| Requests per minute, per account | 1,500 | 1,500 |
| Notifications per month | Unlimited | Unlimited |
| Active devices | Unlimited | Unlimited |

Hosted defaults are shown above. Notifications, questions, and activity operations share rolling 60-second counters. A per-minute rejection returns `429`, `Retry-After: 60`, and `retryAfterSeconds`. There is no monthly notification cap.

Personal and Family include unlimited notifications and devices for free. Per-minute rate limits still apply to notifications, questions, and activity operations. Teams is TBD and is not currently purchasable.

### Expiry and stale content

Set `expiresInSeconds` to the longest useful lifetime for the task: 60–28,800 seconds, with 28,800 as the default. After the deadline, state becomes `expired` and further updates are refused.

`staleAfterSeconds` expresses how long an update should be trusted: 0–28,800 seconds, default 14,400. On iOS, stale content remains visible and updateable but is marked as potentially outdated. Each update moves the stale deadline forward without exceeding expiry; omitting this field retains the previous stale window.

### Activity slots and replacement

Notify allows up to five concurrent activities per target device, including interactive activities. Distinct keys can coexist. Starting the same active key for the same service or token without replacement returns this `409` response:

```json
{
  "ok": false,
  "error": "A Live Activity with this key is still active",
  "code": "ACTIVE_ACTIVITY_CONFLICT",
  "activityId": "act_example_pickup"
}
```

Use `code` to identify a key conflict and update the existing task. Device capacity instead returns `422`, `code: ACTIVITY_CAPACITY_EXCEEDED`, and `limit: 5`; end one of your running tasks before starting another. Interactive Live Activity asks use the same rejection: if any target is full, the whole mixed-target request rolls back without a queued receipt or push. Retry the same payload and idempotency key after a slot is released. Five is Notify’s admission policy, not a hardware specification.

A deliberate replacement uses the same `key` with `replace: true`. It ends only the matching activity owned by this service or token before the new start. `replaced` reports how many were displaced, or zero if no replacement was needed. Device removal remains subject to provider and OS behavior.

Replacement preserves other keys and other requesters’ activities. The matching activity is ended everywhere, including outside the new target set, so its alias can be reused. A stable key plus replacement suits repeated runs of the same task.

### Activity delivery behavior

On iOS, starts include an alert; update/end pushes are silent. Routine progress updates use APNs priority 5. Starts, endings, and interactive state use priority 10. Priority does not grant sound, haptics, or display. Android updates modify the same ongoing progress notification; OS settings still control presentation.

Send updates at meaningful transitions. Activity operations share notification rate limits. There is no monthly allowance to exhaust.

> Read `activity.readiness` to distinguish ready devices, queued starts and accepted starts awaiting an update token. After 90 seconds without registration, `update_token_delayed` is a diagnostic warning, not proof of Apple throttling; the activity still accepts late registration until its deadline. Open Notify and check Live Activity settings if it persists. Reuse one activity for each continuing task and send meaningful progress changes. Unlimited monthly usage does not mean unlimited delivery frequency.

## Access and consent

Keep the connection’s permissions as small as the work requires. Agent grants authorize API operations; human actions and native-device permissions remain separate decisions.

### Agent credentials and scopes

People sign in with Apple or Google; agents can sign in with AgentID using their own identities. The website and iOS app also support X after you connect it to your existing account through [Connected accounts](/account/connections). First-time X sign-in asks you to sign in with Apple, Google, or AgentID and confirm the link. Connecting your identity keeps your existing Notify ownership. Connecting a Clerk identity preserves the original Notify account ID and its devices, notifications, apps, teams, and credentials. A matching email alone does not transfer ownership. If prompted to connect your original account, sign in with the Apple or Google account you previously used for Notify, inspect the displayed original account, and explicitly confirm the link. Accounts already bound to different identities cannot be merged through this flow.

An expired or replaced human session requires signing in again; the browser does not substitute an older account cookie for a missing Clerk session. If account linking is required, complete the recovery step before retrying the operation. Agent keys and OAuth access tokens authorize scoped API work, not human sign-in or account linking. An expired or revoked agent credential returns `401`; reauthorize or replace it. A `403` permission error requires reviewing scopes, not retrying with the same insufficient grant.

Agent HTTP and MCP requests accept `Authorization: Bearer` with a scoped Notify user API key. Existing `ntf_` keys remain compatible. Where Clerk is configured and your account is linked, newly issued keys use the `ak_` prefix. Create these user keys through [Dashboard → API keys](https://notify.belweave.ai/dashboard#api-keys), or use CLI device authorization; a key created outside Notify does not automatically carry Notify ownership or permissions. Missing route scopes return `403` with a `required` list. Resource access is constrained to the token’s account and permitted shared resources; foreign resource IDs return `404`.

For a server integration, choose Create API key, give it a recognizable name, and review its scopes and expiry. The Server integration preset grants `notifications:send`, `interactions:create`, and `services:write`; the default expiry is 90 days. Copy the secret when it appears and save it in your server’s secret store: it is shown only once. Revoke a key from API keys when the integration no longer needs access.

For terminal use, `notify auth login` remains the browser authorization flow. MCP clients can use OAuth or a scoped API-key Bearer header at `https://notify.belweave.ai/mcp`. Browser-authorized CLI and OAuth connections do not require you to copy a dashboard key. Existing keys retain their own permissions, expiry, and revocation state; a new Clerk-managed key uses the expiry selected when it is issued, rather than inheriting another key’s lifetime.

- The default CLI login requests all agent scopes except `events:read` and `tokens:manage`. Explicit `--scope` flags replace the default scope set; repeat the flag for each permission you need.
- Older/narrower connections may lack device, inbox, billing, team, on-call, or connector scopes. Compare `auth status` with the reference’s route requirements and reauthorize only what you need.
- Agent service list/get omit webhook URLs. Create/rotate reveal the URL once; signed-in dashboard access is separate.

### Actions outside scoped API access

A scoped connection can perform supported operational work, but it cannot make these identity or consent decisions. Use the signed-in website or native client for account consent, including an AgentID account consenting for itself. Decisions addressed to a person still require that person’s answer:

- Answer a question: only a human may approve, deny, choose yes/no, or submit a reply. Agents can read and cancel.
- Create API tokens: minting a broader token would defeat the calling token’s scope/lifetime. `tokens:manage` permits listing and revoking existing tokens only.
- Approve hosted-app sign-in, select profile sharing, or issue a Notify pass. Changing an app’s origin resets its approval.
- Register a device through its native client and push-token flow.
- Accept a family or workplace invitation.
- Acknowledge or escalate a page.

## Webhook reference

Exact notification routes, payload fields, HTTP outcomes, and receiver formats. Use the task guides for a first send or question; use this chapter when wiring a sender or callback receiver.

### Notification routes

| Route | Purpose |
| --- | --- |
| `POST /hooks/:token` | Send a notification. |
| `GET /hooks/:token/events/:eventId` | Read the state of an interactive response. |
| `POST /hooks/:token/events/:eventId/cancel` | Withdraw a pending interactive response. |
| `POST /hooks/:token/events/:eventId/withdraw` | Request removal of a delivered notification. |

All webhook writes use JSON with `Content-Type: application/json`. The token identifies the owning service. Invalid JSON fields return `400` and an `issues` array; a token that does not resolve returns `404`. Keep event IDs from successful sends for the event-specific routes.

### Notification fields

| Field | Type | Description |
| --- | --- | --- |
| `body` | string, required | Trimmed nonempty message: at most 8,000 UTF-16 code units and 16,384 UTF-8 bytes. Including `response` reduces the code-unit limit to 2,000. |
| `title` | string | Optional sender title for this message; maximum 80 characters. Otherwise use the service title. |
| `imageUrl` | string | Optional avatar, maximum 2,048 characters. Requires public HTTPS; local names and loopback, private, or link-local addresses are invalid. |
| `url` | string | Tap destination, maximum 2,048 characters. Accepts web links, universal links, documented app schemes, and iOS Shortcuts URLs. |
| `deviceIds` | string[] | Select 1–50 device IDs instead of the default active-device fanout. Exclusive with `group` and `oncall`. |
| `group` | string | Workplace device-group ID (`dgrp_…`). Requires team membership and group targeting permission. Exclusive with `deviceIds` and `oncall`. |
| `oncall` | string | Workplace rotation ID (`ocg_…`). Creates a page with a body up to 2,000 characters; exclusive with `deviceIds`, `group`, and `response`. Families cannot page. |
| `response` | object | Question configuration: approval, yes/no, or typed answer. Exclusive with `oncall` and `appId`. |
| `project` | string | Inbox project name, maximum 80 characters. The first matching send creates the project if capacity permits. |
| `summary` | string | Banner/list digest, maximum 500 characters. The full message remains in the inbox. |
| `bodyFormat` | enum | Stored format marker: `text` (default) or `markdown`. V1 clients still display plain text. |
| `appId` | string | Registered hosted-app ID (`app_…`) available to the sender. Tap opens that app; an optional `url` must match its origin. Exclusive with `response`. |

### HTTP results

```json
{
  "ok": true,
  "eventId": "evt_example_pickup",
  "delivered": 1
}
```

| Field | Type | Description |
| --- | --- | --- |
| `200` | ok | Accepted, or an idempotent replay. |
| `202` | ok | An identical request is still processing. |
| `400` | error | Invalid payload, key, or device selection. |
| `404` | error | Unknown webhook token. |
| `409` | error | Idempotency key reused with a new payload. |
| `429` | error | Rate limit exceeded. |
| `502` | error | All targeted push requests failed with the configured providers. |

Provider diagnostics belong in the dashboard activity log because raw errors may contain device push credentials. Inspect that log when a provider rejects a target; the caller receives the public status shape instead of sensitive provider output.

### Per-question callbacks

A per-question `callback` receives answered outcomes as JSON with user agent `Notify-Callbacks/1`. A classic `{ url, token }` callback uses a public HTTPS destination and `Authorization: Bearer <callback.token>`. A connector `{ target: { connectorId, route, path } }` resolves the active clawchest.com address when delivery runs. Both carry `Notify-Timestamp` and `Notify-Signature: v1=<hex>` over `{timestamp}.{rawBody}`. Redirects are refused and each request times out after 10 seconds.

```json
{
  "type": "notification.response",
  "interactionId": "int_example_pickup",
  "actionDigest": "<the stored interaction actionDigest>",
  "eventId": "evt_example_pickup",
  "correlationId": "pickup-42",
  "kind": "approval",
  "status": "approved",
  "action": "approve",
  "text": null,
  "respondedAt": "2026-10-11T18:06:52.000Z"
}
```

The callback’s `kind` maps a `text` request to `reply`; `approval` and `yes_no` keep their names.

Correlate and deduplicate answers using `interactionId`. The existing `eventId` and `correlationId` remain available for service notifications, but can be null for agent-created asks. `actionDigest` identifies the exact stored Notify prompt; it is not proof that an external action executed. Accept additional JSON fields when reading callbacks. The callback tries immediately and, on non-2xx failure, again after 30 seconds, 2 minutes, 10 minutes, and 1 hour. Five failed attempts exhaust delivery. Store an outcome durably before returning 2xx.

> This callback only reports a person’s answer. It does not report expired or canceled questions. Poll the event route for those states, or use an account reply webhook for interaction lifecycle events.

### Account reply webhooks

Use an account reply webhook when several tools should report interaction outcomes to one receiver. It subscribes to `interaction.answered`, `interaction.expired`, and/or `interaction.canceled`. Create a named HTTPS endpoint in the dashboard and enable it. Use for account replies makes it the single account default, including service-free MCP/API prompts. API and MCP updates use `isDefault: true`; `false` clears that endpoint’s default status. Selecting a new default replaces the old one.

For each outcome, Notify considers the enabled account default, the interaction’s explicit `webhookId`, and the service’s current `defaultReplyWebhookId`. Each distinct subscribed endpoint receives one durable delivery record; repeated selection of the same endpoint does not duplicate that record. Network retries may still reach a receiver more than once. A service’s selected webhook must have the same owning account.

1. Select Generic HTTPS, grok, or Slack. New dashboard endpoints initially select answered, expired, and canceled outcomes; editing an existing endpoint preserves its selected outcomes. A grok routine supplies its webhook URL and Authorization header. Slack incoming webhooks receive a JSON `text` field along with delivery/signature headers.
2. For service-free MCP or API questions, enable Use for account replies on the endpoint and subscribe to the desired outcomes. This avoids attaching `webhookId` to each question.
3. Add receiver-specific custom headers if needed. Reserved security/transport headers cannot be replaced. Header values are encrypted and read back as `••••••••`; either preserve saved headers or replace the entire list with actual values, never the mask.
4. Store the one-time signing secret at the receiver. Rotation supplies a replacement and makes the old secret unusable; update the receiver accordingly.
5. Use Test & logs to queue an `interaction.answered` sample with `test: true` (`202` means queued, not delivered). Inspect up to 50 recent deliveries, including latency, response code, and saved attempt details. Older deliveries may lack per-attempt history.

Manage these endpoints with MCP `webhooks_create`, `webhooks_list`, `webhooks_get`, `webhooks_update`, `webhooks_delete`, `webhooks_test`, and `webhooks_rotate_secret`, or the dashboard `/api/reply-webhooks` routes. Create and rotate return the signing secret once. Get includes the last 50 deliveries. A delivery’s `interactionId` links it to the originating ask; test deliveries use null. Older servers may omit that field. An account can have up to 25 endpoints. Agent endpoint reads require `services:read`; writes require `services:write`.

Before parsing a request, verify `X-Notify-Signature: t=<unix>,v1=<hex>`. Sign the timestamp, a dot, and the unchanged body bytes with HMAC-SHA256 and the endpoint’s signing secret. Reject future timestamps and timestamps older than five minutes, then compare digests in constant time. Parsing and reserializing JSON cannot preserve this signature input.

Node receiver helper; supply the original body bytes as a `Buffer`:

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyNotify(rawBody, signature, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof signature !== "string") return false;
  const match = /^t=([0-9]+),v1=([a-f0-9]{64})$/.exec(signature);
  if (!match) return false;
  const [, timestamp, hex] = match;
  const now = Math.floor(Date.now() / 1000);
  const age = now - Number(timestamp);
  if (!Number.isSafeInteger(Number(timestamp)) || age < 0 || age > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(timestamp + ".")
    .update(rawBody)
    .digest();
  return timingSafeEqual(expected, Buffer.from(hex, "hex"));
}

// Use a raw-body middleware before any JSON parser:
// verifyNotify(req.body, req.get("X-Notify-Signature"), process.env.NOTIFY_WEBHOOK_SECRET)
// Only after verification: JSON.parse(req.body.toString("utf8"))
```

Python receiver helper; supply the original body bytes as `bytes`:

```python
import hashlib
import hmac
import re
import time


def verify_notify(raw_body: bytes, signature: str, secret: str) -> bool:
    if not isinstance(raw_body, bytes) or not isinstance(signature, str):
        return False
    match = re.fullmatch(r"t=([0-9]+),v1=([a-f0-9]{64})", signature)
    if not match:
        return False
    timestamp, received = match.groups()
    age = int(time.time()) - int(timestamp)
    if age < 0 or age > 300:
        return False
    signed = timestamp.encode("ascii") + b"." + raw_body
    expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, received)

# Read the raw body before calling the framework's JSON parser.
# After verification, decode with json.loads(raw_body).
```

> Use durable deduplication after signature verification. `X-Notify-Delivery-Id` identifies a delivery across retries; commit its record together with the side effect. Already processed IDs should return 2xx. Retries preserve the ID and body but refresh the signature timestamp. Tests and separate lifecycle outcomes receive separate IDs.

Reply webhooks try immediately and then after 1, 5, 15, and 40 minutes. Timeouts and 5xx responses retry; 4xx responses and redirects are final failures. Any 2xx acknowledges delivery. Disabling stops new deliveries. These endpoints can coexist with classic callbacks, but their payloads, signature headers, and retry schedules differ.

## Activity fields and layouts

Field limits and platform-specific appearance for progress activities and interactive iOS cards. The lifecycle guide is [Track work](/docs/activity-api).

### State fields and iOS layouts

| Field | Type | Description |
| --- | --- | --- |
| `title` | string, required | Task name, maximum 80 characters. Required at start; may be replaced by an update. |
| `status` | string, required | Current stage, maximum 60 characters. Required at start. |
| `subtitle` | string | Optional short second-line summary replacing visible status in expanded/Lock Screen layouts only, maximum 120 characters. Compact and small-banner views retain status. `null` clears it. Display budgets below are smaller than the accepted payload limit. |
| `detail` | string | Optional supporting text, maximum 240 characters, shown in a two-line detail region. `null` clears it. Overflow remains bounded to the available layout. |
| `progress` | number | Optional fraction from 0 through 1. `null` clears it. |
| `symbol` | enum | Progress icon: `terminal` (default), `code`, `build`, `success`, or `warning`. |
| `accentColor` | string | Six-digit hex, `#RRGGBB`. Defaults to `#246BEB`. |
| `style` | enum | iOS progress layout: `standard` (default), `ring`, `hero`, `terminal`, or `steps`. Updates can select another layout; older builds may use the default. |
| `privacyMode` | enum | `standard` or `private`. On iOS/APNs, private replaces start-alert text with a generic message; the activity content still includes its title/status. |
| `key` | string, start only | Start-time alias, maximum 100 characters, accepted in place of the generated activity ID. Reusable after the previous activity becomes terminal. |
| `replace` | boolean, start only | Start-time replacement flag (default false). Ends only the matching active key owned by the same requester. The response includes `replaced`. |
| `deviceIds` | string[] | Start targeting list of 1–50 device IDs. Omit for eligible capable devices. |

These are iOS layout choices, illustrated below. Progress layouts and interactive choice layouts serve different requests. Native Android shows ordinary notifications; it does not reproduce ActivityKit, Dynamic Island, or this layout gallery. These captures illustrate layout rather than proving push delivery.

Use a short title for the task name, `status` for its current stage, and `subtitle` for a whole-word second-line summary when useful. Display budgets guide writing; they are not guaranteed fit or API rejection limits. Glyph widths, device size, locale, and text size change available space. The native layout measures text, can reduce it to 75% of its base size, then clips at a whole-word boundary if needed. Original text remains available to VoiceOver.

Expanded Dynamic Island and Lock Screen layouts keep a static second-line summary. Its overflow continues before explicit `detail` in a two-line detail region; title overflow does not crowd that region. Compact views hide title, subtitle, and detail; they retain status with a suggested eight-character authoring budget when progress is absent, except terminal, which shows its prompt icon. Actual fit uses measured width rather than a fixed character cutoff. With progress, the percentage takes precedence. Detail can still exceed its two-line space; do not rely on every accepted character being visible.

> A continuous marquee is not promised. [Apple limits widget and Live Activity animations to two seconds](https://developer.apple.com/documentation/widgetkit/animating-data-updates-in-widgets-and-live-activities), and Always-On presentation skips animations. Notify uses the static whole-word summary and two-line detail fallback, including with Reduce Motion enabled; this fallback does not animate.

| Field | Type | Description |
| --- | --- | --- |
| `standard` | Compact | title 0; subtitle 0; status 8; detail 0 |
| `standard` | Expanded / Lock Screen | title 32; subtitle 36; status 36; detail 100 |
| `ring` | Compact | title 0; subtitle 0; status 8; detail 0 |
| `ring` | Expanded / Lock Screen | title 32; subtitle 30; status 30; detail 80 |
| `hero` | Compact | title 0; subtitle 0; status 8; detail 0 |
| `hero` | Expanded / Lock Screen | title 24; subtitle 36; status 36; detail 100 |
| `terminal` | Compact | title 0; subtitle 0; status 0; detail 0 |
| `terminal` | Expanded / Lock Screen | title 36; subtitle 40; status 40; detail 100 |
| `steps` | Compact | title 0; subtitle 0; status 8; detail 0 |
| `steps` | Expanded / Lock Screen | title 32; subtitle 36; status 36; detail 100 |

> Suggested visible-character budgets, not guaranteed fit or API hard limits: proportional glyphs, device width and accessibility text size change fit. 0 means hidden. Expanded and Lock Screen subtitle replaces status on line 2; compact and small banners retain status. Omitted subtitle uses status. Overflow shares the two-line detail area with explicit detail. Compact percentage takes precedence over status when progress is supplied; terminal shows percentage or its prompt icon and never status.

- `standard` — Default progress card with an icon, two text lines, percentage, and horizontal track.
- `ring` — Circular progress display with the percentage inside the gauge.
- `hero` — Large status text above a bottom-edge progress track.
- `terminal` — Command-prompt presentation with monospaced status and secondary detail.
- `steps` — Five stage indicators for workflows better described by phases.
- `approval` — Default two-choice prompt with balanced decision buttons.
- `shell` — Compact command-style question and decision controls.
- `verdict` — Centered question with a divider and prominent primary action.
- `signal` — Decision card with distinct positive and negative controls.

## CLI reference

Create persistent service endpoints and integrate the terminal client into scripts. Connection setup lives in [Get connected](/docs/quickstart#cli-install); send, ask, and activity examples live with their task guides.

### Create a persistent webhook

Create a service when another process should use a persistent webhook instead of a CLI connection. `notify services create` stores the default sender title, image, and destination and returns `webhookUrl` in JSON. Later webhook sends only need their message body.

```bash
notify services create \
  --title "Warehouse desk" \
  --image https://warehouse.example.com/icon.png \
  --url https://warehouse.example.com/pickups
```

```json
{
  "service": {
    "id": "svc_...",
    "title": "Warehouse desk",
    "imageUrl": "https://warehouse.example.com/icon.png"
  },
  "webhookUrl": "https://notify.belweave.ai/hooks/whk_your_token"
}
```

Creation requires `services:write`. `--stdin` accepts the JSON fields directly. `services list` returns service metadata with credentials omitted. Reauthorize an older or narrower connection if it lacks the write scope.

> Capture the returned URL securely. It authorizes sends through that service; it should not appear in public command logs.

### Output and terminal outcomes

Send commands write their result as JSON on stdout and diagnostics on stderr. Most other commands also produce JSON; `apps` commands use readable lines unless `--json` is supplied. Branch on a terminal wait result, or inspect explicit status fields when reading existing prompts. The exit codes below describe outcome-bearing commands.

- `0`: successful operation, or a terminal approved/yes/replied answer.
- `4`: wait timeout, canceled question, or expired question.
- `5`: terminal denied/no answer.
- `7`: no provider accepted a push request and no device delivery was queued.
- `1`: API failure; `2`: command usage error; `3`: authentication/scope failure; `6`: network failure.

```bash
if notify send ask "Start the scheduled maintenance?" --approval --wait --timeout 10m; then
  ./run-maintenance.sh && notify send "Maintenance complete" --title "Maintenance desk"
else
  echo "Maintenance was not approved" >&2
fi
```

## MCP reference

API-key and OAuth connections, granted scopes, and the tool families available to a remote client. For configuration snippets, use [Connect an MCP client](/docs/quickstart#mcp-connect).

### OAuth and discovery

For API-key MCP access, configure your client’s HTTP headers with `Authorization: Bearer` followed by your scoped `ntf_` or `ak_` user API key. Save the value in the client’s secure credential settings. This path does not use OAuth client IDs, client secrets, or browser consent; route scope checks still apply.

1. For OAuth, the first unauthenticated call returns `401` with a `resource_metadata` discovery pointer. Follow that metadata to the current authorization server. Use dynamic registration when the server advertises it and the client supports it; do not assume every hosted client registers automatically.
2. Open the consent page in the browser and sign in with Apple, Google, or AgentID.
3. Review the requested permissions and approve only the access you intend to grant. The server and client determine the consent screen; do not assume that omitting a scope grants a particular permission.
4. The client exchanges its code using PKCE and receives an access token bound to `https://notify.belweave.ai/mcp`. Follow the token response for expiry; the legacy OAuth service and Clerk need not use the same token lifetime. Request `offline_access` when a refreshable connection is needed and supported.

Tool permissions use the agent scope names. Request and inspect the needed scopes explicitly; `offline_access` concerns refresh access, not additional tool permissions. Every registered client requires S256 PKCE, including confidential clients. The OAuth resource/audience is exactly `https://notify.belweave.ai/mcp`; direct authorization and token requests must use that resource.

> MCP discovery and the authentication challenge advertise the default connection scopes, excluding privileged `tokens:manage`. Listing or revoking credentials requires an API key with that scope or an explicitly configured OAuth client and grant that allow it. If a client reports that it is not allowed to request `tokens:manage` during ordinary setup, refresh its discovery metadata and retry authorization without that scope. Do not disconnect a working dashboard connection just to refresh discovery; disconnection revokes its client access.

Disconnect a client in the dashboard to stop its access. A connection with `tokens:manage` can also list OAuth connections as `kind: "oauth"` through `/api/agent/tokens` and revoke them. Disconnecting a Clerk OAuth client also blocks that client ID for your Notify account, covering other independent grants for the same client. Reconnecting requires a newly registered client ID; merely obtaining another token for the disconnected client does not restore access.

If a hosted client asks for a static OAuth client ID, use Dashboard → Connected MCP clients → Custom OAuth clients when available. Obtain the exact callback URL from the client before registering; do not guess a callback for Meta Muse or another hosted service. Callbacks must be HTTPS, with HTTP allowed only for localhost, 127.0.0.1, or IPv6 loopback clients. Select the required permissions. Public clients have no secret. Choose a confidential client only when its server can securely store a secret, and save the secret when it is shown once. PKCE remains required.

Copy the issuer, authorization endpoint, and token endpoint displayed by Notify rather than deriving URLs from a client ID. The manual authorization and token endpoints from `/api/oauth/config` are Notify compatibility endpoints: they bind an omitted resource to the Notify MCP URL and reject a different resource. If your client supports a resource field, set it explicitly to `https://notify.belweave.ai/mcp`. Discovery clients should follow the advertised metadata. Rotate a confidential client secret to replace a lost or exposed secret, then update the client’s secure settings. A missing callback, unsupported PKCE, or an unavailable registration feature must be resolved before retrying OAuth; use scoped API-key MCP when the client supports Bearer headers.

| Route | Purpose |
| --- | --- |
| `GET /.well-known/oauth-protected-resource/mcp` | Protected resource metadata (RFC 9728) for the MCP server. |
| `GET /.well-known/oauth-authorization-server` | Authorization server metadata (RFC 8414): authorize, token, and registration endpoints. |

### Tool inventory

MCP exposes the supported agent operations through the same handlers, validation, scopes, and limits, plus helpers such as `ask`. A missing permission becomes a tool error identifying the required scope.

- `notify` creates an update; `notification_withdraw` requests its removal.
- `ask` creates a choice/reply question. Notification presentations wait for at most 25 seconds per MCP call, then return the existing interaction with `timedOut: true` if it is still pending. Continue with `interactions_wait` and that same `interaction.id`; each wait call also waits at most 25 seconds. Older requested `timeoutSeconds` values up to 600 remain accepted but are capped to this waiting budget. Creation and progress reporting consume the ask budget; stalled creation or network transport can still exceed 25 seconds of wall-clock time. The prompt expiry is separate and a wait timeout never cancels it. With `presentation: live_activity`, it returns the pending interaction immediately with `accepted: 0` and `queued: N`; use `interactions_wait` or `interactions_get` with the same creating token to observe a server-confirmed answer. Queued delivery is not provider acceptance or approval. For separate steps, use `interactions_create`, `interactions_wait`, `interactions_get`, `interactions_list`, and `interactions_cancel`.
- `activities_start`, `activities_update`, `activities_end`, `activities_get`, and `activities_list` manage ongoing work.
- `services_*`, `devices_*`, `events_list`, `activity_feed`, `inbox_*`, `apps_*`, `billing_get`, and `tokens_*` cover account resources.
- `teams_*`, `groups_*`, `oncall_*`, `pages_*`, and `connectors_*` cover shared workplace resources and internal-service connections. `webhooks_*` manages account reply endpoints.
- `auth_status` inspects the current grant; `auth_revoke` disconnects it.

Personal offline mini apps expose `mini_apps_list`, `mini_apps_get`, `mini_apps_review`, `mini_apps_export`, `mini_apps_offer`, `mini_apps_report`, `mini_apps_block`, `mini_apps_disable`, and `mini_apps_remove`. These use the same owner-only API routes. Offer requires `apps:write` and `notifications:send` and sends a push only when explicitly invoked; native installation requires consent.

> Mini-app publish and archive download use the CLI for deterministic exact bytes, with a 12 MiB archive maximum, rather than large raw contents in MCP tool text. Signing keys remain part of the authenticated native trust operation. MCP metadata export does not export archive bytes or local app data.

> MCP does not add human-answer, page-acknowledgement/escalation, invite-acceptance, app-consent/pass, or token-creation operations. Sensitive webhook URLs and join links returned by creation tools are marked as secrets.

## Agent API reference

Route inventory for scoped HTTP clients, with the canonical generated schemas. The CLI and MCP tools use this API; human consent is described in [Access and consent](/docs/access).

### HTTP route inventory

| Route | Purpose |
| --- | --- |
| `GET /api/agent/mini-apps` | List owner-only personal packages. `apps:read`. |
| `GET /api/agent/mini-apps/keys` | Authenticated signing keys for native package trust. `apps:read`. |
| `GET /api/agent/mini-apps/review` | Review owner-only package metadata. `apps:read`. |
| `GET /api/agent/mini-apps/export` | Export owner-only package metadata, without archive bytes or local app data. `apps:read`. |
| `POST /api/agent/mini-apps/publish` | Publish deterministic personal package bytes with the CLI; archive maximum 12 MiB, decoded maximum 8 MiB. `apps:write`. |
| `GET /api/agent/mini-apps/:packageId` | Get owned available package metadata. `apps:read`. |
| `DELETE /api/agent/mini-apps/:packageId` | Remove an owned package; JSON body requires `reason`. `apps:write`. |
| `GET /api/agent/mini-apps/:packageId/archive` | Download exact immutable archive bytes; verify signed digest and size before native installation. `apps:read`. |
| `POST /api/agent/mini-apps/:packageId/offer` | Explicit push to owner devices only; does not install. Empty JSON body. `apps:write` and `notifications:send`. |
| `POST /api/agent/mini-apps/:packageId/report` | Report owned package content; JSON body requires `reason`. `apps:write`. |
| `POST /api/agent/mini-apps/:packageId/block` | Block an owned package; JSON body requires `reason`. `apps:write`. |
| `POST /api/agent/mini-apps/:packageId/disable` | Disable an owned package; JSON body requires `reason`. `apps:write`. |
| `GET /api/agent/auth/status` | Describe the calling token. Any scope. |
| `POST /api/agent/auth/revoke` | Revoke the calling token. Any scope. |
| `GET /api/agent/tokens` | List the account's API tokens (never secrets). `tokens:manage`. |
| `DELETE /api/agent/tokens/:id` | Revoke another token of the same account. `tokens:manage`. |
| `GET /api/agent/devices` | List registered devices. `devices:read`. |
| `DELETE /api/agent/devices/:id` | Remove a device; it registers again when Notify next opens on it. `devices:write`. |
| `GET /api/agent/services` | List webhook services (without webhook URLs). `services:read`. |
| `POST /api/agent/services` | Create a webhook service; the webhook URL is returned once. `services:write`. |
| `GET /api/agent/services/:id` | Get a webhook service. `services:read`. |
| `PATCH /api/agent/services/:id` | Update a webhook service's title, avatar, or tap URL. `services:write`. |
| `DELETE /api/agent/services/:id` | Delete a webhook service. `services:write`. |
| `POST /api/agent/services/:id/share` | Share a webhook service with your family, or unshare it. `services:write`. |
| `POST /api/agent/services/:id/rotate` | Rotate the webhook token; the new URL is returned once. `services:write`. |
| `GET /api/agent/webhooks` | List saved reply webhooks with masked header values. `services:read`. |
| `POST /api/agent/webhooks` | Create a reply webhook; the signing secret is returned once. `services:write`. |
| `GET /api/agent/webhooks/:id` | Read a reply webhook and its last 50 deliveries. `services:read`. |
| `PATCH /api/agent/webhooks/:id` | Update a reply webhook; omit headers to preserve their saved values. `services:write`. |
| `DELETE /api/agent/webhooks/:id` | Delete a reply webhook. `services:write`. |
| `POST /api/agent/webhooks/:id/test` | Queue a signed sample interaction.answered event (202); inspect logs for completion. `services:write`. |
| `POST /api/agent/webhooks/:id/rotate-secret` | Rotate the signing secret; its replacement is returned once. `services:write`. |
| `GET /api/agent/events` | Recent webhook deliveries. `events:read`. |
| `GET /api/agent/activity-feed` | Merged account activity history, newest first (20 per page). `events:read`. |
| `POST /api/agent/notifications` | Send a one-shot push. `notifications:send`. |
| `POST /api/agent/notifications/:id/withdraw` | Withdraw an agent notification from the account's devices. `notifications:send`. |
| `GET /api/agent/interactions` | List the account's pending prompts from every source. `interactions:read`. |
| `POST /api/agent/interactions` | Ask a question (approval, yes/no, or reply). `interactions:create`, `notifications:send`. |
| `GET /api/agent/interactions/:id` | Get a prompt created by this token. `interactions:read`. |
| `GET /api/agent/interactions/:id/wait` | Long-poll a prompt created by this token until it is answered. `interactions:read`. |
| `POST /api/agent/interactions/:id/cancel` | Cancel a pending prompt created by this token. `interactions:create`. |
| `GET /api/agent/activities` | List Live Activities started by this token. `activities:read`. |
| `POST /api/agent/activities` | Start a Live Activity. `activities:write`. |
| `GET /api/agent/activities/:identifier` | Get a Live Activity by ID or key. `activities:read`. |
| `PATCH /api/agent/activities/:identifier` | Update a Live Activity. `activities:write`. |
| `POST /api/agent/activities/:identifier/end` | End a Live Activity. `activities:write`. |
| `GET /api/agent/apps` | List web apps. `apps:read`. |
| `POST /api/agent/apps` | Register a web app (an existing URL is updated). `apps:write`. |
| `GET /api/agent/apps/:id` | Get a web app. `apps:read`. |
| `PATCH /api/agent/apps/:id` | Update a web app's name, URL, icon, or project. `apps:write`. |
| `DELETE /api/agent/apps/:id` | Remove a web app. `apps:write`. |
| `POST /api/agent/apps/:id/revoke` | Revoke sign-in consent; the owner approves again on the next sign-in request. `apps:write`. |
| `POST /api/agent/apps/:id/share` | Move an app into a team, or (teamId null) back to your own apps. `apps:write`. |
| `GET /api/agent/teams` | List the teams you belong to. `teams:read`. |
| `POST /api/agent/teams` | Create a family with `kind: family`; you become its owner. Omitting `kind` or using `kind: team` returns `403` because Teams is TBD. `teams:write`. |
| `GET /api/agent/teams/:id` | A team and its members. `teams:read`. |
| `PATCH /api/agent/teams/:id` | Update a team (name: admin; auto-join and retention: owner). `teams:write`. |
| `DELETE /api/agent/teams/:id` | Delete a team (owner); its apps return to the members who added them. `teams:write`. |
| `POST /api/agent/teams/:id/leave` | Leave a team (the owner must transfer ownership first). `teams:write`. |
| `PATCH /api/agent/teams/:id/members/:userId` | Change a member's role; `owner` transfers ownership (owner only). `teams:write`. |
| `DELETE /api/agent/teams/:id/members/:userId` | Remove a member (admin or owner); the owner cannot be removed. `teams:write`. |
| `GET /api/agent/teams/:id/invites` | List invites (admin or owner). `teams:read`. |
| `POST /api/agent/teams/:id/invites` | Create a 7-day join link (admin or owner). `teams:write`. |
| `DELETE /api/agent/teams/:id/invites/:inviteId` | Revoke an invite. `teams:write`. |
| `GET /api/agent/teams/:id/apps` | The team's shared apps. `teams:read`, `apps:read`. |
| `GET /api/agent/teams/:id/groups` | The team's device groups. `teams:read`. |
| `POST /api/agent/teams/:id/groups` | Create a device group (admin or owner). `teams:write`. |
| `GET /api/agent/teams/:id/groups/:groupId` | A device group. `teams:read`. |
| `PATCH /api/agent/teams/:id/groups/:groupId` | Update a device group (admin or owner). `teams:write`. |
| `DELETE /api/agent/teams/:id/groups/:groupId` | Delete a device group (admin or owner). `teams:write`. |
| `GET /api/agent/teams/:id/devices` | Devices belonging to team members (admin or owner). `teams:read`. |
| `POST /api/agent/teams/:id/domain` | Request a DNS TXT challenge for an email domain (owner). `teams:write`. |
| `DELETE /api/agent/teams/:id/domain` | Clear the pending or verified domain (owner). `teams:write`. |
| `POST /api/agent/teams/:id/domain/verify` | Verify the pending domain's DNS TXT record (owner). `teams:write`. |
| `GET /api/agent/teams/:id/audit` | Audit log (admin or owner); members receive 403. `teams:read`. |
| `GET /api/agent/teams/:id/oncall` | The team's on-call groups with current and upcoming shifts. `oncall:read`. |
| `POST /api/agent/teams/:id/oncall` | Create an on-call group (admin or owner). `oncall:write`. |
| `GET /api/agent/teams/:id/pages` | The team's pages, newest first. `oncall:read`. |
| `GET /api/agent/oncall/me` | Your upcoming shifts and the open pages that notified you. `oncall:read`. |
| `GET /api/agent/oncall/:groupId` | An on-call group. `oncall:read`. |
| `PATCH /api/agent/oncall/:groupId` | Edit a group's name, rotation, or escalation (admin or owner). `oncall:write`. |
| `DELETE /api/agent/oncall/:groupId` | Delete an on-call group (admin or owner). `oncall:write`. |
| `POST /api/agent/oncall/:groupId/overrides` | Put someone on call for a window (admins: anyone; members: themselves). `oncall:write`. |
| `DELETE /api/agent/oncall/:groupId/overrides/:overrideId` | Remove an override. `oncall:write`. |
| `POST /api/agent/oncall/:groupId/pages` | Page the group: whoever is on call first, then escalation. `oncall:write`. |
| `GET /api/agent/pages/:id` | A page. `oncall:read`. |
| `GET /api/agent/connectors` | List tunnel connectors. `connectors:read`. |
| `POST /api/agent/connectors` | Register a tunnel connector; the callback HMAC secret is returned once. `connectors:write`. |
| `GET /api/agent/connectors/:id` | A tunnel connector. `connectors:read`. |
| `PATCH /api/agent/connectors/:id` | Update a connector's name, routes, or on-call group. `connectors:write`. |
| `DELETE /api/agent/connectors/:id` | Remove a tunnel connector. `connectors:write`. |
| `POST /api/agent/connectors/:id/heartbeat` | Mark a connector online and refresh its last-seen time. `connectors:write`. |
| `POST /api/agent/connectors/:id/access` | Gateway-only pass exchange or access revalidation; requires the connector's credential and a phone-issued pass. `connectors:write`. |
| `POST /api/agent/connectors/:id/callback-access` | Gateway-only authorization of a signed callback against its saved target and durable replay receipt. `connectors:write`. |
| `POST /api/agent/connectors/:id/rotate-secret` | Rotate a connector callback HMAC secret; the new secret is returned once. `connectors:write`. |
| `POST /api/agent/pages/:id/resolve` | Resolve a page; anyone still being paged is cleared. `oncall:write`. |
| `GET /api/agent/inbox/projects` | Inbox projects with unread counts. `inbox:read`. |
| `GET /api/agent/inbox/notifications` | Inbox notifications, newest first, with cursor pagination. `inbox:read`. |
| `GET /api/agent/inbox/notifications/:id` | One inbox notification with its full body. `inbox:read`. |
| `POST /api/agent/inbox/notifications/read-all` | Mark everything up to a list response's readThroughToken read. `inbox:write`. |
| `POST /api/agent/inbox/notifications/:id/read` | Mark one notification read. `inbox:write`. |
| `POST /api/agent/inbox/notifications/:id/unread` | Mark one notification unread. `inbox:write`. |
| `GET /api/agent/billing` | Plan, limits, and remaining usage. `billing:read`. |

```bash
notify inbox list --unread --limit 20
notify inbox read-all --project unfiled
notify interaction list
notify send withdraw anot_...
notify services rotate svc_...
notify apps update app_example_warehouse --name "Maintenance board"
notify billing
notify tokens list   # needs --scope tokens:manage at login
```

### Direct interaction fields

Direct HTTP and MCP questions use the interaction schema rather than the webhook’s nested `response` object. Create requires `interactions:create` and `notifications:send`; reading and waiting require `interactions:read`. A successful create only establishes the pending request.

| Field | Type | Description |
| --- | --- | --- |
| `title` | string, required | Sender title, 1–80 characters after trimming. |
| `prompt` | string, required | Question, 1–2,000 characters after trimming. live_activity presentation caps this at 240. |
| `kind` | enum, required | `approval`, `yes_no`, or `reply`. Note that webhook type `text` maps to agent kind `reply`. |
| `expiresInSeconds` | integer | 30–86,400 seconds, default 900. live_activity presentation cannot exceed 28,800. |
| `presentation` | enum | `notification` (default) or `live_activity`. The latter uses ActivityKit on capable iOS and an ordinary actionable alert on native Android. |
| `style` | enum | Only with live_activity: `approval`, `shell`, `verdict`, or `signal`. |
| `primaryLabel / secondaryLabel` | string | Only with live_activity: 1–24 characters each, single-line labels. Canonical returned actions do not change. |
| `imageUrl / url` | string | Optional sender image and tap destination, using notification validation. Not accepted with live_activity. |
| `deviceIds / group` | string[] / string | Optional selected devices or authorized workplace group. Supply one targeting form, not both. |
| `callback` | object | Classic URL/token or connector-target answered callback; see [per-question callbacks](/docs/notification-api#response-callbacks). |
| `webhookId` | string | Optional saved account reply endpoint (`whk_…`) in addition to applicable defaults. |

```bash
curl -X POST https://notify.belweave.ai/api/agent/interactions   -H "Authorization: Bearer $NOTIFY_TOKEN"   -H 'Content-Type: application/json'   -d '{"title":"Warehouse desk","prompt":"Hold package PKG-42 until tomorrow?","kind":"approval","expiresInSeconds":900}'
```

> For live_activity, use approval or yes_no, never reply. The generated [OpenAPI schemas](/docs/agent-api#agent-api-openapi) describe create, wait, get, list, and cancel HTTP requests. Only a valid human answer can settle the question as approved or denied.

### Generated OpenAPI schemas

Use the OpenAPI 3.1 document for exact HTTP schemas and automation. It includes `x-notify-scopes` and request validators generated from the server’s schemas. The route inventory above is kept in step with that implementation.

OpenAPI document:

```text
https://notify.belweave.ai/api/agent/openapi.json
```

## Reach an internal service

Expose a private HTTP service through Notify Connector and the OpenTunnel relay. The public hostname reaches the service; a verified Notify pass or signed callback still determines who may use it.

### Relay and connector lifecycle

The relay lives at `https://api.clawchest.com`; tunnel addresses are `https://<id>.clawchest.com` and named routes `https://<route>.<id>.clawchest.com`. Tunnel creation uses a scoped Notify agent credential, introspected at `/api/relay/introspect`. A public tunnel address provides reachability, not visitor authorization.

- `@belweave/notify-connector` uses OpenTunnel 0.5.1, declares public routes, registers web apps, and sends heartbeats. Notify retains its Cloudflare relay and AWS backend.
- After five minutes without a heartbeat, the connector is marked offline. Its configured `oncallGroupId` can receive a page.
- A callback `target` containing `connectorId`, `route`, and `path` follows the connector’s live address at delivery time.

### Expose a local port

Run the web app and the Notify CLI on your computer. Keep both processes running, then open the registered app from Apps in Notify on your phone. This example connects a local warehouse dashboard on port 3000:

```bash
notify auth login
notify connect 3000 --name warehouse
# https://warehouse.<id>.clawchest.com
```

OpenTunnel 0.5.1 adds optional PROXY protocol v1/v2 support. Use `notify connect 3000 --name warehouse --proxy-protocol v2` only when that upstream listener explicitly accepts PROXY protocol before HTTP, or before TLS for an HTTPS upstream. Ordinary web servers should omit the flag. In the SDK, a route can be `{ target: "127.0.0.1:3000", proxyProtocol: "v2" }`; plain string routes keep their existing behavior. The authenticated gateway remains mandatory and forwards the preamble only after authorization. Visitor addresses come from the relay connection, not caller-supplied HTTP forwarding headers.

The connector adopts the 0.5.1 client’s reconnect and transient-failure handling. Certificate and tunnel state remain in memory for the running connector; restarting does not restore a persisted tunnel identity or certificate.

The named origin is registered as a Notify web app. The connector's mandatory gateway validates a one-time Notify pass, then rechecks current access for every page, asset, API request and WebSocket. The exact origin prevents a sibling route from reusing its pass. The updated `notify connect` and `connectors rotate-secret` save the callback signing secret in a mode-0600 file and print only its path, including with `NOTIFY_TOKEN`; the environment token itself is not saved. The normal locations are `~/Library/Application Support/notify/connectors` on macOS and `~/.config/notify/connectors` on Linux, or beside `NOTIFY_CONFIG` when set. Older CLI releases printed the secret to stderr; update before running in an agent log. Default logins include connector read/write scopes; narrowed or older logins need `connectors:write`. Rotate with `notify connectors rotate-secret <id>`.

### Return an answer to a private receiver

To return an approval to a private receiver, register its listening port as a connector route and save that route plus the receiver path on the interaction. Notify signs the answer and sends it through the fixed `/.notify/callback` gateway ingress. The gateway authorizes the saved target, current workspace access and durable replay receipt before forwarding; callbacks cannot bypass sign-in on ordinary app routes.

```bash
# On the laptop: a tiny receiver on port 8780, then
# notify connect 8780 --name cb

# From CI or an agent:
notify send ask "Approve the maintenance window?" --approval --stdin <<'JSON'
{
  "callback": {
    "target": {
      "connectorId": "conn_example_warehouse",
      "route": "cb",
      "path": "/approve"
    }
  }
}
JSON
```

Verify `Notify-Signature: v1=<hex>` as HMAC-SHA256 of `{timestamp}.{rawBody}`, using `Notify-Timestamp` and the connector’s `callbackSecret`. A classic URL/token callback instead uses the callback token as its HMAC key and also sends a bearer Authorization header. Accept timestamps only within five minutes and reject future values.

```json
{
  "type": "notification.response",
  "interactionId": "int_example_pickup",
  "actionDigest": "<the stored interaction actionDigest>",
  "eventId": "evt_...",
  "kind": "approval",
  "status": "approved",
  "action": "approve",
  "text": null,
  "respondedAt": "2026-10-09T12:00:00.000Z"
}
```

> The SDK exports `verifyCallbackSignature` for this format. Verify before acting and deduplicate by `interactionId` because retries can repeat answers. Legacy service receivers may also use `eventId`; agent-created asks can have a null event ID. Map a verified canonical action to a known operation; do not execute text from the callback as a shell command.

### SDK middleware for a private site

The connector registers a hosted-app origin and places its mandatory gateway in front of the private dashboard. The gateway validates ES256, exact issuer and origin audience, expiry, app/workspace claims, durable one-time replay consumption and current access. It covers static assets and WebSocket upgrades as well as application requests. Your server still authorizes access to its own data using the gateway’s verified identity, and its listener must not be publicly reachable around that gateway.

For direct integrations, the SDK also supplies `honoNotifyPass`, `expressNotifyPass`, and `fastifyNotifyPass`. Configure issuer `https://notify.belweave.ai`, the exact audience, and the required atomic durable `consumeJti` store; the connector README includes a database example. Missing or unavailable replay storage denies access. These raw-pass helpers are separate from the gateway’s short-lived sessions and continuous revocation checks. `verifyNotifyPass` alone verifies cryptography, not replay or current membership.

## Existing team administration

These operations support existing workplace teams. New team signup is unavailable. Teams share organization resources while members keep their own accounts and sign-in choices; [families](/docs/family) have a smaller, separate feature set.

### Availability and usage

New workplace team creation is unavailable and returns `403`. Personal and Family plans are available for new accounts. Teams pricing is to be determined; existing teams keep their current capabilities.

Teams is TBD and new workplace signup is unavailable. Existing teams retain free access without seat charges; this administration reference serves those teams.

> Pages have no monthly notification cap, whether raised by an agent connection or service. Administrative notices about invitations and shared apps are excluded from usage accounting.

### Role permissions

Existing workplace teams assign each account one of these roles. These permissions apply to teams, not Family groups:

| Field | Type | Description |
| --- | --- | --- |
| `owner` | one per team | Everything an admin can do, plus deleting the team, managing billing, transferring ownership, verifying the email domain, auto-join, and retention. The owner must transfer ownership before leaving. |
| `admin` | role | Renames the team, invites and removes members, changes roles, removes any team app, creates and edits on-call and device groups, and reads the audit log. |
| `member` | role | Uses and adds team apps, raises and acknowledges pages, and schedules overrides for themselves. |

Assigning `owner` transfers ownership and changes the former owner to `admin`. Removing a member immediately prevents new team-app passes and removes that person from rotations.

### Issue a workplace invitation

An owner or admin can issue a revocable, single-person invite that lasts seven days. Supplying `email` also alerts an existing Notify account at that address. A `domain` restriction must use the organization’s verified domain.

```bash
notify teams invite team_... --email teammate@example.com --role member
notify teams invite team_... --domain acme.com --role member
# { "invite": { ... }, "code": "…", "url": "https://notify.belweave.ai/join/…" }
```

The join page identifies the team, inviter, and assigned role. The recipient signs in to accept; an agent may prepare an invitation but cannot join on a person’s behalf.

### Shared workplace apps

Members can add a [hosted app](/docs/web-apps) or share one they originally added with `notify apps share app_... --team team_...`. Use `--personal` to return it to personal use. Other members receive a notice and see the shared app.

- Each viewer chooses sign-in consent and profile sharing independently.
- A team-app pass carries pairwise `sub`, `team_id`, and the current `team_role`, checked when issued. The app server still decides authorization.
- The original contributor and team admins can rename/remove the app. Deleting the team returns shared apps to their original contributors.

### Target a device group

A device group (`dgrp_…`) selects workplace members and/or individual devices; it is not an on-call rotation. Owners/admins manage groups. Targeting is allowed for those admins/owners and current group members; other teammates get `403`, and cross-team IDs return `404`. `group` is exclusive with `deviceIds` and `oncall`. Group questions settle on the first valid answer from a current group member.

```bash
notify groups create --team team_... --name Maintenance --members user_a,user_b
notify send "Cold-room sensor is offline" --group dgrp_...
notify send ask "Start maintenance?" --approval --group dgrp_...
notify activity start --title Maintenance --status Running --group dgrp_...
notify page ocg_example_maintenance "Pager" --group dgrp_...
```

### Domains and retention

Organization settings belong to the existing team. To claim a domain, the owner must have a verified email there. Requesting a challenge stores a pending token; publish `_notify-verification.<domain>` with TXT value `notify-verify=<token>` and run verify. Only successful verification establishes the unique `verifiedDomain`. Pending domains need not be unique. Public email providers and Apple relay domains cannot be claimed. Auto-join is owner-controlled, off by default, and requires a verified domain; an eligible user must have `emailVerified: true` and an available seat. SSO/SCIM are future work.

Retention may be `7`, `30`, `90`, or `365` days, or `null` indefinitely. It applies to organization-targeted notifications, prompts, and pages, not personal sends. Hourly cleanup removes expired rows in bounded indexed batches and resumes shortly after a time-budget stop. It preserves pending prompts, their events, and active activities.

```bash
notify teams domain team_... acme.com
# Add TXT _notify-verification.acme.com = notify-verify=<token>
notify teams verify-domain team_...
notify teams update team_... --auto-join --retention 30
notify teams update team_... --no-domain --no-retention
```

### Review organization events

Owners and admins can inspect the append-only team audit in the dashboard or `GET /api/agent/teams/:id/audit`; members receive `403`. Filters include an inclusive `from`. CSV export walks at most 50,000 rows and reports additional rows with `X-Audit-Truncated: true`. Records contain actor, target, IP, user agent, and timestamp without message bodies or replies. Covered events include invites, membership/roles, deletion, device groups, app sharing, org-targeted notifications/activities/pages, answered approvals, and settings. The human responder is the actor for an answer. Token creation/revocation is excluded from team audit.

## Page a responder

For existing workplace teams, choose the current responder and escalate unanswered pages. This chapter covers rotations, page creation, and the human acknowledgement that stops escalation.

### Schedule responders

An existing workplace team’s owner or admin can define a responder order. Daily/weekly handoffs use an IANA time zone and local clock, preserving the chosen local time through daylight-saving changes.

```bash
notify oncall create --team team_... --name Primary \
  --members user_a,user_b,user_c --period weekly \
  --handoff 09:00 --timezone America/New_York
```

If `startsAt` is absent, the first responder’s shift begins at the most recent handoff. A timed override replaces the rotation during its window; members can schedule their own coverage. Group reads include current and upcoming shifts.

### Escalation and deduplication

A new page contacts the current responder, or the entire group if no responder is scheduled. Configured escalation continues until an acknowledgement stops it:

| Field | Type | Description |
| --- | --- | --- |
| `afterMinutes` | integer | Minutes after the previous step (or the page) without an acknowledgement. |
| `target` | next \| group | `next` pages the next person in the rotation; `group` pages everyone in it who has not been paged yet. |

The default schedule contacts the next responder after five minutes, then the group ten minutes later. Repeated `dedupKey` values join an existing open page and increment `repeatCount` without issuing another initial page.

```bash
notify page ocg_example_maintenance "Cold-room temperature above threshold" \
  --body "Sensor reports 12°C since 14:02" --dedup-key cold-room-temperature
```

### Page from a service

A service can route a message to a rotation using `oncall`. Its owner must belong to that workplace team. Do not combine it with `deviceIds`, `group`, or `response`. The webhook `Idempotency-Key` becomes the page deduplication key.

```json
{
  "title": "Cold-room monitor",
  "body": "Temperature above threshold for 5 minutes",
  "oncall": "ocg_example_maintenance"
}
```

The page response includes `pageId` and provider-acceptance count `delivered`. Keep the body within 2,000 characters. CLI equivalents are `notify page` and `notify send --oncall`.

### Acknowledge, escalate, and resolve

A current teammate can acknowledge through a supported Lock Screen action, the app, or the website. The first accepted acknowledgement stops escalation; later acknowledgements are refused. Notify requests clearing the other devices’ alerts. Escalate immediately advances to the next step.

> Acknowledgement and escalation require a person. Agents can raise, inspect, and resolve pages, but cannot acknowledge or escalate as if a person had taken responsibility.

When the incident is finished, resolve it on the website or with `notify pages resolve page_... --note "Maintenance completed"`. Resolution also stops escalation on a page that was never acknowledged.
