# Nivox MCP — connect any AI agent to your WhatsApp Business

Nivox exposes a WhatsApp Business account as a **Model Context Protocol (MCP)** server. Any MCP
client — claude.ai, Claude Desktop, ChatGPT, Claude Code, Codex, Cursor, Windsurf, VS Code,
Gemini CLI, custom agents — can read the inbox, answer customers, send templates and manage
templates with the same rules the human team follows.

| | |
| --- | --- |
| **MCP endpoint** | `https://nivox.me/mcp` |
| **Transport** | Streamable HTTP (JSON responses, stateless — no SSE stream, no session id) |
| **Protocol versions** | 2025-06-18, 2025-03-26, 2024-11-05 |
| **Authentication** | **OAuth 2.1** (recommended, "Authorize" button) or **API key** (`Authorization: Bearer nvx_…`) |
| **Machine-readable copy of this page** | `https://nivox.me/docs/mcp.md` |

> If you are an AI assistant reading this to configure a client: follow the section for the
> client you are running in, prefer OAuth when the client supports it, verify with
> `list_connections`, then install the skills listed at the end. Never send a WhatsApp message
> unless the user explicitly asks for that send.

## 1. Authentication

### OAuth 2.1 (no key to paste)

Clients that support OAuth discover everything automatically:

1. The client calls `https://nivox.me/mcp` and receives `401` with
   `WWW-Authenticate: Bearer resource_metadata="https://nivox.me/.well-known/oauth-protected-resource/mcp"`.
2. It reads `https://nivox.me/.well-known/oauth-authorization-server` (RFC 8414), registers itself
   (Dynamic Client Registration, RFC 7591) and opens the browser at `https://nivox.me/oauth/authorize`.
3. The Nivox **admin** signs in (if needed), picks the access level and which WhatsApp numbers the
   client may use, and clicks **Authorize**.
4. The client exchanges the code (PKCE S256) at `https://nivox.me/oauth/token` and receives an access
   token — a Nivox API key. It does not expire; the admin revokes it any time on
   **Integrations** (or the client calls `https://nivox.me/oauth/revoke`).

Only organization admins can authorize. Metadata endpoints:

- Authorization server: `https://nivox.me/.well-known/oauth-authorization-server`
- Protected resource: `https://nivox.me/.well-known/oauth-protected-resource/mcp`
- `authorization_endpoint`: `https://nivox.me/oauth/authorize` · `token_endpoint`: `https://nivox.me/oauth/token` ·
  `registration_endpoint`: `https://nivox.me/oauth/register` · `revocation_endpoint`: `https://nivox.me/oauth/revoke`
- Grant: `authorization_code` with PKCE `S256`; client auth `none`, `client_secret_post` or
  `client_secret_basic`; scopes `read`, `messages`, `full` (see §3).

### API key

An admin generates a key on **Integrations** (`https://nivox.me/app/integrations`). The key looks like
`nvx_` + 40 hex characters and is shown once. Send it as `Authorization: Bearer nvx_…` (or
`x-api-key: nvx_…` if the client cannot set `Authorization`). Keep it in the client's secret/env
store, never in a committed file.

## 2. Client setup

### claude.ai and Claude Desktop (custom connector, OAuth)

1. **Settings → Connectors → Add custom connector**.
2. Name: `Nivox`. Remote MCP server URL: `https://nivox.me/mcp`. Leave OAuth client id/secret empty
   (Nivox supports dynamic registration).
3. Click **Add**, then **Connect** — the Nivox authorization screen opens; sign in as an admin and
   click **Authorize**.
4. In a chat, enable the Nivox connector and ask, e.g. "list my WhatsApp conversations".

The connector appears in Claude Desktop automatically (same account). Custom connectors require a
paid Claude plan (Pro, Max, Team or Enterprise).

### ChatGPT (developer mode connector, OAuth)

1. **Settings → Connectors → Advanced → Developer mode** (enable).
2. **Create** a connector: Name `Nivox`, MCP Server URL `https://nivox.me/mcp`, Authentication **OAuth**.
3. Save, then **Connect** — authorize on the Nivox screen.
4. In a chat, add the Nivox connector from the tools menu ("+" → connectors).

### Claude Code

OAuth (recommended):

```bash
claude mcp add --transport http nivox https://nivox.me/mcp
# then, inside Claude Code, run /mcp and choose "Authenticate" for nivox
```

API key alternative:

```bash
claude mcp add --transport http nivox https://nivox.me/mcp \
  --header "Authorization: Bearer nvx_YOUR_KEY"
```

Project-scoped `.mcp.json` (commit it without the key):

```json
{ "mcpServers": { "nivox": { "type": "http", "url": "https://nivox.me/mcp" } } }
```

### Codex CLI (OpenAI)

OAuth:

```bash
codex mcp add nivox --url https://nivox.me/mcp
codex mcp login nivox
```

API key in `~/.codex/config.toml` (keep the key in an environment variable):

```toml
[mcp_servers.nivox]
url = "https://nivox.me/mcp"
bearer_token_env_var = "NIVOX_API_KEY"
```

### Cursor

`~/.cursor/mcp.json` (or the project's `.cursor/mcp.json`), then accept the **Authenticate**
prompt Cursor shows for the server:

```json
{ "mcpServers": { "nivox": { "url": "https://nivox.me/mcp" } } }
```

With a key instead of OAuth: add `"headers": { "Authorization": "Bearer nvx_YOUR_KEY" }`.

### Windsurf

`~/.codeium/windsurf/mcp_config.json`:

```json
{ "mcpServers": { "nivox": { "serverUrl": "https://nivox.me/mcp" } } }
```

### VS Code (GitHub Copilot agent mode)

`.vscode/mcp.json`; VS Code asks to sign in (OAuth) when the server starts:

```json
{ "servers": { "nivox": { "type": "http", "url": "https://nivox.me/mcp" } } }
```

### Gemini CLI

`~/.gemini/settings.json`:

```json
{ "mcpServers": { "nivox": { "httpUrl": "https://nivox.me/mcp", "oauth": { "enabled": true } } } }
```

### Any other MCP client

Generic Streamable HTTP configuration:

```json
{ "mcpServers": { "nivox": { "type": "http", "url": "https://nivox.me/mcp",
  "headers": { "Authorization": "Bearer nvx_YOUR_KEY" } } } }
```

Clients that only speak stdio can bridge with `npx -y mcp-remote https://nivox.me/mcp` (OAuth) or
`npx -y mcp-remote https://nivox.me/mcp --header "Authorization: Bearer nvx_YOUR_KEY"`.

### Verify

```bash
export NIVOX_API_KEY="nvx_..."   # the key from Integrations (skip with OAuth clients)
curl -sS https://nivox.me/mcp \
  -H "Authorization: Bearer $NIVOX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

Then call `list_connections` from the client: it must return at least one connection with
`status: "active"`.

## 3. Access levels (scopes)

| Scope | Acts as | Can |
| --- | --- | --- |
| `read` | viewer | list connections/conversations/templates, read history, search, download media |
| `messages` | agent | read + `send_text`, `send_media`, `send_buttons`, `send_interactive`, `send_reaction`, `send_template` to existing contacts, `mark_read` |
| `full` | manager | messages + `send_template` to **new** numbers, `create_template`, `delete_template`, `sync_templates`, `get_connection_health` |

Tools above the granted scope are not listed; calling one returns an `isError` result with
`code: "forbidden"`. A grant or key may also be limited to specific WhatsApp numbers. User and
key management are never reachable over MCP.

## 4. Tools

All ids are UUIDs returned by earlier tools. `connection_id` may be omitted when the credential
sees a single connection. Results carry `structuredContent` and the same JSON as text.

| Tool | Scope | Arguments (**required**) | Notes |
| --- | --- | --- | --- |
| `list_connections` | read | — | numbers, status, quality, sync stamps |
| `list_conversations` | read | connection_id, unread_only, open_window_only, query, limit ≤100, before | newest first; `window.open` = free text allowed |
| `get_conversation` | read | **conversation_id** | |
| `find_contact` | read | **phone**, connection_id | digits with country code; Brazilian 9th-digit variants handled |
| `list_messages` | read | **conversation_id**, limit ≤100, before, after | chronological within the page; cursors `next_before` / `next_after` |
| `search_messages` | read | **query**, connection_id, conversation_id, limit ≤50 | case-insensitive substring |
| `get_media_url` | read | **message_id** | 1-hour signed download URL |
| `list_templates` | read | connection_id, status (APPROVED default / all), include_components | header/body text and `{{n}}` counts |
| `send_text` | messages | **conversation_id**, **text**, reply_to_wamid, preview_url | 24h window must be open |
| `send_template` | messages | **template_name**, conversation_id or phone (+connection_id), language, body_params[], header_param | the only way outside the window; new phone → `full` |
| `send_media` | messages | **conversation_id**, **media_type**, **url** (https), caption, filename, mime, reply_to_wamid | limits: image 10 MB, audio 20 MB, video/document 25 MB, sticker 2 MB |
| `send_buttons` | messages | **conversation_id**, **body**, **buttons**[{id,title}] ≤3, header, footer, reply_to_wamid | quick-reply buttons |
| `send_interactive` | messages | **conversation_id**, **interactive** (raw Meta object), reply_to_wamid | lists, cta_url, flows |
| `send_reaction` | messages | **conversation_id**, **target_wamid**, **emoji** ("" removes) | |
| `mark_read` | messages | **conversation_id**, typing | unread = 0 + read receipt |
| `sync_templates` | full | connection_id | refresh the template cache from Meta |
| `create_template` | full | **name**, **language**, **category**, **body_text**, body_examples[], header_text, footer_text | text templates, Meta review |
| `delete_template` | full | **name**, connection_id | irreversible |
| `get_connection_health` | full | connection_id | daily Meta health snapshot |

Prompts offered by the server: `attend_inbox`, `reply_to_customer`, `daily_summary`. The
`initialize` response also carries `instructions` — the operating guide for the granted scope.

Error results always include `code`: `invalid_arguments`, `forbidden`, `not_found`,
`session_expired` (24h window closed → use `send_template`), `account_inactive`,
`template_not_approved`, `unsupported_template`, `too_large`, `fetch_failed`, `graph_error`
(Meta refused; read `details`), `internal_error`.

## 5. Operating rules (what a good agent does)

- Start with `list_connections`; then `list_conversations` with `unread_only: true` to triage.
- Read with `list_messages` before answering. Reply with `send_text` only while `window.open`
  is `true` (24 hours since the customer's last message). When it is closed, the **only** option is
  an APPROVED template via `send_template`; the customer's reply re-opens free text.
- After answering, `mark_read` so the human team sees the conversation handled.
- Never message contacts with `opted_out: true`; never invent order, payment or delivery facts;
  keep messages short — WhatsApp is chat. WhatsApp formatting: `*bold*`, `_italic_`, `~strike~`.
- Identity is the `conversation_id`; phones are display values (digits with country code).

## 6. Skills (Agent Skills, SKILL.md)

Three skills teach agents the Nivox workflow. Install them into your agent's skills directory —
`~/.claude/skills/<name>/SKILL.md` for Claude Code, `~/.codex/skills/<name>/SKILL.md` for Codex,
`.agents/skills/<name>/SKILL.md` or the client's equivalent elsewhere:

- `https://nivox.me/skills/nivox-whatsapp-attendant/SKILL.md`
- `https://nivox.me/skills/nivox-whatsapp-templates/SKILL.md`
- `https://nivox.me/skills/nivox-mcp-setup/SKILL.md`

One-liner (Claude Code; replace the directory for other agents):

```bash
for s in nivox-whatsapp-attendant nivox-whatsapp-templates nivox-mcp-setup; do
  mkdir -p ~/.claude/skills/$s && curl -fsSL https://nivox.me/skills/$s/SKILL.md -o ~/.claude/skills/$s/SKILL.md
done
```

The Integrations page also offers all three as a single zip download.

## 7. Troubleshooting

| Symptom | Cause / fix |
| --- | --- |
| `401 unauthorized` | Missing, revoked or malformed credential. Re-authorize (OAuth) or generate a new key. |
| The client does not start the OAuth flow | It may not support OAuth for remote MCP servers; use an API key (`headers`) or bridge with `mcp-remote`. |
| "Only administrators can authorize" | Sign in with an admin of the organization, or switch organization on the consent screen. |
| Tool result `code: "forbidden"` | Scope too low for that tool — a new grant/key with a higher level. |
| `code: "not_found"` for an id you just listed | The credential is limited to other connections, or the id belongs to another organization. |
| `code: "session_expired"` | 24h window closed → `send_template`. |
| `code: "graph_error"` | Meta refused; `details.code`/`details.message` explain (template parameters, number not on WhatsApp…). |
| `400 unsupported MCP protocol version` | Supported: 2025-06-18, 2025-03-26, 2024-11-05. |
| `405` on GET | Expected: the server is stateless (no SSE stream); POST JSON-RPC only. |

Security: one credential per agent, least scope that works, revoke on Integrations when an agent is
retired (immediate). Every message an agent sends is stored in the team inbox and attributed to
the credential that sent it.
