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:
- The client calls
https://nivox.me/mcpand receives401withWWW-Authenticate: Bearer resource_metadata="https://nivox.me/.well-known/oauth-protected-resource/mcp". - It reads
https://nivox.me/.well-known/oauth-authorization-server(RFC 8414), registers itself (Dynamic Client Registration, RFC 7591) and opens the browser athttps://nivox.me/oauth/authorize. - The Nivox admin signs in (if needed), picks the access level and which WhatsApp numbers the client may use, and clicks Authorize.
- The client exchanges the code (PKCE S256) at
https://nivox.me/oauth/tokenand receives an access token — a Nivox API key. It does not expire; the admin revokes it any time on Integrations (or the client callshttps://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_codewith PKCES256; client authnone,client_secret_postorclient_secret_basic; scopesread,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)
- Settings → Connectors → Add custom connector.
- Name:
Nivox. Remote MCP server URL:https://nivox.me/mcp. Leave OAuth client id/secret empty (Nivox supports dynamic registration). - Click Add, then Connect — the Nivox authorization screen opens; sign in as an admin and click Authorize.
- 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)
- Settings → Connectors → Advanced → Developer mode (enable).
- Create a connector: Name
Nivox, MCP Server URLhttps://nivox.me/mcp, Authentication OAuth. - Save, then Connect — authorize on the Nivox screen.
- 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:
- Inline (≤ 5 MB) —
fileis 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. - Upload link (up to the full limit) — for a file on the agent's machine:
create_media_upload(filename, conversation_id)answersupload.url, a readycurlline andfile(upload:<id>). PUT the raw bytes to the link — no header needed, the link is its own credential — then callsend_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). - URL —
urlis 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)
- Start with
list_connections; thenlist_conversationswithunread_only: trueto triage. - Read with
list_messagesbefore answering. Reply withsend_textonly whilewindow.openistrue(24 hours since the customer's last message). When it is closed, the only option is an APPROVED template viasend_template; the customer's reply re-opens free text. - After answering,
mark_readso 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). - With CRM/Help Desk on:
get_contact(contact_id)before answering (history, deals, open tickets, notes),add_noteafter, andcreate_ticketfor anything you cannot resolve now. Never resolve a ticket you did not resolve, and never mark a deal won unless the user said so.
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:
https://nivox.me/skills/nivox-whatsapp-attendant/SKILL.mdhttps://nivox.me/skills/nivox-whatsapp-templates/SKILL.mdhttps://nivox.me/skills/nivox-crm/SKILL.mdhttps://nivox.me/skills/nivox-helpdesk/SKILL.mdhttps://nivox.me/skills/nivox-mcp-setup/SKILL.md
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.