# BreakPass MCP tool reference

Last updated 23 Oct 2026. Page: https://breakpass.ai/docs/mcp/ · JSON twin: https://breakpass.ai/docs/mcp/tools.json

BreakPass is a hosted MCP server. Your agent reaches you by WhatsApp, SMS or phone call through eight tools. Each verified number carries the person's name, set in Settings > Phone numbers, which your agent uses instead of the number. Agents never receive your phone number.

The tool contract of the BreakPass MCP server. Example values are illustrative; amounts in cents are placeholders (0), not prices. Not designed or guaranteed for emergencies or life-safety use.

## Connect

| Item | Value |
|---|---|
| Endpoint | `https://mcp.breakpass.ai/mcp` |
| Transport | HTTP only. No SSE. |
| MCP version | 2026-07-28 (stateless requests with `Mcp-Method` and `Mcp-Name` headers). Legacy clients are still accepted: 2025-06-18 and 2025-11-25, with `initialize` and a session. |
| Server info | Name `breakpass`, title "BreakPass". Tool list responses carry `ttlMs` 3,600,000. |
| Timestamps | ISO 8601 in UTC. The server sends milliseconds, for example 2026-10-03T19:00:00.000Z; the examples leave them out. |
| Tool results | Every tool returns `structuredContent` (with an `outputSchema`) and a one-line text summary. No phone number, payment detail or country appears in any tool output, error or log. |

Server instructions:

> BreakPass alerts the account owner, and people they invited who confirmed their number with a code, by WhatsApp, SMS or phone call. It can only reach those verified numbers, by the person's name; it can't call, text or message anyone else. Send only what the user asked to be told about. BreakPass reaches the user by WhatsApp, SMS or phone call. `get_setup_status` reports whether the account can send yet and what is left to set up. `list_workflows` returns the saved workflows and when each fits; a workflow_id is one of those. `start_workflow` runs a saved workflow when the user may need follow-up: Inform for no reply, Acknowledge when receipt matters, Escalate when the user must know or act before returning. `send_whatsapp`, `send_sms` and `make_call` send one message or place one call. `start_workflow` takes an idempotency_key. An acknowledgement means the user saw the alert; it is not approval to act. A recipient is a person's name from get_setup_status (owner means the user), not a phone number. Messages are for what the user asked to be told about. BreakPass sends alerts only to the account owner's verified numbers. It can't contact emergency services, and delivery isn't guaranteed, so it isn't designed for emergency or life-safety use.

## Authentication

| Item | Value |
|---|---|
| Protocol | OAuth 2.1 with PKCE. `S256` only. |
| Resource metadata (RFC 9728) | `GET /.well-known/oauth-protected-resource` and `…/mcp`. `resource` is `https://mcp.breakpass.ai/mcp`, `authorization_servers` is `https://mcp.breakpass.ai`. |
| Authorization server metadata (RFC 8414) | `GET /.well-known/oauth-authorization-server`. Issuer `https://mcp.breakpass.ai`. Endpoints `/authorize`, `/token` (also revocation) and `/register`. |
| Client registration | Client ID Metadata Documents (CIMD), or Dynamic Client Registration (deprecated, kept until summer 2027) at `/register`. No pre-registration. |
| Resource indicators (RFC 8707) | `resource` is required. Tokens are bound to `https://mcp.breakpass.ai/mcp`. Other audiences are rejected. |
| Issuer (RFC 9207) | `iss` on every authorization response, errors included. |
| Tokens | Access token 1 hour. Refresh token 90 days, rotating on use. |
| Unauthenticated | HTTP 401 with `WWW-Authenticate: Bearer resource_metadata="…", scope="alerts.read alerts.send"`. A tool outside the granted scopes gets HTTP 403 `insufficient_scope`. Authentication failures are never tool errors. |
| Sign-in and consent | `/authorize` opens BreakPass sign-in (Google or an email code). An email with no account gets "You need a BreakPass account"; no account is created here. Then a consent page: the app's name, with a tick when BreakPass recognises the platform; otherwise the name is marked "(name not verified)" with "We can't confirm this app's name." Then where access goes, the scopes, the account, and Allow or Deny. Signing in to BreakPass is separate from granting an agent access. A phone number is not needed to connect. |

### Scopes

| Scope | Tools | Notes |
|---|---|---|
| `alerts.read` | get_setup_status, list_workflows, get_alert_status | Read-only. Works from sign-in, before any payment. |
| `alerts.send` | send_whatsapp, send_sms, make_call, start_workflow, cancel_alert | Sends alerts and stops them. The owner can untick it on the consent page for a read-only connection. |

## Tools

### `get_setup_status`

Reports where the account is in setup: readiness states, blockers, the people on the account by name (never phone numbers) with their channels, balance and the next setup link. Call it first and follow its next steps. It sends nothing and costs nothing.

- Scope: `alerts.read`
- Annotations: `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true`, `openWorldHint: false`
- Cost: No cost. Read-only.

#### Input

No input.

#### Output

| Field | Type | Description |
|---|---|---|
| `account_state` (required) | string | Where the account is in setup. One of `signed_in`, `payment_needed`, `verification_needed`, `test_needed`, `ready`, `suspended`. An agent never sees `signed_in` or `test_needed`: a paid account with a verified number reads `ready`. |
| `readiness` (required) | object | Separate readiness states. Installing MCP or paying does not set them all. |
| `readiness.account_connected` (required) | boolean | An agent is connected. |
| `readiness.delivery_ready` (required) | boolean | `true` once the account can send (paid, with a verified number). |
| `readiness.agent_guidance_configured` (required) | boolean | Ticked by the owner in the BreakPass app. BreakPass cannot verify an agent's memory. |
| `onboarding_complete` (required) | boolean | `true` once setup is complete. For information only: `start_workflow` and the direct sends work as soon as the account can send (`delivery_ready`). |
| `blockers` (required) | array of object | What still blocks sending. |
| `blockers[].code` (required) | string | Blocker code. |
| `blockers[].message` (required) | string | Readable reason. |
| `blockers[].action_url` | string | Where the owner fixes it. Absent while payment is missing. |
| `recipients` (required) | array of object | The people on the account, one entry each, by the name your agent sends as `recipient`. Never phone numbers. |
| `recipients[].name` (required) | string | The person's name as saved in Settings > Phone numbers. The owner shows as the Name from Settings, else `Owner`; once another number is picked in Settings > Your number, the owner's own number keeps its name (or `Previous number`). Use it as `recipient` (any case). |
| `recipients[].is_owner` (required) | boolean | Whether this person is the owner: the number picked in Settings > Your number, by default the owner's own. |
| `recipients[].verified` (required) | boolean | Whether the person confirmed the number with a code. |
| `recipients[].channels` (required) | object | Channel capability for this person's number. |
| `recipients[].channels.whatsapp` (required) | string | WhatsApp capability. One of `yes`, `no`, `unknown`. |
| `recipients[].channels.sms` (required) | string | SMS capability. One of `available`, `coming_soon`, `unavailable`. |
| `recipients[].channels.call` (required) | string | Call capability. One of `available`, `unavailable`. |
| `recipients[].price_cents` (required) | object | Price per channel, in US cents: an object with `whatsapp`, `sms` and `call`, each an integer, or `null` where that channel isn't offered. |
| `recipients[].max_chars` (required) | object | The longest message each channel takes for this number, in characters: an object with `whatsapp`, `sms`, `sms_confirm_link` (an SMS that carries the confirmation link; `null` outside the US and Canada, where no SMS carries a link) and `call`, each an integer, or `null` where that channel isn't offered. SMS counts the +1 footer or the "BreakPass: " prefix. A reminder number ("Reminder 2 of 3: ") takes room on top. |
| `default_recipient` (required) | string or null | The name of the person an alert reaches when the agent names no recipient: the number picked in Settings > Your number (the owner, `is_owner` true), else the owner's own number. `null` before the account has an owner number. |
| `owner_name` (required) | string or null | The owner's name as saved in Settings, for a greeting, or `null`. Never a phone number. |
| `balance_cents` (required) | integer | US cents the owner can spend now. |
| `reserved_cents` (required) | integer | US cents held for alerts that are still running. |
| `spent_today_cents` (required) | integer | US cents spent today. |
| `workflows_version` (required) | integer | Version of the saved workflows. |
| `setup_url` | string | Where the owner finishes setup. Absent while payment is missing. |

#### Errors

`ACCOUNT_SUSPENDED` ("This BreakPass account is closed.") for a closed account. Authentication failures are HTTP 401 or 403, never tool errors.

#### Example

Request:

```json
{
  "name": "get_setup_status",
  "arguments": {}
}
```

Response (`structuredContent`):

```json
{
  "account_state": "ready",
  "readiness": {
    "account_connected": true,
    "delivery_ready": true,
    "agent_guidance_configured": false
  },
  "onboarding_complete": true,
  "blockers": [],
  "recipients": [
    {
      "name": "Sam",
      "is_owner": true,
      "verified": true,
      "channels": {
        "whatsapp": "yes",
        "sms": "coming_soon",
        "call": "available"
      },
      "price_cents": {
        "whatsapp": 0,
        "sms": null,
        "call": 0
      },
      "max_chars": {
        "whatsapp": 600,
        "sms": null,
        "sms_confirm_link": null,
        "call": 150
      }
    }
  ],
  "default_recipient": "Sam",
  "owner_name": "Sam",
  "balance_cents": 2400,
  "reserved_cents": 0,
  "spent_today_cents": 0,
  "workflows_version": 1,
  "setup_url": "https://app.breakpass.ai/start"
}
```

### `list_workflows`

Lists the saved alert workflows (Inform, Acknowledge, Escalate) with their ids, readable steps, limits and version. It reads the current configuration on every call, so changes the owner makes in the BreakPass app show up on the next read. It sends nothing and costs nothing.

- Scope: `alerts.read`
- Annotations: `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true`, `openWorldHint: false`
- Cost: No cost. Read-only.

#### Input

| Field | Type | Description |
|---|---|---|
| `recipient` | string | A person's name, so `max_cost_cents` is priced for that person's number. Default: the owner. |

#### Output

| Field | Type | Description |
|---|---|---|
| `version` (required) | integer | Version of the workflow set. |
| `updated_at` (required) | timestamp | When the set last changed. |
| `recipient` (required) | string | The name the costs are worked out for: `recipient` from the input, else the default recipient. |
| `workflows` (required) | array of object | The saved workflows. |
| `workflows[].workflow_id` (required) | string | Id to pass to `start_workflow`. One of `inform`, `acknowledge`, `escalate`. |
| `workflows[].name` (required) | string | Display name. |
| `workflows[].summary` (required) | string | One line on what the workflow does. |
| `workflows[].use_when` (required) | string | When to use this workflow. |
| `workflows[].version` (required) | integer | Version of this workflow. |
| `workflows[].steps` (required) | array of object | The readable steps, with the channel each uses for the recipient. |
| `workflows[].steps[].offset_min` (required) | integer | Minutes from the first alert, not from the previous step. |
| `workflows[].steps[].kind` (required) | string | Kind of step. One of `message`, `call`. |
| `workflows[].steps[].channel` (required) | string | The channel this step uses for the recipient. One of `whatsapp`, `sms`, `call`. |
| `workflows[].ack_required_default` (required) | boolean | Whether the workflow asks for acknowledgement (the same value as `requires_acknowledgement`). |
| `workflows[].requires_acknowledgement` (required) | boolean | Whether the workflow asks for acknowledgement, as the owner set it with the "Wait for acknowledgement" switch. |
| `workflows[].sms_fallback` (required) | boolean | Whether one SMS fallback is allowed after a confirmed WhatsApp delivery failure. It is false when the workflow's steps already fill `max_attempts`, because the fallback is an attempt of its own. |
| `workflows[].max_attempts` (required) | integer | Most attempts in one run, fallback included. |
| `workflows[].deadline_min` (required) | integer | Minutes after which the alert ends. |
| `workflows[].max_cost_cents` (required) | integer | Worst-case cost in US cents, for information. Reflects the holder in `recipient` when given. |
| `workflows[].available` (required) | boolean | Whether every step has a channel for the recipient. |
| `workflows[].editable_in` (required) | string | Where the owner edits this workflow (the BreakPass app). One of `portal`. |
| `decision_model` (required) | object or null | `null` unless the owner turned on the advanced decision matrix. |
| `decision_model.matrix_enabled` (required) | boolean | Whether the matrix is on. |
| `decision_model.urgency_threshold` (required) | integer | Score from which urgency counts as high. |
| `decision_model.importance_threshold` (required) | integer | Score from which importance counts as high. |

#### Errors

`RECIPIENT_NOT_FOUND`, `RECIPIENT_AMBIGUOUS`

#### Example

Request:

```json
{
  "name": "list_workflows",
  "arguments": {
    "recipient": "owner"
  }
}
```

Response (`structuredContent`):

```json
{
  "version": 1,
  "updated_at": "2026-10-03T09:00:00Z",
  "recipient": "owner",
  "workflows": [
    {
      "workflow_id": "inform",
      "name": "Inform",
      "summary": "Message at minute 0. No acknowledgement. One SMS if a WhatsApp message can't be delivered.",
      "use_when": "The user only needs to know. One message, no reply needed.",
      "version": 1,
      "steps": [
        {
          "offset_min": 0,
          "kind": "message",
          "channel": "whatsapp"
        }
      ],
      "ack_required_default": false,
      "requires_acknowledgement": false,
      "sms_fallback": true,
      "max_attempts": 2,
      "deadline_min": 75,
      "max_cost_cents": 0,
      "available": true,
      "editable_in": "portal"
    },
    {
      "workflow_id": "acknowledge",
      "name": "Acknowledge",
      "summary": "Messages at minutes 0, 10 and 30, then it stops; an acknowledgement stops it sooner. One SMS if a WhatsApp message can't be delivered.",
      "use_when": "It matters that the user saw it. Up to 3 messages, at 0, 10 and 30 minutes, until acknowledged.",
      "version": 1,
      "steps": [
        {
          "offset_min": 0,
          "kind": "message",
          "channel": "whatsapp"
        },
        {
          "offset_min": 10,
          "kind": "message",
          "channel": "whatsapp"
        },
        {
          "offset_min": 30,
          "kind": "message",
          "channel": "whatsapp"
        }
      ],
      "ack_required_default": true,
      "requires_acknowledgement": true,
      "sms_fallback": true,
      "max_attempts": 4,
      "deadline_min": 40,
      "max_cost_cents": 0,
      "available": true,
      "editable_in": "portal"
    },
    {
      "workflow_id": "escalate",
      "name": "Escalate",
      "summary": "Message at minute 0; calls at minutes 0, 1 and 2, then it stops; an acknowledgement stops it sooner. One SMS if a WhatsApp message can't be delivered.",
      "use_when": "The user must know or act before returning. A message and a call at once, then calls at 1 and 2 minutes; 3 calls in all.",
      "version": 1,
      "steps": [
        {
          "offset_min": 0,
          "kind": "message",
          "channel": "whatsapp"
        },
        {
          "offset_min": 0,
          "kind": "call",
          "channel": "call"
        },
        {
          "offset_min": 1,
          "kind": "call",
          "channel": "call"
        },
        {
          "offset_min": 2,
          "kind": "call",
          "channel": "call"
        }
      ],
      "ack_required_default": true,
      "requires_acknowledgement": true,
      "sms_fallback": true,
      "max_attempts": 5,
      "deadline_min": 12,
      "max_cost_cents": 0,
      "available": true,
      "editable_in": "portal"
    }
  ],
  "decision_model": null
}
```

### `send_whatsapp`

Sends one WhatsApp message to a verified number. This spends the owner's balance and contacts a real person. One attempt: no retries and no follow-up. For repeated attempts use `start_workflow`.

- Scope: `alerts.send`
- Annotations: `readOnlyHint: false`, `destructiveHint: true`, `idempotentHint: false`, `openWorldHint: true`
- Cost: The cost is reserved when you call and charged when the message is delivered. A message that is not delivered is released. The price per alert for each country and channel is shown in the owner's BreakPass account. Amounts are in US cents (USD).

#### Input

| Field | Type | Description |
|---|---|---|
| `message` (required) | string | One line of plain text (line breaks become spaces or full stops; markdown, HTML and invisible characters are removed), 600 characters at most. At most 150 characters when the alert includes a call (make_call, or a workflow with calls such as escalate), because a call is cut off 55 seconds after it is answered; under 150 reads well on every channel. An SMS is one part and is not cut. To a US or Canadian (+1) number it ends with a blank line and "This is an automated BreakPass alert. Reply STOP to opt out.", which leaves 98 characters for the message (8 when it has a character outside the SMS alphabet, such as ł). Where the SMS sender shows as a number (AR, BE, CL, CO, IN, MX) it starts with "BreakPass: ": 149 characters (59). To other countries there is no footer and no prefix: 160 characters (70). To a US or Canadian number a confirmation link (`require_ack`, or a workflow that asks for acknowledgement) takes 37 characters; a reminder number ("Reminder 2 of 3: ") takes room too; `get_setup_status` gives each number's room per channel (`max_chars`). An alert that goes by SMS with a longer message is refused with the number of characters to remove, and a WhatsApp alert's SMS fallback is not sent when the text does not fit one SMS. At most one link, a full https address, no link shorteners and no wa.me links. |
| `recipient` | string | A person's name from `recipients` in `get_setup_status`, as saved in Settings > Phone numbers (any case, at most 30 characters); `owner` is the owner: the number picked in Settings > Your number, by default the owner's own. Default: the owner. No other destination parameter exists: no tool accepts a phone number. |
| `agent_name` | string | The sender name the person sees (a call says "with an alert from" this name), 1 to 30 characters. Default: the connected app's name. |
| `use_case` | string | Optional label, 40 characters at most, lowercase `a-z`, `0-9`, space and `-`. |
| `event_reference` | string | Optional. Your own id for the event, 100 characters at most. |
| `idempotency_key` | string | Optional, 64 characters at most. Same key and same payload within 24 hours returns the original alert with `idempotent_replay: true`. Same key with a different payload returns `IDEMPOTENCY_CONFLICT`. |
| `require_ack` | boolean | Default `false`. When `true`, the message carries the Acknowledge button. The alert then stays open for an acknowledgement window of 60 minutes from when it is accepted (`deadline_at`, initial value) and ends as `expired` if nobody acknowledges. It never retries. Acknowledging confirms receipt. It never approves anything. |

#### Output

| Field | Type | Description |
|---|---|---|
| `alert_id` (required) | string | Public alert id, for example `A-7K3Q9X`. |
| `status` (required) | string | Always `queued`. Without an `idempotency_key`, the same recipient, channel and message within 60 seconds returns the original alert with `idempotent_replay: true`; nothing is charged. One of `queued`. |
| `channel` (required) | string | The channel used. One of `whatsapp`, `sms`, `call`. |
| `recipient` (required) | string | The person's name. |
| `estimated_cost_cents` (required) | integer | What this send is expected to cost, in US cents (0 for a free setup test). The final amount is `charged_cents` in `get_alert_status`. |
| `require_ack` (required) | boolean | Whether acknowledgement was requested. |
| `deadline_at` (required) | timestamp or null | When the acknowledgement window ends, or `null` when no acknowledgement was requested. |
| `idempotent_replay` (required) | boolean | `true` when this response repeats an earlier request: the same `idempotency_key`, or the same send within 60 seconds. |
| `notice` | string | Present once the account has 5 failed sends today (while that rule is switched on). It reads: "From now on today, texts that carriers block as spam are charged at the normal price." |

#### Errors

`SETUP_INCOMPLETE`, `INSUFFICIENT_BALANCE`, `RATE_LIMITED`, `RECIPIENT_NOT_FOUND`, `RECIPIENT_AMBIGUOUS`, `RECIPIENT_NOT_VERIFIED`, `RECIPIENT_OPTED_OUT`, `CHANNEL_UNAVAILABLE`, `CONTENT_REJECTED`, `IDEMPOTENCY_CONFLICT`, `ACCOUNT_SUSPENDED`, `PROVIDER_ERROR`, `SERVICE_PAUSED`, `TEMPLATE_MISMATCH`

#### Example

Request:

```json
{
  "name": "send_whatsapp",
  "arguments": {
    "message": "Checkout is failing.",
    "recipient": "owner",
    "agent_name": "Checkout monitor",
    "use_case": "checkout",
    "require_ack": false,
    "idempotency_key": "checkout-failing-2026-10-03-1"
  }
}
```

Response (`structuredContent`):

```json
{
  "alert_id": "A-7K3Q9X",
  "status": "queued",
  "channel": "whatsapp",
  "recipient": "owner",
  "estimated_cost_cents": 0,
  "require_ack": false,
  "deadline_at": null,
  "idempotent_replay": false
}
```

### `send_sms`

Sends one SMS to a verified number. This spends the owner's balance and contacts a real person. One attempt: no retries and no follow-up. The text is never cut: a message that does not fit one SMS is refused (see `message`). Outside the US and Canada an SMS carries no link: a message with a link is refused there ("Carriers outside the US and Canada block texts with links. Send it by WhatsApp, or remove the link."). SMS is not offered in every country; where it is not, the tool returns `CHANNEL_UNAVAILABLE`.

- Scope: `alerts.send`
- Annotations: `readOnlyHint: false`, `destructiveHint: true`, `idempotentHint: false`, `openWorldHint: true`
- Cost: The cost is reserved when you call and charged when the SMS is delivered, or when the carrier accepts it without a receipt. It is released if sending fails. The price per alert for each country and channel is shown in the owner's BreakPass account. Amounts are in US cents (USD).

#### Input

| Field | Type | Description |
|---|---|---|
| `message` (required) | string | One line of plain text (line breaks become spaces or full stops; markdown, HTML and invisible characters are removed), 600 characters at most. At most 150 characters when the alert includes a call (make_call, or a workflow with calls such as escalate), because a call is cut off 55 seconds after it is answered; under 150 reads well on every channel. An SMS is one part and is not cut. To a US or Canadian (+1) number it ends with a blank line and "This is an automated BreakPass alert. Reply STOP to opt out.", which leaves 98 characters for the message (8 when it has a character outside the SMS alphabet, such as ł). Where the SMS sender shows as a number (AR, BE, CL, CO, IN, MX) it starts with "BreakPass: ": 149 characters (59). To other countries there is no footer and no prefix: 160 characters (70). To a US or Canadian number a confirmation link (`require_ack`, or a workflow that asks for acknowledgement) takes 37 characters; a reminder number ("Reminder 2 of 3: ") takes room too; `get_setup_status` gives each number's room per channel (`max_chars`). An alert that goes by SMS with a longer message is refused with the number of characters to remove, and a WhatsApp alert's SMS fallback is not sent when the text does not fit one SMS. At most one link, a full https address, no link shorteners and no wa.me links. |
| `recipient` | string | A person's name from `recipients` in `get_setup_status`, as saved in Settings > Phone numbers (any case, at most 30 characters); `owner` is the owner: the number picked in Settings > Your number, by default the owner's own. Default: the owner. No other destination parameter exists: no tool accepts a phone number. |
| `agent_name` | string | The sender name the person sees (a call says "with an alert from" this name), 1 to 30 characters. Default: the connected app's name. |
| `use_case` | string | Optional label, 40 characters at most, lowercase `a-z`, `0-9`, space and `-`. |
| `event_reference` | string | Optional. Your own id for the event, 100 characters at most. |
| `idempotency_key` | string | Optional, 64 characters at most. Same key and same payload within 24 hours returns the original alert with `idempotent_replay: true`. Same key with a different payload returns `IDEMPOTENCY_CONFLICT`. |
| `require_ack` | boolean | Default `false`. When `true`, the SMS carries a link to a page where the holder confirms (US and Canadian numbers only). Opening the link changes nothing; the holder presses a button on the page. Outside the US and Canada an SMS carries no link, so `send_sms` with `require_ack` is refused there (`CHANNEL_UNAVAILABLE`: "An SMS can be acknowledged only on a US or Canadian number. Send it without acknowledgement, or by WhatsApp or a call."). The alert then stays open for an acknowledgement window of 60 minutes from when it is accepted (`deadline_at`, initial value) and ends as `expired` if nobody acknowledges. It never retries. Acknowledging confirms receipt. It never approves anything. |

#### Output

| Field | Type | Description |
|---|---|---|
| `alert_id` (required) | string | Public alert id, for example `A-7K3Q9X`. |
| `status` (required) | string | Always `queued`. Without an `idempotency_key`, the same recipient, channel and message within 60 seconds returns the original alert with `idempotent_replay: true`; nothing is charged. One of `queued`. |
| `channel` (required) | string | The channel used. One of `whatsapp`, `sms`, `call`. |
| `recipient` (required) | string | The person's name. |
| `estimated_cost_cents` (required) | integer | What this send is expected to cost, in US cents (0 for a free setup test). The final amount is `charged_cents` in `get_alert_status`. |
| `require_ack` (required) | boolean | Whether acknowledgement was requested. |
| `deadline_at` (required) | timestamp or null | When the acknowledgement window ends, or `null` when no acknowledgement was requested. |
| `idempotent_replay` (required) | boolean | `true` when this response repeats an earlier request: the same `idempotency_key`, or the same send within 60 seconds. |
| `notice` | string | Present once the account has 5 failed sends today (while that rule is switched on). It reads: "From now on today, texts that carriers block as spam are charged at the normal price." |

#### Errors

`SETUP_INCOMPLETE`, `INSUFFICIENT_BALANCE`, `RATE_LIMITED`, `RECIPIENT_NOT_FOUND`, `RECIPIENT_AMBIGUOUS`, `RECIPIENT_NOT_VERIFIED`, `RECIPIENT_OPTED_OUT`, `CHANNEL_UNAVAILABLE`, `CONTENT_REJECTED`, `IDEMPOTENCY_CONFLICT`, `ACCOUNT_SUSPENDED`, `PROVIDER_ERROR`, `SERVICE_PAUSED`

#### Example

Request:

```json
{
  "name": "send_sms",
  "arguments": {
    "message": "A refund over $200 waits for your OK.",
    "recipient": "owner",
    "use_case": "refunds",
    "require_ack": true
  }
}
```

Response (`structuredContent`):

```json
{
  "alert_id": "A-4M8TQ2",
  "status": "queued",
  "channel": "sms",
  "recipient": "owner",
  "estimated_cost_cents": 0,
  "require_ack": true,
  "deadline_at": "2026-10-03T20:05:00Z",
  "idempotent_replay": false
}
```

### `make_call`

Phones a verified number and reads the message out loud (at most 150 characters; a call is cut off 55 seconds after it is answered). This spends the owner's balance and contacts a real person. One attempt: no redial and no follow-up. Machine detection is off, so a voicemail that picks up hears the message and the call counts as answered (charged as answered). For repeated calls use `start_workflow`.

- Scope: `alerts.send`
- Annotations: `readOnlyHint: false`, `destructiveHint: true`, `idempotentHint: false`, `openWorldHint: true`
- Cost: The cost is reserved when you call and charged by the outcome of the call (Terms, "When a charge applies"): a call answered by a person or voicemail costs the price shown, and a call that isn't picked up costs a reduced fee. The rest is released. The price for each country and channel is shown in the owner's BreakPass account. Amounts are in US cents (USD).

#### Input

| Field | Type | Description |
|---|---|---|
| `message` (required) | string | One line of plain text (line breaks become spaces or full stops; markdown, HTML and invisible characters are removed), 600 characters at most. At most 150 characters when the alert includes a call (make_call, or a workflow with calls such as escalate), because a call is cut off 55 seconds after it is answered; under 150 reads well on every channel. An SMS is one part and is not cut. To a US or Canadian (+1) number it ends with a blank line and "This is an automated BreakPass alert. Reply STOP to opt out.", which leaves 98 characters for the message (8 when it has a character outside the SMS alphabet, such as ł). Where the SMS sender shows as a number (AR, BE, CL, CO, IN, MX) it starts with "BreakPass: ": 149 characters (59). To other countries there is no footer and no prefix: 160 characters (70). To a US or Canadian number a confirmation link (`require_ack`, or a workflow that asks for acknowledgement) takes 37 characters; a reminder number ("Reminder 2 of 3: ") takes room too; `get_setup_status` gives each number's room per channel (`max_chars`). An alert that goes by SMS with a longer message is refused with the number of characters to remove, and a WhatsApp alert's SMS fallback is not sent when the text does not fit one SMS. At most one link, a full https address, no link shorteners and no wa.me links. |
| `recipient` | string | A person's name from `recipients` in `get_setup_status`, as saved in Settings > Phone numbers (any case, at most 30 characters); `owner` is the owner: the number picked in Settings > Your number, by default the owner's own. Default: the owner. No other destination parameter exists: no tool accepts a phone number. |
| `agent_name` | string | The sender name the person sees (a call says "with an alert from" this name), 1 to 30 characters. Default: the connected app's name. |
| `use_case` | string | Optional label, 40 characters at most, lowercase `a-z`, `0-9`, space and `-`. |
| `event_reference` | string | Optional. Your own id for the event, 100 characters at most. |
| `idempotency_key` | string | Optional, 64 characters at most. Same key and same payload within 24 hours returns the original alert with `idempotent_replay: true`. Same key with a different payload returns `IDEMPOTENCY_CONFLICT`. |
| `require_ack` | boolean | Default `false`. When `true`, the call says "Press 1 to acknowledge." after the message; pressing 1 at any point in the call acknowledges. The alert then stays open for an acknowledgement window of 60 minutes from when it is accepted (`deadline_at`, initial value) and ends as `expired` if nobody acknowledges. It never retries. Acknowledging confirms receipt. It never approves anything. |

#### Output

| Field | Type | Description |
|---|---|---|
| `alert_id` (required) | string | Public alert id, for example `A-7K3Q9X`. |
| `status` (required) | string | Always `queued`. Without an `idempotency_key`, the same recipient, channel and message within 60 seconds returns the original alert with `idempotent_replay: true`; nothing is charged. One of `queued`. |
| `channel` (required) | string | The channel used. One of `whatsapp`, `sms`, `call`. |
| `recipient` (required) | string | The person's name. |
| `estimated_cost_cents` (required) | integer | What this send is expected to cost, in US cents (0 for a free setup test). The final amount is `charged_cents` in `get_alert_status`. |
| `require_ack` (required) | boolean | Whether acknowledgement was requested. |
| `deadline_at` (required) | timestamp or null | When the acknowledgement window ends, or `null` when no acknowledgement was requested. |
| `idempotent_replay` (required) | boolean | `true` when this response repeats an earlier request: the same `idempotency_key`, or the same send within 60 seconds. |
| `notice` | string | Present once the account has 5 failed sends today (while that rule is switched on). It reads: "From now on today, texts that carriers block as spam are charged at the normal price." |

#### Errors

`SETUP_INCOMPLETE`, `INSUFFICIENT_BALANCE`, `RATE_LIMITED`, `RECIPIENT_NOT_FOUND`, `RECIPIENT_AMBIGUOUS`, `RECIPIENT_NOT_VERIFIED`, `RECIPIENT_OPTED_OUT`, `CHANNEL_UNAVAILABLE`, `CONTENT_REJECTED`, `IDEMPOTENCY_CONFLICT`, `ACCOUNT_SUSPENDED`, `PROVIDER_ERROR`, `SERVICE_PAUSED`

#### Example

Request:

```json
{
  "name": "make_call",
  "arguments": {
    "message": "Checkout is failing.",
    "recipient": "owner",
    "require_ack": true,
    "idempotency_key": "checkout-failing-call-1"
  }
}
```

Response (`structuredContent`):

```json
{
  "alert_id": "A-2R6WZ5",
  "status": "queued",
  "channel": "call",
  "recipient": "owner",
  "estimated_cost_cents": 0,
  "require_ack": true,
  "deadline_at": "2026-10-03T20:05:00Z",
  "idempotent_replay": false
}
```

### `start_workflow`

Starts a saved workflow on BreakPass servers. The timers run on our side until the owner acknowledges or a stop rule applies, so the alert keeps going if your agent restarts. This spends the owner's balance and contacts a real person. Retries are always limited. The alert snapshots the workflow version: edits affect new alerts only. Poll `get_alert_status`; do not call again.

- Scope: `alerts.send`
- Annotations: `readOnlyHint: false`, `destructiveHint: true`, `idempotentHint: true`, `openWorldHint: true`
- Cost: The balance is held one step at a time: the first step when you call, each later step when it's due, up to the worst case (`max_cost_cents`, for information). A step the balance can't cover isn't sent, and the alert ends `failed` with `INSUFFICIENT_BALANCE`. Each attempt is charged on its own: a message when it is delivered, a call by its outcome (Terms, "When a charge applies"). Everything else is released when the alert ends or the owner acknowledges. The price per alert for each country and channel is shown in the owner's BreakPass account. Amounts are in US cents (USD).

#### Input

| Field | Type | Description |
|---|---|---|
| `workflow_id` (required) | string | One of the ids `list_workflows` returns (today `inform`, `acknowledge`, `escalate`); an unknown id returns `WORKFLOW_NOT_FOUND` with the valid ids in `details.available`. |
| `message` (required) | string | One line of plain text (line breaks become spaces or full stops; markdown, HTML and invisible characters are removed), 600 characters at most. At most 150 characters when the alert includes a call (make_call, or a workflow with calls such as escalate), because a call is cut off 55 seconds after it is answered; under 150 reads well on every channel. An SMS is one part and is not cut. To a US or Canadian (+1) number it ends with a blank line and "This is an automated BreakPass alert. Reply STOP to opt out.", which leaves 98 characters for the message (8 when it has a character outside the SMS alphabet, such as ł). Where the SMS sender shows as a number (AR, BE, CL, CO, IN, MX) it starts with "BreakPass: ": 149 characters (59). To other countries there is no footer and no prefix: 160 characters (70). To a US or Canadian number a confirmation link (`require_ack`, or a workflow that asks for acknowledgement) takes 37 characters; a reminder number ("Reminder 2 of 3: ") takes room too; `get_setup_status` gives each number's room per channel (`max_chars`). An alert that goes by SMS with a longer message is refused with the number of characters to remove, and a WhatsApp alert's SMS fallback is not sent when the text does not fit one SMS. At most one link, a full https address, no link shorteners and no wa.me links. |
| `recipient` | string | A person's name from `recipients` in `get_setup_status`, as saved in Settings > Phone numbers (any case, at most 30 characters); `owner` is the owner: the number picked in Settings > Your number, by default the owner's own. Default: the owner. No other destination parameter exists: no tool accepts a phone number. |
| `agent_name` | string | The sender name the person sees (a call says "with an alert from" this name), 1 to 30 characters. Default: the connected app's name. |
| `use_case` | string | Optional label, 40 characters at most, lowercase `a-z`, `0-9`, space and `-`. |
| `event_reference` | string | Optional. Your own id for the event, 100 characters at most. |
| `idempotency_key` (required) | string | Required, 64 characters at most. Same key and same payload within 24 hours returns the original alert with `idempotent_replay: true`: no new send and no charge. Same key with a different payload returns `IDEMPOTENCY_CONFLICT`. |

#### Output

| Field | Type | Description |
|---|---|---|
| `alert_id` (required) | string | Public alert id. |
| `status` (required) | string | Always `queued`. One of `queued`. |
| `workflow` (required) | object | The workflow that was started. |
| `workflow.id` (required) | string | Workflow id. One of `inform`, `acknowledge`, `escalate`. |
| `workflow.name` (required) | string | Workflow name. |
| `workflow.version` (required) | integer | Version snapshotted into this alert. |
| `steps` (required) | array of object | The steps as they run for this recipient. |
| `steps[].offset_min` (required) | integer | Minutes from the first alert, not from the previous step. |
| `steps[].kind` (required) | string | Kind of step. One of `message`, `call`. |
| `steps[].channel` (required) | string | The channel this step uses for the recipient. One of `whatsapp`, `sms`, `call`. |
| `recipient` (required) | string | The person's name. |
| `require_ack` (required) | boolean | Whether acknowledgement is on for this workflow. |
| `requires_acknowledgement` (required) | boolean | Whether this alert asks for acknowledgement: the workflow's setting when it started. |
| `max_cost_cents` (required) | integer | The most this alert can cost, in US cents, for information. |
| `deadline_at` (required) | timestamp or null | When the alert ends if nothing else stops it. |
| `idempotent_replay` (required) | boolean | `true` when this response repeats an earlier request with the same `idempotency_key`. |
| `next_step` (required) | string | Guidance for the agent: "Poll `get_alert_status`; do not call `start_workflow` again." |
| `notice` | string | Present once the account has 5 failed sends today (while that rule is switched on). It reads: "From now on today, texts that carriers block as spam are charged at the normal price." |

#### Errors

`SETUP_INCOMPLETE`, `INSUFFICIENT_BALANCE`, `RATE_LIMITED`, `RECIPIENT_NOT_FOUND`, `RECIPIENT_AMBIGUOUS`, `RECIPIENT_NOT_VERIFIED`, `RECIPIENT_OPTED_OUT`, `CHANNEL_UNAVAILABLE`, `CONTENT_REJECTED`, `IDEMPOTENCY_CONFLICT`, `ACCOUNT_SUSPENDED`, `PROVIDER_ERROR`, `SERVICE_PAUSED`, `WORKFLOW_NOT_FOUND`, `TEMPLATE_MISMATCH`

#### Example

Request:

```json
{
  "name": "start_workflow",
  "arguments": {
    "workflow_id": "escalate",
    "message": "Checkout is failing.",
    "recipient": "owner",
    "agent_name": "Checkout monitor",
    "use_case": "checkout",
    "idempotency_key": "checkout-failing-2026-10-03T19-00"
  }
}
```

Response (`structuredContent`):

```json
{
  "alert_id": "A-9D5HX1",
  "status": "queued",
  "workflow": {
    "id": "escalate",
    "name": "Escalate",
    "version": 1
  },
  "steps": [
    {
      "offset_min": 0,
      "kind": "message",
      "channel": "whatsapp"
    },
    {
      "offset_min": 0,
      "kind": "call",
      "channel": "call"
    },
    {
      "offset_min": 1,
      "kind": "call",
      "channel": "call"
    },
    {
      "offset_min": 2,
      "kind": "call",
      "channel": "call"
    }
  ],
  "recipient": "owner",
  "require_ack": true,
  "requires_acknowledgement": true,
  "max_cost_cents": 0,
  "deadline_at": "2026-10-03T19:12:00Z",
  "idempotent_replay": false,
  "next_step": "Poll get_alert_status; do not call start_workflow again."
}
```

### `get_alert_status`

Returns the status of alerts you started: delivered, acknowledged, failed, expired or cancelled, with every attempt. Delivered is not acknowledged, and silence is not a confirmed failure. It sends nothing and costs nothing.

- Scope: `alerts.read`
- Annotations: `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true`, `openWorldHint: false`
- Cost: No cost. Read-only.

#### Input

| Field | Type | Description |
|---|---|---|
| `alert_ids` | array of string | 1 to 20 alert ids (`A-XXXXXX`) returned by the send tools or `start_workflow`. With neither `alert_ids` nor `event_reference`, the open alerts are listed. |
| `event_reference` | string | The `event_reference` sent with the alert: returns the alerts that carry it, newest first (at most 20). It takes the place of `alert_ids`. |

#### Output

| Field | Type | Description |
|---|---|---|
| `alerts` (required) | array of object | One entry per alert found, under the key `alerts`. |
| `alerts[].alert_id` (required) | string | Public alert id. |
| `alerts[].kind` (required) | string | Direct send or workflow run. One of `direct`, `workflow`. |
| `alerts[].workflow` (required) | object or null | The workflow this alert runs, or `null` for a direct send. |
| `alerts[].workflow.id` (required) | string | Workflow id. One of `inform`, `acknowledge`, `escalate`. |
| `alerts[].workflow.version` (required) | integer | Workflow version snapshotted when the alert started. |
| `alerts[].recipient` (required) | string | The person's name. |
| `alerts[].status` (required) | string | See the status values below. One of `queued`, `sent`, `delivered`, `acknowledged`, `failed`, `expired`, `cancelled`. |
| `alerts[].terminal` (required) | boolean | `true` when the alert has ended. |
| `alerts[].awaiting_ack` (required) | boolean | `true` while delivered and waiting for an acknowledgement. |
| `alerts[].requires_acknowledgement` (required) | boolean | Whether the alert asks for acknowledgement: its workflow's setting when it started, or `require_ack`. |
| `alerts[].outcome` (required) | string | What the alert reads as, the same as in the BreakPass app. One of `queued`, `delivering`, `waiting_for_ack`, `acknowledged`, `answered`, `delivered`, `not_acknowledged`, `not_answered`, `failed`, `cancelled`. |
| `alerts[].outcome_text` (required) | string | The words for `outcome`, for example "Waiting for you to acknowledge". |
| `alerts[].created_at` (required) | timestamp | When the alert was created. |
| `alerts[].deadline_at` (required) | timestamp or null | When the alert ends if nothing else stops it. |
| `alerts[].next_attempt_at` (required) | timestamp or null | When the next attempt is due. |
| `alerts[].acknowledged_at` (required) | timestamp or null | When the holder acknowledged. |
| `alerts[].ack_via` (required) | string or null | How the holder acknowledged. One of `whatsapp_button`, `dtmf`, `sms_link`, `portal`. |
| `alerts[].late_ack_at` (required) | timestamp or null | An acknowledgement that arrived after the alert had expired. The status does not change. |
| `alerts[].cancelled_at` (required) | timestamp or null | When the alert was cancelled. |
| `alerts[].cancelled_by` (required) | string or null | Who cancelled. One of `agent`, `owner`, `system`. |
| `alerts[].failure` (required) | object or null | Why the alert failed, or `null`. |
| `alerts[].failure.code` (required) | string | Failure code. |
| `alerts[].failure.reason` (required) | string or null | The reason as the BreakPass app shows it, `R01` to `R15`, or `null` when there is none. |
| `alerts[].failure.message` (required) | string | Readable reason. Never a number, a code or alert text. |
| `alerts[].charged_cents` (required) | integer | US cents charged so far. |
| `alerts[].attempts` (required) | array of object | Every attempt, in order. |
| `alerts[].attempts[].seq` (required) | integer | Attempt number within the alert. |
| `alerts[].attempts[].offset_min` (required) | integer | Minutes from the first alert. |
| `alerts[].attempts[].kind` (required) | string | Kind of attempt. One of `message`, `call`, `fallback`. |
| `alerts[].attempts[].channel` (required) | string | Channel of the attempt. One of `whatsapp`, `sms`, `call`. |
| `alerts[].attempts[].status` (required) | string | Attempt status. One of `scheduled`, `sending`, `sent`, `delivered`, `delivered_unconfirmed`, `read`, `ringing`, `answered`, `voicemail`, `no_answer`, `busy`, `failed`, `skipped`. |
| `alerts[].attempts[].scheduled_at` (required) | timestamp or null | When the attempt is due. |
| `alerts[].attempts[].sent_at` (required) | timestamp or null | When it was sent or the call was placed. |
| `alerts[].attempts[].delivered_at` (required) | timestamp or null | When delivery was confirmed. |
| `alerts[].attempts[].failure_code` (required) | string or null | Set when the attempt failed or was skipped. |
| `alerts[].attempts[].failure_reason` (required) | string or null | The reason as the BreakPass app shows it, `R01` to `R15`. |
| `alerts[].attempts[].failure_message` (required) | string or null | Readable reason for a failed or skipped attempt. |
| `alerts[].attempts[].outcome` (required) | string or null | `not_picked_up` for a call nobody picked up (charged the reduced fee, not an error); otherwise `null`. One of `not_picked_up`. |
| `alerts[].attempts[].charged_cents` (required) | integer | US cents charged for this attempt. |
| `not_found` (required) | array of string | Ids that weren't found on this account. |

Status values:

| Status | Meaning |
|---|---|
| `queued` | Accepted. Nothing sent yet. |
| `sent` | Something was sent and the alert is still running (the app shows it as "Delivering"). |
| `delivered` | At least one attempt was delivered. Final for Inform and for direct sends without `require_ack`. Otherwise see `awaiting_ack`. |
| `acknowledged` | The holder confirmed receipt. Pending steps are cancelled and unused balance is released. |
| `failed` | The alert ended without a delivery, or a step was blocked (for example a step the balance couldn't cover). |
| `expired` | The deadline passed without an acknowledgement. |
| `cancelled` | Stopped by `cancel_alert`, the owner or the system. |

#### Errors

`ALERT_NOT_FOUND` (details: `not_found`), `CONTENT_REJECTED` (for example "Send alert_ids or event_reference, not both.")

#### Example

Request:

```json
{
  "name": "get_alert_status",
  "arguments": {
    "alert_ids": [
      "A-9D5HX1"
    ]
  }
}
```

Response (`structuredContent`):

```json
{
  "alerts": [
    {
      "alert_id": "A-9D5HX1",
      "kind": "workflow",
      "workflow": {
        "id": "escalate",
        "version": 1
      },
      "recipient": "owner",
      "status": "acknowledged",
      "terminal": true,
      "awaiting_ack": false,
      "requires_acknowledgement": true,
      "outcome": "acknowledged",
      "outcome_text": "Acknowledged",
      "created_at": "2026-10-03T19:00:00Z",
      "deadline_at": "2026-10-03T19:12:00Z",
      "acknowledged_at": "2026-10-03T19:00:40Z",
      "ack_via": "dtmf",
      "charged_cents": 0,
      "attempts": [
        {
          "seq": 1,
          "offset_min": 0,
          "kind": "message",
          "channel": "whatsapp",
          "status": "delivered",
          "charged_cents": 0,
          "outcome": null
        },
        {
          "seq": 2,
          "offset_min": 0,
          "kind": "call",
          "channel": "call",
          "status": "answered",
          "charged_cents": 0,
          "outcome": null
        }
      ]
    }
  ],
  "not_found": []
}
```

### `cancel_alert`

Stops all unsent steps of an alert and releases the unused reservation. Calling it again is safe. An attempt already in flight, such as a call that is ringing, may finish.

- Scope: `alerts.send`
- Annotations: `readOnlyHint: false`, `destructiveHint: true`, `idempotentHint: true`, `openWorldHint: true`
- Cost: No new charge. Balance not yet captured is released. Attempts that already connected or were delivered stay charged.

#### Input

| Field | Type | Description |
|---|---|---|
| `alert_id` (required) | string | The alert id (`A-XXXXXX`) to cancel. |

#### Output

The final status of the alert, with the fields of one `get_alert_status` entry, not wrapped in a key.

#### Errors

`ALERT_NOT_FOUND`, `CONTENT_REJECTED`

#### Example

Request:

```json
{
  "name": "cancel_alert",
  "arguments": {
    "alert_id": "A-5F2VC8"
  }
}
```

Response (`structuredContent`):

```json
{
  "alert_id": "A-5F2VC8",
  "status": "cancelled",
  "terminal": true,
  "cancelled_at": "2026-10-03T19:02:10Z",
  "cancelled_by": "agent"
}
```

## Workflows

|  | Inform (`inform`) | Acknowledge (`acknowledge`) | Escalate (`escalate`) |
|---|---|---|---|
| Steps | Message at minute 0. | Message at minutes 0, 10 and 30, if still unacknowledged. | A WhatsApp message and a call at minute 0; calls at minutes 1 and 2, if still unacknowledged. |
| Acknowledgement | Off by default; the owner can switch it on. | On by default; the owner can switch it off. | On by default; the owner can switch it off. |
| SMS fallback | One, only after a confirmed WhatsApp delivery failure. | Same. | Same (for the minute-0 message). |
| Max attempts, fallback included | 2 | 4 | 5 |
| Stops on | Delivery, or a bounded delivery failure. | Acknowledgement, cancellation, an empty balance, or expiry after the last attempt. | Same. |
| Deadline (initial) | 75 min | 40 min (last attempt + 10 min grace). | 12 min (last attempt + 10 min grace). |

## Delivery and acknowledgement

Delivered means a provider confirmed the message reached the phone, or the call was answered (machine detection is off, so a voicemail that picks up counts as answered). It does not mean seen or acknowledged.

Acknowledged means the holder explicitly confirmed receipt. It confirms receipt only. It never approves an agent action or a purchase, and it does not mean an issue is resolved.
**What a call says.** Every call opens with "This is BreakPass by X Labs, with an alert from {agent_name}.", then the message (and "Press 1 to acknowledge." when acknowledgement is asked), a 3-second pause, then "To stop all BreakPass alerts to this number, press 9." On +1 numbers it adds "To reach us, call 3 3 2, 2 8 7, 9 2 0 0." (+1 332 287 9200). Key 9 counts only from that sentence to the end of the call; it stops all BreakPass alerts to the number from every account, and the call says "You will receive no more BreakPass alerts. Goodbye." Otherwise the call ends with "Goodbye." or "Acknowledged. Goodbye."

## Idempotency and duplicates

- Same `idempotency_key` and same payload within 24 hours: the original alert is returned with `idempotent_replay: true`. No new send, no charge.
- Same key and a different payload: `IDEMPOTENCY_CONFLICT`.
- No key on a direct tool: the same recipient, channel and message within 60 seconds returns the original alert with `idempotent_replay: true`. Nothing is charged.
- `start_workflow` requires a key. Send one every time.

## Limits

Initial defaults. They can change.

| Scope | Limit |
|---|---|
| Account | Until a payment is confirmed, an account can send only setup tests, within these limits: 10 alerts a minute, 100 an hour, 500 a day, and at most 30 alerts and 6 calls a day. Paid plans: no account-wide count cap; the unbilled-sends budget applies to every account. |
| One holder (recipient) | 20 alerts an hour. |
| Calls to one holder | 1 at a time, 10 an hour (support can raise it for an account). Free setup-test calls don't count. |
| Runaway brake | Accounts without a paid plan: a connection that hits a rate cap 3 times in 10 minutes is paused for 15 minutes. On a paid plan the per-number limits never pause the connection. |
| Content | `message` 600 (150 when the alert includes a call; one SMS part when it goes by SMS), `recipient` 30, `agent_name` 30, `use_case` 40, `event_reference` 100, `idempotency_key` 64, `workflow_id` 40 and alert ids 32 characters; `alert_ids` 1 to 20. Control characters are stripped. |

## Errors

A tool error returns `isError: true` and `structuredContent.error` with `code`, `message`, optional `retry_after_s` and optional `details`.

```json
{
  "isError": true,
  "structuredContent": {
    "error": {
      "code": "RATE_LIMITED",
      "message": "Too many alerts to this recipient in the last hour (the limit is 20 an hour).",
      "retry_after_s": 1200,
      "details": { "reason": "holder_per_hour" }
    }
  }
}
```

| Code | When | Retry? |
|---|---|---|
| `SETUP_INCOMPLETE` | Payment, number verification or the residence confirmation is missing. Details: `blockers`, `setup_url` (absent while payment is missing). | After setup |
| `INSUFFICIENT_BALANCE` | Available balance is below the price of the alert or of its next step. Details: `price_cents`, `balance_cents`. No link. | After the owner adds balance in their BreakPass account |
| `RATE_LIMITED` | A rate cap, the account's unbilled-sends budget, one call at a time to a number, or the runaway brake applies. `retry_after_s` (next to `code`) says when to try again; `details.reason` names the limit (table below). | After `retry_after_s` |
| `RECIPIENT_NOT_FOUND` | No one on this account has that name. Use a name from `recipients` in `get_setup_status`, or `owner` for the user's own number. | No |
| `RECIPIENT_AMBIGUOUS` | Two or more people on the account share the name, so nothing was sent. The owner renames one in Settings > Phone numbers. | After the owner renames one |
| `RECIPIENT_NOT_VERIFIED` | The number isn't verified yet, or it is paused because the plan allows fewer numbers. `details.reason`: `pending`, `plan_limit`, or, when no recipient was named, `number_not_verified` / `number_paused`. | After the user verifies the number or picks another one |
| `RECIPIENT_OPTED_OUT` | The person stopped all BreakPass alerts to the number (STOP, key 9 or email). Only a new invite and verification restart them. | No |
| `CHANNEL_UNAVAILABLE` | A channel the send or workflow needs is not available for that number. Details: `available_channels`, `unavailable_steps` (workflows), `channel` and `reason`: `not_in_country`, `coming_soon`, `no_whatsapp`, `test_alert_pending`, `rate_above_max`, `country_unsupported`, `country_not_offered`, `whatsapp_capacity`, `whatsapp_not_live`, `sms_ack_us_ca_only`, `recipient_channel_paused`, `channel_unavailable`. Examples: "This number isn't on WhatsApp. Use SMS or a call.", "BreakPass doesn't reach this country yet.", "WhatsApp is paused right now. Use make_call, or send_sms where SMS is offered." | Another channel or workflow |
| `WORKFLOW_NOT_FOUND` | Unknown `workflow_id`. Details: `available` (the valid ids). | No |
| `WORKFLOWS_LOCKED` | Not returned today (the workflow lock was withdrawn); kept in the contract. | No |
| `PLAN_LIMIT` | Not returned by the tools today; a number paused by the plan returns `RECIPIENT_NOT_VERIFIED` (reason `plan_limit`). | No |
| `ALERT_NOT_FOUND` | Unknown id, or the alert is not on this account. | No |
| `CONTENT_REJECTED` | A field breaks a rule: missing, empty, over its limit, the wrong type or format, a message too long for one SMS or for a call, a link shortener or wa.me link, a link in an SMS outside the US and Canada, or emoji only. Details: `field`, with `max_length`, `rule` or `shorten_by`. | Fix the input |
| `IDEMPOTENCY_CONFLICT` | The same `idempotency_key` was used with a different payload. | Use a new key |
| `ACCOUNT_SUSPENDED` | The account is suspended (an admin action or a dispute freeze) or closed, or its sends are paused until a number is verified. | No |
| `PROVIDER_ERROR` | A delivery provider returned a server error or timed out, after failover. | After 30 seconds |
| `TEMPLATE_MISMATCH` | A WhatsApp message template failed its check, so the send was refused. | Later |
| `SERVICE_PAUSED` | Sending is paused for this scope ("Sending is paused right now. Try again later."). Details: `flag`; `retry_after_s` 300. | Later |

### `RATE_LIMITED` reasons

`details.reason` of a `RATE_LIMITED` error, with its message. A refusal without a message reads "Too many alerts. Wait before sending again."

| `details.reason` | Message | `retry_after_s` | Other details |
|---|---|---|---|
| `holder_per_hour` | Too many alerts to this recipient in the last hour (the limit is 20 an hour). | To the end of the hour | None |
| `holder_calls_per_hour` | Too many calls to this recipient in the last hour (the limit is 10 calls an hour to one number). An account with a raised limit shows its own figure. | 600 | `limit` |
| `call_in_progress` | A call to this recipient is already in progress. | 60 | None |
| `unbilled_budget` | Too many sends from this account didn't go through recently, so sending is paused for this account for now. Try again later. | Varies | `channel`, `window` (`hour` or `day`), `limit` |
| `unbilled_send_busy` | Other sends from this account are still on their way. Try again in a moment. | 2 | `channel` |
| `runaway_brake` | This agent connection is paused for sending too fast. | The seconds left of the 15 minutes | None |
| `account_per_minute`, `account_per_hour`, `account_per_day` | Too many alerts for this account right now. (Accounts without a paid plan.) | To the end of the window | None |
| `new_account_per_day` | This account has reached its daily alert limit. Try again tomorrow. | To the end of the day | None |
| `new_account_calls_per_day` | This account has reached its daily call limit. Try again tomorrow. | To the end of the day | `limit` (6) |
| `rate_cap` | Too many alerts right now. (Fallback.) | 60 | None |
