Skip to content
Nigora Zap API

    Sign in Create account

    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.

    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

    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:

    Terminal window
    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.

    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.

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

    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>

    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 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.

    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.

    Turn on opt-out in PATCH /settings:

    Terminal window
    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-outs lists the opted-out contacts (with limit and before for paging).
    • DELETE /opt-outs/{phone} lets you send to a phone number again. It responds 204, even if the number was not on the list.