Webhooks
Webhooks are POST calls that the API makes to an address in your system when something happens: a message arrived, a message was sent, the number connected or disconnected.
Events
Section titled “Events”| Event | When it happens |
|---|---|
message.received |
A message arrived |
message.sent |
A queued message was sent |
message.status |
A message’s status changed (includes send failures) |
presence |
A contact’s presence changed, such as “typing”. The body carries chatId and presence |
instance.connected |
The number connected. The body carries phone |
instance.disconnected |
The number disconnected. The body carries reason and loggedOut |
contact.opted_out |
A contact asked to stop receiving messages. The body carries phone |
Configure
Section titled “Configure”PUT /v1/instances/{instanceId}/webhooks replaces the whole configuration. Always send the complete set. Each event has its own URL and can be turned on or off:
curl -X PUT https://api.zap.nigora.com.br/v1/instances/INSTANCIA_ID/webhooks \ -H "Authorization: Bearer TOKEN" \ -H "Content-Type: application/json" \ -d '{ "events": { "message.received": { "url": "https://meusistema.com.br/hooks/recebida", "enabled": true }, "message.status": { "url": "https://meusistema.com.br/hooks/status", "enabled": true } }, "notifySentByMe": false, "receiveFilters": { "ignoreGroups": true } }'The response (200) carries the saved configuration and the secret, the key used for signing.
URLs can be up to 2,048 characters and must be public. Internal network and loopback addresses (such as localhost) are rejected with 400 and url_not_allowed. Fields that do not follow the format above also return 400 (validation_error). Events with no URL, or with enabled: false, do not trigger calls.
Receive filters
Section titled “Receive filters”In receiveFilters, each field is false by default. Turn one on to not receive that kind of message:
ignoreGroups, ignorePrivate, ignoreText, ignoreImage, ignoreVideo, ignoreAudio, ignoreDocument.
With notifySentByMe on, you also receive message.received for messages you sent yourself from the phone.
To read the current configuration, use GET /webhooks. The response includes the signing secret.
What the API sends
Section titled “What the API sends”The body is always JSON, in this format:
{ "event": "message.received", "instanceId": "…", "timestamp": "2026-11-02T14:30:00.000Z", "data": { }}data changes depending on the event. In message.received:
| Field | Description |
|---|---|
waMessageId |
The message’s identifier on WhatsApp |
chatId |
The conversation, such as 5511999999999@s.whatsapp.net (groups end in @g.us) |
from |
Who sent it, in the same format |
fromMe |
true if it was sent by the number itself |
isGroup |
true if it came from a group |
timestamp |
The message’s time, in seconds since 1970 |
type |
text, image, video, audio, document or other |
text |
Text, or null |
quotedMessageId |
The message being replied to, or null |
pushName |
The sender’s display name, or null |
media |
For media: url, mimeType, fileName and expiresAt |
mediaSkipped |
true if the media could not be downloaded or was too large |
In message.sent, data carries id (the one returned in the send’s 202), waMessageId and type. In message.status, it carries id, waMessageId and status (sent, delivered, read or failed); when sending fails, it also carries error (see Sending messages).
Headers
Section titled “Headers”| Header | Content |
|---|---|
x-nigora-event |
Event name |
x-nigora-delivery |
Identifier of this delivery |
x-nigora-signature |
Signature of the body, in the format sha256=<hex> |
Verify the signature
Section titled “Verify the signature”Before trusting a call, check its signature. It is the HMAC-SHA256 of the exact bytes of the received body, computed with the instance’s secret (the secret from GET /webhooks) and written in hexadecimal, after the sha256= prefix.
import { createHmac, timingSafeEqual } from 'node:crypto'
// `corpoBruto` is the received body, not passed through JSON.parse and re-serialized.function assinaturaValida(corpoBruto, cabecalho, segredo) { const esperada = 'sha256=' + createHmac('sha256', segredo).update(corpoBruto).digest('hex') const a = Buffer.from(esperada) const b = Buffer.from(cabecalho ?? '') return a.length === b.length && timingSafeEqual(a, b)}<?php$corpo = file_get_contents('php://input');$esperada = 'sha256=' . hash_hmac('sha256', $corpo, $segredo);if (!hash_equals($esperada, $_SERVER['HTTP_X_NIGORA_SIGNATURE'] ?? '')) { http_response_code(401); exit;}To rotate the secret, call POST /webhooks/secret/regenerate. The response is { "secret": "..." } and the old one stops working. Retries and resends made after the change are already signed with the new secret.
Respond fast, with a 2xx
Section titled “Respond fast, with a 2xx”Respond with any 2xx status to acknowledge, within 10 seconds. Any other response, or no response, counts as a failure and the API tries again, with a growing wait between attempts (the first wait is 5 seconds and it doubles with each failure), up to 8 attempts in total. After that, the delivery is marked failed.
Do the heavy processing after responding. Acknowledge first and work afterwards, preferably in a queue of your own.
Because resends can happen, use the x-nigora-delivery header to ignore a delivery you have already processed.
History and resending
Section titled “History and resending”The API keeps deliveries from the last 7 days.
| Operation | Route |
|---|---|
| List deliveries, from newest to oldest | GET /webhooks/deliveries (parameters limit, from 1 to 100, and before, an ISO 8601 date with time zone, for paging) |
| View a delivery: event, status, attempts and content | GET /webhooks/deliveries/{id} |
| Resend a delivery | POST /webhooks/deliveries/{id}/resend |
The list comes in { "deliveries": [...] }. Each delivery carries id, event, url, status (pending, success or failed), attempts, lastStatusCode, lastResponse, createdAt and updatedAt. Fetching a single delivery adds the payload.
Resending responds 202 with { "id": "..." }, the identifier of a new delivery with the same content, to the same URL. An id that does not exist in this instance returns 404 with webhook_delivery_not_found.
The history is useful when your system was down: list the failed deliveries and resend them.
Contact opt-out
Section titled “Contact opt-out”Turn on opt-out in PATCH /settings:
curl -X PATCH https://api.zap.nigora.com.br/v1/instances/INSTANCIA_ID/settings \ -H "Authorization: Bearer TOKEN" \ -H "Content-Type: application/json" \ -d '{ "optOut": { "enabled": true, "keywords": ["SAIR", "PARAR"] } }'When a contact sends, in a private chat, a message that is just one of those words (ignoring case and accents), they are added to the opt-out list and you receive the contact.opted_out event. New sends to them are rejected with 422 (contact_opted_out). The default word is SAIR.
GET /opt-outslists the opted-out contacts (withlimitandbeforefor paging).DELETE /opt-outs/{phone}lets you send to a phone number again. It responds204, even if the number was not on the list.