Nivox REST API — WhatsApp Business, CRM and Help Desk over HTTP

The Nivox REST API gives platforms, backends and automation tools the same operations the Nivox MCP server gives AI agents: read the WhatsApp inbox, answer customers, send templates and media, manage templates, and work the CRM and Help Desk — with the same rules the human team follows. One API key works on both. Ids, field names and error codes are identical, and every operation names its MCP tool, so a platform can call REST from its backend and let its AI agent use MCP on the same data. Optional webhooks push incoming messages and delivery statuses to your platform.

Base URL https://nivox.me/api/v1
OpenAPI 3.1 https://nivox.me/api/v1/openapi.json (public)
Authentication Authorization: Bearer nvx_… or x-api-key: nvx_…
Format JSON; { "ok": true, "data": … } / { "ok": false, "error": { … } }
MCP twin https://nivox.me/mcp — same key, same operations
Machine-readable copy of this page https://nivox.me/docs/api.md

If you are an AI assistant integrating a platform: read §1–§4, use the OpenAPI document for exact schemas, send an Idempotency-Key on every send, verify webhook signatures (§7). Never send a WhatsApp message to a real customer unless the user explicitly asks for that send.

Contents: 1. Authentication · 2. Conventions · 3. REST and MCP together · 4. Quick start · 5. Endpoint reference · 6. Objects · 7. Webhooks · 8. Troubleshooting · 9. Changelog

1. Authentication

An organization admin creates API keys on Integrations (https://nivox.me/app/integrations). A key is nvx_ + 40 hex characters and is shown once (Nivox keeps only its SHA-256). Send it as Authorization: Bearer nvx_… — or x-api-key: nvx_… for tools that cannot set Authorization. Keep it in a secret store or an environment variable (NIVOX_API_KEY), never in code and never in a browser bundle: a key is a header, not a cookie, so whoever sees it can use it.

Scope Acts as Can
read viewer every GET: connections, conversations, history, search, media links, templates, contacts, opportunities, tickets, notes, stages
messages agent read + sends in existing conversations and templates to existing contacts, mark as read, CRM/Help Desk everyday writes, members
full manager messages + templates to new numbers, create/delete/sync templates, connection health

Check a key: GET /v1/me answers the organization, scope, connection ids, enabled modules and the exact operations the key can call right now.

2. Conventions

2.1 Envelope

{ "ok": true, "data": { "conversations": [], "count": 0, "next_before": null } }
{
  "ok": false,
  "error": {
    "code": "session_expired",
    "message": "outside the 24h window — send a template to reopen the conversation",
    "details": { "hint": "The 24h customer-service window is closed. Send an APPROVED template with send_template to re-open it; free text is allowed again once the customer replies." }
  }
}

Branch on error.code (stable), never on message (human text; it may name the MCP tool).

2.2 Errors

Code HTTP Meaning / what to do
invalid_arguments 400 A field is missing or wrong — the message names it. Fix the request.
invalid_json 400 The body is not a JSON object.
unauthorized 401 Missing, malformed, revoked or expired key. Do not retry.
forbidden 403 Scope too low (details.requiredRole). Ask an admin for a higher-scope key.
module_disabled 403 The CRM or Help Desk module is off for the organization.
not_found 404 Wrong id — or another organization's / another connection's (the same answer on purpose).
route_not_found 404 No such method + path. Check openapi.json.
method_not_allowed 405 The path exists with other methods (see the Allow header).
session_expired 409 The 24-hour window is closed: send an APPROVED template.
account_inactive 409 The WhatsApp connection is not active; sends are refused.
template_not_approved 409 The template exists but is not APPROVED.
duplicate 409 POST /v1/contacts: that WhatsApp identity already has a contact.
no_stages 409 The organization has no stage for that entity (all were deleted in the app).
already_in_kind 409 convert: the contact already is that kind.
no_media 409 The message has no downloadable media (yet).
idempotency_in_progress 409 A request with the same Idempotency-Key is still running — retry after Retry-After.
payload_too_large 413 Request body over 1 MB.
too_large 413 Media file over the limit for its type.
unsupported_template 422 Template with a media/location header (not supported yet).
fetch_failed 422 Nivox could not download the media url.
idempotency_conflict 422 The Idempotency-Key was used for a different request — use a new key.
rate_limited 429 Reserved for future limits — honour Retry-After.
internal_error 500 Unexpected; retry with backoff (reuse the Idempotency-Key).
graph_error 502 Meta refused (details.details carries Meta's code, e.g. a template parameter). Retry only if transient.

2.3 Ids, timestamps and pagination

2.4 Idempotency

Send Idempotency-Key: <1–255 printable ASCII characters> on POST requests (a UUID per logical operation). Within 24 hours, repeating the same request with the same key does not run it again: the first answer is replayed byte for byte with Idempotent-Replayed: true. The same key with a different request answers 422 idempotency_conflict; while the first is still running, 409 idempotency_in_progress. Answers of 500 or above are not stored (a retry runs again). Keys are scoped to the API key.

2.5 Limits

2.6 Request ids, versioning, CORS

3. REST and MCP together

Every REST operation is an MCP tool behind a second transport: the same validation, the same scope and module gates, the same pipeline and the same audit trail. The reference below names the tool of each endpoint; the OpenAPI document carries it as x-mcp-tool.

4. Quick start

export NIVOX_API_KEY="nvx_..."          # from Integrations
BASE=https://nivox.me/api/v1

curl -s $BASE/me -H "Authorization: Bearer $NIVOX_API_KEY"                                    # who am I, what can I call
curl -s $BASE/connections -H "Authorization: Bearer $NIVOX_API_KEY"                           # my WhatsApp numbers
curl -s "$BASE/conversations?unread_only=true" -H "Authorization: Bearer $NIVOX_API_KEY"      # inbox triage
curl -s "$BASE/conversations/$CONVERSATION_ID/messages" -H "Authorization: Bearer $NIVOX_API_KEY"

# reply while window.open is true
curl -s -X POST $BASE/conversations/$CONVERSATION_ID/messages -H "Authorization: Bearer $NIVOX_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" -d '{"type":"text","text":"Oi! Já verifico para você."}'

curl -s -X POST $BASE/conversations/$CONVERSATION_ID/read -H "Authorization: Bearer $NIVOX_API_KEY" -H "Content-Type: application/json" -d '{}'

5. Endpoint reference

All paths are relative to https://nivox.me/api. Parameters marked (required) are required.

Discovery

GET /v1

Public index: name, version and the documentation, OpenAPI and MCP URLs. No key needed.

GET /v1/openapi.json

This API as an OpenAPI 3.1 document (paths, request bodies from the real tool schemas, response schemas, webhook payloads). No key needed. Import it into Postman, Insomnia or a client generator.

GET /v1/me

The calling key. Scope: any.

Response data: { api_key: { id, name, scope, role, connection_ids }, organization: { id, name }, modules: { crm, helpdesk }, mcp_endpoint, operations: [{ method, path, operation_id, mcp_tools }] }

curl -s https://nivox.me/api/v1/me -H "Authorization: Bearer $NIVOX_API_KEY"

Connections

GET /v1/connections

The WhatsApp Business numbers this key can operate. Start here.

Scope: read · MCP tool: list_connections

Response data: { connections: Connection[], scope }

curl -s https://nivox.me/api/v1/connections -H "Authorization: Bearer $NIVOX_API_KEY"

GET /v1/connections/{connection_id}/health

Latest Meta health snapshot: messaging limits, quality rating, display-name status, payment method (payment.status ok | issue), template counts, 7-day volumes and 30-day cost. Refreshed daily.

Scope: full · MCP tool: get_connection_health

Path parameter Type Description
connection_id uuid The connection.

Response data: { connection_id, phone, status, registered, quality_rating, health, health_checked_at }

Conversations

GET /v1/conversations

Inbox listing, most recent activity first. window.open says whether free text is allowed (24 h since the customer last wrote).

Scope: read · MCP tool: list_conversations

Query parameter Type Description
connection_id uuid WhatsApp connection (GET /v1/connections). Optional when the key sees a single connection.
unread_only boolean Only conversations with unread customer messages.
open_window_only boolean Only conversations where free text is allowed now.
query string ≤ 80 Case-insensitive match on the contact name or on phone digits.
limit integer 1–100 Page size (default 30).
before ISO timestamp Cursor: next_before of the previous page.

Response data: { conversations: Conversation[], count, next_before }

curl -s "https://nivox.me/api/v1/conversations?unread_only=true&limit=20" -H "Authorization: Bearer $NIVOX_API_KEY"

GET /v1/conversations/lookup

The conversation(s) of a phone number (digits with country code). Brazilian ninth-digit variants are matched.

Scope: read · MCP tool: find_contact

Query parameter Type Description
phone string 8–20 (required) e.g. 5511999998888.
connection_id uuid WhatsApp connection (GET /v1/connections). Optional when the key sees a single connection.

Response data: { phone, conversations: Conversation[], hint? }

curl -s "https://nivox.me/api/v1/conversations/lookup?phone=5511999998888" -H "Authorization: Bearer $NIVOX_API_KEY"

GET /v1/conversations/{conversation_id}

One conversation with its contact and 24-hour window state.

Scope: read · MCP tool: get_conversation

Path parameter Type Description
conversation_id uuid The conversation.

Response data: { conversation: Conversation, connection: { id, phone, status } }

GET /v1/conversations/{conversation_id}/messages

Message history, chronological (oldest first) within the page. Default: the latest 30. Page backwards with before; poll forward with after.

Scope: read · MCP tool: list_messages

Path parameter Type Description
conversation_id uuid The conversation.
Query parameter Type Description
limit integer 1–100 Page size (default 30).
before ISO timestamp Only messages created before this (next_before).
after ISO timestamp Only messages created after this (next_after), oldest first.

Response data: { conversation_id, contact_name, window_open: Window, messages: Message[], count, has_more, next_before, next_after }

curl -s "https://nivox.me/api/v1/conversations/$CONVERSATION_ID/messages?limit=50" -H "Authorization: Bearer $NIVOX_API_KEY"

POST /v1/conversations/{conversation_id}/messages

Send into this conversation. Same body as POST /v1/messages (the type table below) without conversation_id, which the path fixes.

Scope: messages · MCP tool: send_text · send_template · send_media · send_buttons · send_interactive · send_reaction

Path parameter Type Description
conversation_id uuid The conversation.

Response data: see POST /v1/messages. 201 (a reaction answers 200).

curl -s -X POST https://nivox.me/api/v1/conversations/$CONVERSATION_ID/messages -H "Authorization: Bearer $NIVOX_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"type":"text","text":"Olá! Seu pedido saiu para entrega."}'

POST /v1/conversations/{conversation_id}/read

Zero the unread counter in the team inbox and send WhatsApp read receipts (blue ticks); typing: true also shows "typing…".

Scope: messages · MCP tool: mark_read

Path parameter Type Description
conversation_id uuid The conversation.
Body field Type Description
typing boolean Also show the typing indicator for a few seconds.

Response data: { conversation_id, unread_count, receipt_sent }

Messages

POST /v1/messages

One endpoint for every kind of send, picked by type. Free text, media, buttons, interactive messages and reactions need the 24-hour window to be open (session_expired otherwise); a template works any time and is the only way to reach a number with no conversation yet (phone, scope full).

Scope: messages · MCP tool: send_text · send_template · send_media · send_buttons · send_interactive · send_reaction

Body field Type Description
type string (required) text · template · media · buttons · interactive · reaction.
conversation_id uuid Required for every type except template with phone.
text string 1–4096 text: the message. WhatsApp formatting: *bold*, _italic_, ~strike~.
preview_url boolean text: render link previews (default true).
reply_to_wamid string text, media, buttons, interactive: quote this customer message.
template_name string (required) template: an APPROVED template (GET /v1/templates).
language string template: pt_BR, en_US… Optional when the template has one language.
body_params string[] ≤ 20 template: values for {{1}}, {{2}}… of the body, in order.
header_param string ≤ 60 template: value for the header {{1}}.
phone string template: destination digits with country code, instead of conversation_id. A NEW number needs scope full.
connection_id uuid template with phone: which number sends (optional with one connection).
media_type string (required) media: image · video · audio · document · sticker.
url https URL (required) media: public file, fetched by Nivox (image 10 MB, audio 20 MB, video/document 25 MB, sticker 2 MB).
caption string ≤ 1024 media: caption (not for audio/sticker).
filename string ≤ 120 media: file name shown for documents.
mime string ≤ 100 media: override the MIME type the URL reports.
body string 1–1024 (required) buttons: the message text.
buttons array ≤ 3 (required) buttons: [{ "id": "yes", "title": "Sim" }] (title ≤ 20).
header string ≤ 60 buttons: header text.
footer string ≤ 60 buttons: footer text.
interactive object (required) interactive: a raw Meta interactive object (list, cta_url, flow…).
target_wamid string (required) reaction: the message to react to.
emoji string ≤ 8 (required) reaction: one emoji; "" removes the reaction.

Response data: { message_id, wamid, conversation_id, status: "sent", type } (templates add template and the filled text); a reaction answers { conversation_id, wamid, emoji }. 201 (reaction 200). conversation_id may differ from the one you sent to when Meta canonicalized the number.

Send an Idempotency-Key with every send (§2.4). Delivery and read arrive later as message.status webhooks (§7) or by re-reading the messages.

curl -s -X POST https://nivox.me/api/v1/messages -H "Authorization: Bearer $NIVOX_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"type":"template","phone":"5511999998888","template_name":"order_shipped","language":"pt_BR","body_params":["4821"]}'

GET /v1/messages/search

Case-insensitive substring search over message text across the conversations this key can see.

Scope: read · MCP tool: search_messages

Query parameter Type Description
query string 2–80 (required) Text to find.
connection_id uuid WhatsApp connection (GET /v1/connections). Optional when the key sees a single connection.
conversation_id uuid Restrict to one conversation.
limit integer 1–50 Page size (default 20).

Response data: { messages: Message[], count }

GET /v1/messages/{message_id}/media

A 1-hour signed download URL for the image, audio, video or document of a message whose media.available is true. Download it; do not store the link.

Scope: read · MCP tool: get_media_url

Path parameter Type Description
message_id uuid The message.

Response data: { message_id, url, mime, bytes, expires_in_seconds }409 no_media while the file is not available.

Templates

GET /v1/templates

Message templates of a connection (cached copy of Meta’s list; the first call on a never-synced connection pulls it from Meta). Only APPROVED templates can be sent.

Scope: read · MCP tool: list_templates

Query parameter Type Description
connection_id uuid WhatsApp connection (GET /v1/connections). Optional when the key sees a single connection.
status string APPROVED (default) · PENDING · REJECTED · PAUSED · all.
include_components boolean Include Meta’s raw components (buttons, examples).

Response data: { connection_id, synced_at, templates: Template[], count }

POST /v1/templates

Submit a new text template to Meta for approval (minutes to hours). Body variables are {{1}}, {{2}}… and each needs an example.

Scope: full · MCP tool: create_template

Body field Type Description
connection_id uuid WhatsApp connection (GET /v1/connections). Optional when the key sees a single connection.
name string (required) Lowercase letters, digits and underscores.
language string (required) pt_BR, en_US, es_ES…
category string (required) UTILITY · MARKETING · AUTHENTICATION.
body_text string 1–1024 (required) The body with {{n}} placeholders.
body_examples string[] ≤ 20 One example per {{n}}, in order (Meta requires them).
header_text string ≤ 60 Optional text header.
footer_text string ≤ 60 Optional footer.

Response data: { connection_id, id, name, status } 201

POST /v1/templates/sync

Refresh the template cache of a connection from Meta (statuses, new templates).

Scope: full · MCP tool: sync_templates

Body field Type Description
connection_id uuid WhatsApp connection (GET /v1/connections). Optional when the key sees a single connection.

Response data: { connection_id, synced, synced_at }

DELETE /v1/templates/{name}

Delete a template (every language of that name) on Meta. Irreversible.

Scope: full · MCP tool: delete_template

Path parameter Type Description
name string Template name.
Query parameter Type Description
connection_id uuid WhatsApp connection (GET /v1/connections). Optional when the key sees a single connection.

Response data: { connection_id, name, deleted: true }

Contacts, notes, stages and members

Needs the CRM or the Help Desk module (403 module_disabled otherwise).

GET /v1/contacts

People the organization knows (leads and clients), created automatically from WhatsApp traffic.

Scope: read · Module: CRM or Help Desk · MCP tool: list_contacts

Query parameter Type Description
kind string lead · client.
stage_key string Lead stage key (GET /v1/stages).
client_status string active · inactive.
owner_user_id uuid A member id (GET /v1/members).
query string ≤ 80 Name, phone, e-mail or company.
updated_after ISO timestamp Delta sync: only records changed after this.
limit integer 1–100 Page size (default 30).

Response data: { contacts: Contact[], count }

POST /v1/contacts

Create a contact for someone who has not written on WhatsApp yet.

Scope: messages · Module: CRM or Help Desk · MCP tool: create_contact

Body field Type Description
display_name string ≤ 120 Name (this or phone is required).
phone string ≤ 20 Digits with country code.
email string ≤ 254 E-mail.
company string ≤ 120 Company.
tags string[] ≤ 20 Tags (≤ 40 characters each).
kind string lead (default) · client.
stage_key string Lead stage; default = the first stage.
client_status string active · inactive (clients).
owner_user_id uuid A member id (GET /v1/members).

Response data: { contact: Contact } 201409 duplicate when that WhatsApp identity already has a contact.

GET /v1/contacts/{contact_id}

Everything about one person in one call: the contact and stage, their conversations on the connections this key can see, open opportunities, open tickets and the last 10 notes.

Scope: read · Module: CRM or Help Desk · MCP tool: get_contact

Path parameter Type Description
contact_id uuid The contact (every Conversation carries contact_id).

Response data: { contact: Contact, conversations: [{ id, connection_id, phone, unread_count, last_message_at, window_open }], open_opportunities: Opportunity[] | null, open_tickets: Ticket[] | null, notes: Note[] } — a module that is off comes back null.

PATCH /v1/contacts/{contact_id}

Correct or enrich a contact. A name set here is never overwritten by the WhatsApp profile name.

Scope: messages · Module: CRM or Help Desk · MCP tool: update_contact

Path parameter Type Description
contact_id uuid The contact.
Body field Type Description
display_name string ≤ 120 Name.
phone string ≤ 20 Digits with country code.
email string ≤ 254 E-mail.
company string ≤ 120 Company.
tags string[] ≤ 20 Replaces the whole tag list.
owner_user_id uuid A member id (GET /v1/members).
clear_owner boolean Leave the contact with no owner.

Response data: { contact: Contact }

POST /v1/contacts/{contact_id}/move

Leads: move to another stage (a won stage converts the lead). Clients: set the status.

Scope: messages · Module: CRM or Help Desk · MCP tool: move_contact

Path parameter Type Description
contact_id uuid The contact.
Body field Type Description
stage_key string Lead stage key (leads only).
client_status string active · inactive (clients only).

Response data: { contact: Contact }

POST /v1/contacts/{contact_id}/convert

Turn a lead into a client, or a client back into a lead.

Scope: messages · Module: CRM or Help Desk · MCP tool: convert_contact

Path parameter Type Description
contact_id uuid The contact.
Body field Type Description
kind string (required) lead · client.
stage_key string Lead stage when converting back to a lead.
client_status string active · inactive.

Response data: { contact: Contact }409 already_in_kind when it already is that kind.

GET /v1/contacts/{contact_id}/notes

Internal notes about a contact, newest first.

Scope: read · Module: CRM or Help Desk · MCP tool: list_notes

Path parameter Type Description
contact_id uuid The contact.
Query parameter Type Description
limit integer 1–100 Page size (default 20).

Response data: { notes: Note[], count }

POST /v1/contacts/{contact_id}/notes

Add an internal note (never sent to WhatsApp), optionally attached to an opportunity or a ticket.

Scope: messages · Module: CRM or Help Desk · MCP tool: add_note

Path parameter Type Description
contact_id uuid The contact.
Body field Type Description
body string 1–4000 (required) The note.
opportunity_id uuid Attach to this opportunity.
ticket_id uuid Attach to this ticket.

Response data: { note: Note } 201

GET /v1/stages

The organization’s stage vocabulary per entity, in board order. key is what every move takes; kind is open, won or lost.

Scope: read · Module: CRM or Help Desk · MCP tool: list_stages

Query parameter Type Description
entity string lead · opportunity · ticket; omit for all three.

Response data: { stages: Stage[], count }

GET /v1/members

Team members who can own a record or be assigned a ticket — id and display name only.

Scope: messages · Module: CRM or Help Desk · MCP tool: list_members

Response data: { members: [{ user_id, display_name }], count }

Opportunities

Needs the CRM module.

GET /v1/opportunities

Deals in the sales pipeline, most recently updated first.

Scope: read · Module: CRM · MCP tool: list_opportunities

Query parameter Type Description
stage_key string Opportunity stage key.
contact_id uuid Deals of one contact.
owner_user_id uuid A member id (GET /v1/members).
open_only boolean Only deals neither won nor lost.
query string ≤ 80 Title match.
updated_after ISO timestamp Delta sync.
limit integer 1–100 Page size (default 30).

Response data: { opportunities: Opportunity[], count }

POST /v1/opportunities

Open a deal for a contact.

Scope: messages · Module: CRM · MCP tool: create_opportunity

Body field Type Description
contact_id uuid (required) The contact.
title string 1–120 (required) What is being sold.
value_cents integer ≥ 0 Integer cents: R$ 1.500,00 is 150000.
currency string (3) ISO code, BRL default.
stage_key string Default = the first stage.
owner_user_id uuid A member id (GET /v1/members).
expected_close_on YYYY-MM-DD Expected close date.

Response data: { opportunity: Opportunity } 201

GET /v1/opportunities/{opportunity_id}

One deal with its contact.

Scope: read · Module: CRM · MCP tool: get_opportunity

Path parameter Type Description
opportunity_id uuid The opportunity.

Response data: { opportunity: Opportunity, contact: Contact | null }

PATCH /v1/opportunities/{opportunity_id}

Change title, value, currency, expected close date or owner.

Scope: messages · Module: CRM · MCP tool: update_opportunity

Path parameter Type Description
opportunity_id uuid The opportunity.
Body field Type Description
title string 1–120 Title.
value_cents integer ≥ 0 Value in cents.
currency string (3) ISO code.
expected_close_on YYYY-MM-DD Expected close date.
owner_user_id uuid A member id (GET /v1/members).
clear_owner boolean Leave the deal with no owner.

Response data: { opportunity: Opportunity }

POST /v1/opportunities/{opportunity_id}/move

Advance or close a deal. A won stage stamps the close date and turns the contact into a client; lost records lost_reason.

Scope: messages · Module: CRM · MCP tool: move_opportunity

Path parameter Type Description
opportunity_id uuid The opportunity.
Body field Type Description
stage_key string (required) Opportunity stage key.
lost_reason string ≤ 200 Only for a lost stage.

Response data: { opportunity: Opportunity, contact_converted }

Tickets

Needs the Help Desk module.

GET /v1/tickets

Support tickets, most recently updated first.

Scope: read · Module: Help Desk · MCP tool: list_tickets

Query parameter Type Description
stage_key string Ticket stage key.
priority string low · normal · high · urgent.
assignee_user_id uuid Tickets of one member.
contact_id uuid Tickets of one contact.
conversation_id uuid Tickets opened from one conversation.
open_only boolean Only tickets neither resolved nor closed.
query string ≤ 80 Subject match.
updated_after ISO timestamp Delta sync.
limit integer 1–100 Page size (default 30).

Response data: { tickets: Ticket[], count }

POST /v1/tickets

Open a ticket — from a conversation (the contact is resolved for you) or for a contact.

Scope: messages · Module: Help Desk · MCP tool: create_ticket

Body field Type Description
conversation_id uuid The WhatsApp conversation it came from (preferred).
contact_id uuid When there is no conversation.
subject string 1–160 (required) Subject.
description string ≤ 8000 The problem, in detail.
priority string low · normal (default) · high · urgent.
stage_key string Default = the first ticket stage.
assignee_user_id uuid A member id (GET /v1/members).

Response data: { ticket: Ticket } 201

GET /v1/tickets/{ticket_id}

One ticket with its contact.

Scope: read · Module: Help Desk · MCP tool: get_ticket

Path parameter Type Description
ticket_id uuid The ticket.

Response data: { ticket: Ticket, contact: Contact | null }

PATCH /v1/tickets/{ticket_id}

Change subject, description, priority or the linked conversation.

Scope: messages · Module: Help Desk · MCP tool: update_ticket

Path parameter Type Description
ticket_id uuid The ticket.
Body field Type Description
subject string 1–160 Subject.
description string ≤ 8000 Description.
priority string low · normal · high · urgent.
conversation_id uuid Link the ticket to this conversation.

Response data: { ticket: Ticket }

POST /v1/tickets/{ticket_id}/move

Move a ticket: won = resolved, lost = closed; back to an open stage clears both.

Scope: messages · Module: Help Desk · MCP tool: move_ticket

Path parameter Type Description
ticket_id uuid The ticket.
Body field Type Description
stage_key string (required) Ticket stage key.

Response data: { ticket: Ticket }

POST /v1/tickets/{ticket_id}/assign

Assign a ticket to a member, or unassign it.

Scope: messages · Module: Help Desk · MCP tool: assign_ticket

Path parameter Type Description
ticket_id uuid The ticket.
Body field Type Description
assignee_user_id uuid Member id (GET /v1/members).
unassign boolean Leave the ticket unassigned.

Response data: { ticket: Ticket }

6. Objects

Every object has all its fields present (null rather than absent); snake_case throughout.

Connectionid, phone, verified_name, business, status (active accepts sends), registered, flow (classic · coexistence), quality_rating, health_checked_at, templates_synced_at, connected_at.

Conversation

{
  "id": "3fbb9c0a-0349-4363-a3ff-da21b273be0c",
  "connection_id": "f8a7d953-5b66-4cd8-b4e3-03168aa7add2",
  "contact_id": "79db5fd9-1fe5-4f9d-bbe2-6988253b1e71",
  "contact": { "name": "Maria Souza", "phone": "5511988887777", "wa_id": "5511988887777", "opted_out": false },
  "unread_count": 1,
  "last_message": { "preview": "Meu pedido já saiu?", "type": "text", "at": "2026-09-14T18:22:31.120Z", "direction": "inbound" },
  "window": { "open": true, "expires_at": "2026-09-15T18:22:31.120Z", "can_send_free_text": true, "last_inbound_at": "2026-09-14T18:22:31.120Z" },
  "created_at": "2026-09-01T12:00:00.000Z"
}

Message

{
  "id": "0d8f6c1e-8b7a-4c1e-9f5a-2c1d3e4f5a6b",
  "wamid": "wamid.HBgNNTUxMTk4ODg4Nzc3NxUCABIYFjNFQjBDMEI4",
  "conversation_id": "3fbb9c0a-0349-4363-a3ff-da21b273be0c",
  "direction": "inbound",
  "type": "text",
  "status": "received",
  "text": "Meu pedido já saiu?",
  "created_at": "2026-09-14T18:22:31.120Z",
  "author": "Maria Souza"
}

status: sending · sent · delivered · read · failed (outbound) · received (inbound). Optional fields: author (inbound), sent_via (user | api_key, outbound), reply_to_wamid, reactions, template, media ({ mime, bytes, status, available, filename } — download with GET /v1/messages/{message_id}/media), location, contacts, interactive, button, order (Meta's shapes), error ({ code, message } on failed sends).

Templateid, name, language, status, category, header ({ format, text, variables } or null), body ({ text, variables }), footer, buttons, components (with include_components).

Contactid, kind (lead · client), name, name_source, phone, wa_id, email, company, tags, stage ({ key, name, kind }, leads), client_status, owner_user_id, source, stage_changed_at, converted_at, created_via/updated_via (user | api_key), created_at, updated_at.

Opportunityid, contact_id, title, value_cents, currency, stage, owner_user_id, expected_close_on, won_at, lost_at, lost_reason, is_open, stage_changed_at, created_via, updated_via, created_at, updated_at.

Ticketid, number, contact_id, conversation_id, subject, description, stage, priority, assignee_user_id, resolved_at, closed_at, is_open, stage_changed_at, created_via, updated_via, created_at, updated_at.

Noteid, contact_id, opportunity_id, ticket_id, body, author_via, author_user_id, created_at. Stagekey, name, entity, kind (open · won · lost), position, is_default.

7. Webhooks

Optional. Instead of polling, an organization admin registers an HTTPS endpoint on Integrations → Webhooks and Nivox POSTs signed events to it.

7.1 Setup

  1. Integrations → Webhooks → New webhook: the public https:// URL (no IP addresses, no localhost or internal domains, no credentials in the URL), a description, the events, and optionally which WhatsApp connections.
  2. Copy the signing secret (whsec_…) — shown once; New secret rotates it.
  3. Send test delivers a webhook.test event right away and shows the HTTP status. Pause stops new events for the endpoint; Show deliveries lists the last 14 days, with Redeliver.

7.2 Events

Event Fires when
message.received A customer message arrived and was stored (media mirrored when possible).
message.sent An outbound message was accepted by Meta — from the inbox, the REST API or an MCP agent.
message.status An outbound message became delivered, read or failed.
webhook.test Sent by Send test only.

Reactions, template status changes and CRM/Help Desk events are not webhooks in v1.

7.3 Payload

{
  "id": "5a0f4d8e-2f7b-4f6c-9d0e-1b2c3d4e5f60",
  "type": "message.received",
  "created_at": "2026-09-14T18:22:31.180Z",
  "api_version": "v1",
  "org_id": "0b8e1c7a-5d2f-4e39-9a61-3c4d5e6f7a8b",
  "data": {
    "connection": { "id": "f8a7d953-5b66-4cd8-b4e3-03168aa7add2", "phone": "+55 11 99999-0000" },
    "conversation": { "id": "3fbb9c0a-0349-4363-a3ff-da21b273be0c", "contact_id": "79db5fd9-1fe5-4f9d-bbe2-6988253b1e71", "…": "the Conversation object" },
    "message": { "id": "0d8f6c1e-8b7a-4c1e-9f5a-2c1d3e4f5a6b", "direction": "inbound", "type": "text", "status": "received", "text": "Meu pedido já saiu?", "…": "the Message object" }
  }
}

data.conversation and data.message are exactly the objects of §6. message.sent has the same data. message.status adds data.status (delivered · read · failed) and data.error ({ code, message } for failed, else null), and data.message.status is the status that fired.

7.4 Headers

Header Value
webhook-id The event id (= payload id), identical on every retry — deduplicate on it.
webhook-timestamp Unix seconds when this attempt was signed.
webhook-signature v1,<base64 HMAC-SHA256> (space-separated list).
X-Nivox-Event The event type.
X-Nivox-Delivery The delivery id (one per endpoint and event).
X-Nivox-Attempt 1, 2, 3…
User-Agent Nivox-Webhooks/1.0

7.5 Verifying the signature

Nivox signs with Standard Webhooks: the signature is base64(HMAC-SHA256(key, "{webhook-id}.{webhook-timestamp}.{raw body}")), where key is the base64-decoded part of the secret after whsec_. Verify the raw bytes you received (not re-serialized JSON) and refuse timestamps more than 5 minutes away. Libraries that do this: standardwebhooks (npm, PyPI, Go, Ruby…) and Svix.

import crypto from 'node:crypto';

export function verifyNivoxWebhook(rawBody, headers, secret) {
  const id = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signatures = headers['webhook-signature'] ?? '';
  if (!id || !timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const expected = crypto.createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest('base64');
  return signatures.split(' ').some((entry) => {
    const [version, signature] = entry.split(',');
    return version === 'v1' && signature?.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  });
}
import base64, hashlib, hmac, time

def verify_nivox_webhook(raw_body: bytes, headers, secret: str) -> bool:
    msg_id, ts = headers.get("webhook-id"), headers.get("webhook-timestamp")
    if not msg_id or not ts or abs(time.time() - int(ts)) > 300:
        return False
    key = base64.b64decode(secret.removeprefix("whsec_"))
    expected = base64.b64encode(hmac.new(key, f"{msg_id}.{ts}.".encode() + raw_body, hashlib.sha256).digest()).decode()
    return any(hmac.compare_digest(e.split(",", 1)[1], expected)
               for e in headers.get("webhook-signature", "").split() if e.startswith("v1,"))

7.6 Delivery, retries and ordering

8. Troubleshooting

Symptom Cause / fix
401 unauthorized Wrong header, revoked or expired key, or a session/JWT token instead of nvx_….
403 forbidden on sends A read key. Ask an admin for messages (or full for new numbers and templates).
403 module_disabled CRM / Help Desk off for the organization; a Nivox operator enables it.
404 not_found for an id you just listed The key is limited to other connections, or the id is another organization's.
404 route_not_found Method or path typo — paths start with /v1; compare with openapi.json.
409 session_expired The 24-hour window closed: send a template; free text resumes when the customer replies.
502 graph_error Meta refused: read error.details (template parameters, number not on WhatsApp, payment method…).
A retry sent the message twice Send an Idempotency-Key with every POST.
Webhook never arrives Endpoint paused or disabled, URL not public https, a redirect, a non-2xx answer or > 10 s — check Show deliveries and Send test.
Signature does not match Verify the raw request bytes with the current secret (rotating invalidates the old one).

9. Changelog