Skip to main content
A webhook is the reverse path of the API: instead of you querying Nuvia, Nuvia sends a POST to your server whenever something happens in your operation: a message arrives, a contact is created, a field is filled in by the agent.
Each company has at most one webhook. It’s not a collection: you configure one URL and choose which events it receives. Trying to create a second one returns 409.

Configure

POST /v1/webhooks with the URL and the list of events. Required scope: webhooks:create.

What Nuvia sends

Every delivery is a POST with these headers: And the body always has the same three-field structure:
  • event: the event that triggered the delivery.
  • timestamp: ISO 8601 UTC, generated when the delivery was queued.
  • data: the resource related to the event (see the table below).
The body doesn’t identify the company: there’s no companyId in the envelope. If your server serves multiple Nuvia companies, use one URL per company or a distinct value in custom_headers to know whose event it is.

Available events

For message, conversation, and contact events, data carries the resource in the same format the API reference returns on the corresponding GET requests, for example, GET /v1/messages/conversation/{id} for messages. The exceptions are FIELD_UPDATED, MESSAGE_FAILED, and CONVERSATION_STEP_CHANGED, detailed below.
Treat data tolerantly: read the fields you use and ignore the rest. New fields may appear as the platform evolves, and that isn’t considered a breaking change.

FIELD_UPDATED is different

This event doesn’t return an entity, but a description of the change:
field.context indicates what the field belongs to: contact, conversation, or business. value has the field’s type: string, number, boolean, or list. conversationId only comes through when the change happened inside a conversation.

MESSAGE_FAILED covers delivery failure, not send failure

The event fires when WhatsApp returns a failure status for a message that already left Nuvia. It applies to the WhatsApp Cloud API channel only.
These cases generate no event at all, even though the message ends up with status: FAILED on the platform:
  • an error at send time, when the request to WhatsApp fails
  • a send blocked by Nuvia before it goes out, such as an expired 24-hour window
  • failures on channels other than WhatsApp Cloud API
If your integration needs to know that the message never went out, MESSAGE_FAILED does not cover that case. Check the message status in the API reference.
The body carries the message with its updated status and a failure_reason key with the cause:
code is the WhatsApp error code, for example 131026 for a number that does not use WhatsApp and 131047 for an expired 24-hour window. message is display-ready text, the same one the operator sees on the conversation card, and it is written in Portuguese, as the example above shows. Use code for your integration logic and message only for display. The text may change without notice, the code will not.
Read the reason from failure_reason, never from the message’s errors field. In the webhook body, errors arrives out of date and almost always empty, because the event is assembled before the platform writes the errors. The populated errors shows up in the message GET.failure_reason is optional: when WhatsApp does not report the cause, the key does not come through. Handle its absence without breaking.

CONVERSATION_STEP_CHANGED delivers the change, not the conversation

The event fires when the conversation moves to another pipeline step, whether from the dashboard, the AI agent, or the API. It only fires on an actual step change.
previousStep comes as null when the conversation was not in any step yet. origin indicates what caused the change and may not come through.
This event does not replace CONVERSATION_UPDATED. When the step changes through a PATCH on the conversation, whoever subscribes to both receives both in the same request. Through the dedicated step change, only CONVERSATION_STEP_CHANGED goes out.
A campaign step can be configured to move the conversation to a funnel step at the moment of the send. In that case the step changes without the lead having replied. Don’t use the step change, on its own, as a sign of the lead’s interest.

Who sent the message

Message events carry two fields that, together, tell where the message came from: Messages sent from the WhatsApp Business app in coexistence arrive through the MESSAGE_RECEIVED event, not MESSAGE_SENT. So, to detect the lead’s reply, filter by sender_type: CONTACT and message_type: INCOMING, not just by the event name. If your integration sends messages through the API and needs to tell them apart from the AI’s messages, store the IDs of the messages it sent itself.

Validate that the call came from Nuvia

There’s no HMAC signature. Nuvia doesn’t sign the request body, so there’s no way to cryptographically verify the origin.
The available mechanism is custom_headers: register a secret value and check it on every request.
Node.js
Use HTTPS and a long, random value. Since the secret travels in a header on every delivery, treat it as a credential: rotate it with PUT /v1/webhooks/{id} if you suspect a leak.

Delivery guarantees

These are the actual characteristics of delivery. Read this before building anything that depends on a webhook:
If your server is down, returns 5xx, or times out, the event is lost. The failure is logged on Nuvia’s side, but nothing is resent. For critical data, reconcile periodically with the API (for example, by listing a conversation’s messages) instead of relying solely on the webhook.
Nuvia waits at most 10 seconds for your response. Respond 2xx immediately and process in the background. If you process before responding, legitimate deliveries turn into timeouts.
Status events come from WhatsApp, which may deliver them out of order (a MESSAGE_DELIVERED arriving after the MESSAGE_READ for the same message). Nuvia doesn’t reorder them. Treat each event by the state it carries, without assuming sequence.
MESSAGE_DELIVERED and MESSAGE_READ are only sent when the status actually changes. WhatsApp resends the same status multiple times, and those repeats are discarded: you don’t get a duplicate because of it. Still, write an idempotent handler: use the message identifier as the key.
With status: "INACTIVE", or for events you didn’t include in events, Nuvia doesn’t queue any delivery, and there’s no history to recover afterward. Reactivating doesn’t recover the time it was stopped.

Manage the configuration

The GET returns a single object with the company’s configuration, or null if there is none. It’s not a list. The id used in PUT and DELETE comes from that object. After removal, the company can create a new webhook again. To pause temporarily without losing the configuration, prefer PUT with status: "INACTIVE" instead of deleting.

Next steps

Integration examples

The webhook inside a complete use case, with an automatic reply.

API reference

Parameters and responses for the webhook endpoints.