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. When the organization has the CRM or Help Desk module enabled, the same server also exposes its contacts, pipeline, deals and support tickets.

MCP endpoint https://nivox.me/mcp
Transport Streamable HTTP (JSON responses, stateless — no SSE stream, no session id)
Protocol versions 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05
Authentication OAuth 2.1 (recommended, "Authorize" button) or API key (Authorization: Bearer nvx_…)
REST API (same operations, same key) https://nivox.me/api/v1 — https://nivox.me/docs/api
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:

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

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

API key alternative:

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

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

Codex CLI (OpenAI)

OAuth:

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

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

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

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

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

Gemini CLI

~/.gemini/settings.json:

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

Any other MCP client

Generic Streamable HTTP configuration:

{ "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

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, read contacts/opportunities/tickets/notes/stages/tags
messages agent read + send_text, send_media (+ create_media_upload), send_buttons, send_interactive, send_reaction, send_template to existing contacts, mark_read + create/update/move contacts, tag and untag them, notes, opportunities and tickets
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, stage editing, renaming or deleting a tag, and deletions are never reachable over MCP.

Optional modules

CRM and Help Desk are switched on per organization by Nivox. A module that is off does not list its tools at all; calling one by name returns code: "module_disabled". Contacts, notes and stages (list_contacts, get_contact, add_note, list_stages…) come with either module; *_opportunity needs CRM and *_ticket needs Help Desk. Call tools/list to see what this organization actually has.

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 carries contact_id — the bridge to the CRM tools
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, file or url, caption, filename, mime, reply_to_wamid file: a data: URI (≤ 5 MB) or upload:<id> from create_media_upload; url: public https. Limits: image 10 MB, audio 20 MB, video/document 25 MB, sticker 2 MB — see "Sending files"
create_media_upload messages filename, mime, media_type, size, conversation_id or connection_id one-time PUT link for a local file (1 h) + the file to send (24 h, reusable)
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 (quality, limits, payment-method status)

Sending files (no public URL needed)

WhatsApp never needs a public link from you: Nivox uploads the bytes to Meta itself. Hand send_media the file in whichever way your client can:

  1. Inline (≤ 5 MB) — file is a data: URI, the convention MCP's file-input proposals use: data:application/pdf;name=proposta.pdf;base64,JVBERi0xLjcK… (name= percent-encoded, optional). Best for code that builds the call itself; an LLM should not type base64 by hand.
  2. Upload link (up to the full limit) — for a file on the agent's machine: create_media_upload(filename, conversation_id) answers upload.url, a ready curl line and file (upload:<id>). PUT the raw bytes to the link — no header needed, the link is its own credential — then call send_media(conversation_id, media_type, file: "upload:<id>"). The link works once, for one hour; the file can be sent to any conversation of the same number for 24 h, as often as needed. Needs a client that can make an HTTP request (a terminal, code execution).
  3. URL — url is a public https address Nivox downloads (no private or local hosts).
# 2. upload link: the curl line from create_media_upload, with your local path
curl -sS -X PUT -H 'Content-Type: application/pdf' --data-binary @./proposta.pdf 'https://nivox.me/api/v1/uploads/<token>'

A client that can neither run code nor make an HTTP request cannot forward a file the user attached in the chat: MCP has no way yet to carry a binary from the client to the server (file inputs are an open proposal), so the model would have to retype the file byte by byte. Use a URL there, or an agent with a terminal.

CRM and Help Desk tools

Listed only when the organization has the module (§3). Every conversation carries contact_id, which is what these tools take. find_contact is the WhatsApp lookup by phone; get_contact is the customer record.

Tool Module · Scope Arguments (required) Notes
list_contacts contacts · read kind (lead/client), stage_key, client_status, owner_user_id, tag, query, updated_after, limit ≤100 people the organization knows; created automatically from WhatsApp traffic; tag filters by one label (key or name)
get_contact contacts · read contact_id the context call: contact + stage + conversations across every number + open opportunities + open tickets + last 10 notes
create_contact contacts · messages display_name, phone, email, company, tags[], kind, stage_key, client_status, owner_user_id for people who have not written on WhatsApp; tags[] takes keys or names — a name this organization lacks is created
update_contact contacts · messages contact_id, display_name, phone, email, company, tags[], owner_user_id, clear_owner a name set here is never overwritten by the WhatsApp profile name; tags[] REPLACES the whole list (keys or names; unknown names are created)
move_contact contacts · messages contact_id, stage_key (lead) or client_status (client) a won stage converts the lead into a client
convert_contact contacts · messages contact_id, kind, stage_key, client_status lead ⇄ client
add_note contacts · messages contact_id, body, opportunity_id, ticket_id internal; never sent to WhatsApp
list_notes contacts · read contact_id, limit ≤100 newest first
list_stages contacts · read entity (lead/opportunity/ticket) the organization's own stage keys and their kind (open/won/lost)
list_tags contacts · read — the organization's label vocabulary: key (what you name), name (what humans read), color
tag_contact contacts · messages contact_id, tag one label; a key reuses an existing tag, an unknown NAME creates one
untag_contact contacts · messages contact_id, tag takes the label off this person; the tag stays in the vocabulary
list_members contacts · messages — ids and display names of the team, so update_contact/update_opportunity (owner_user_id) and assign_ticket have someone to name
list_opportunities crm · read stage_key, contact_id, owner_user_id, open_only, query, updated_after, limit ≤100 deals
get_opportunity crm · read opportunity_id deal + contact
create_opportunity crm · messages contact_id, title, value_cents (integer cents), currency, stage_key, owner_user_id, expected_close_on
update_opportunity crm · messages opportunity_id, title, value_cents, currency, expected_close_on, owner_user_id, clear_owner
move_opportunity crm · messages opportunity_id, stage_key, lost_reason won stamps the close date and converts the contact
list_tickets helpdesk · read stage_key, priority, assignee_user_id, contact_id, conversation_id, open_only, query, updated_after, limit ≤100
get_ticket helpdesk · read ticket_id ticket + contact
create_ticket helpdesk · messages subject, conversation_id or contact_id, description, priority, stage_key, assignee_user_id from a conversation the contact is resolved for you; per-org ticket number
update_ticket helpdesk · messages ticket_id, subject, description, priority, conversation_id
move_ticket helpdesk · messages ticket_id, stage_key won = resolved, lost = closed; back to open clears both
assign_ticket helpdesk · messages ticket_id, assignee_user_id or unassign member ids come from the Nivox app

Tags are a per-organization vocabulary, not free text. Call list_tags before labelling and name a key you saw there: a name this organization does not have yet is created on the spot, so one typo becomes a permanent label nobody else uses. A contact's tags are those keys (at most 20 per contact, 200 per organization); list_contacts takes one of them as tag. Renaming or recolouring a label — and removing it from everybody — are manager actions in the Nivox app, and the key never moves when the name changes, so nothing you stored goes stale.

Editing stages and deleting contacts, deals or tickets are not MCP tools — they stay in the Nivox app (manager+). Ask the user to do it there.

Prompts offered by the server: attend_inbox, reply_to_customer, daily_summary, plus qualify_lead and triage_ticket / pipeline_review when the matching module is on. The initialize response also carries instructions — the operating guide for the granted scope and the modules this organization has.

Error results always include code. Every tool: forbidden (scope too low), module_disabled (that module is off for the organization), internal_error (unexpected). WhatsApp tools also: 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), upload_pending (409 — send_media got an upload whose bytes were never PUT), limit_reached (409, create_media_upload — 50 unsent uploads pending), and invalid_arguments (a malformed data: URI, both or neither of file/url, an upload of another number). CRM/Help Desk tools also: invalid_arguments (the value you gave is wrong — same code the product UI gets for the same mistake, one shared pipeline), not_found (wrong id, or another organization's row — the same answer either way, on purpose), no_stages (409 — a create/move found zero stages for that entity; only possible if every stage of a kind was deleted in the app), duplicate (409, create_contact only — that WhatsApp identity already has a contact), already_in_kind (409, convert_contact only — already that kind), limit_reached (409 — a contact already carries 20 tags, or the organization already defines 200).

5. Operating rules (what a good agent does)

6. Skills (Agent Skills, SKILL.md)

Five skills teach agents the Nivox workflow (the CRM and Help Desk ones only matter when those modules are on). 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:

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

for s in nivox-whatsapp-attendant nivox-whatsapp-templates nivox-crm nivox-helpdesk 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 of them 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.
Tool result code: "module_disabled" The CRM or Help Desk module is off for this organization; a Nivox operator enables it.
No get_contact / create_ticket in tools/list Same cause: neither module is enabled.
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…).
code: "too_large" on an inline file Over 5 MB inline: create_media_upload + PUT, then file: "upload:<id>".
code: "upload_pending" The PUT never happened (or failed) — run the curl line, then send again.
The upload link answers 404 Wrong or expired link (1 h, one use), or the key that created it was revoked: create a new one.
400 unsupported MCP protocol version Supported: 2025-11-25, 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.