---
title: "Webhooks"
description: "Every messaging API tells your system when something happens: a message received, a delivery confirmed, a session dropped, a QR code scanned. You register a URL, we send a POST — and the polling loop disappears from your code."
lang: en
canonical: https://doc.apibrasil.io/webhooks
markdown: https://doc.apibrasil.io/webhooks.md
source: apibrasil-documentation
---

# Webhooks

> Every messaging API tells your system when something happens: a message received, a delivery confirmed, a session dropped, a QR code scanned. You register a URL, we send a POST — and the polling loop disappears from your code.

## Channels

Each channel has its own events, its own envelope and its own pitfalls — the SMS body is an array, the WhatsApp Wpp one is an object with an HMAC signature, and the Baileys one does not even say which event it is. Open the one you use.

| Channel | Events | Destination URLs | Summary | Page |
| --- | --- | --- | --- | --- |
| SMS | 4 | 1 | Notifications for each message's journey — and, when someone replies, the text of that reply. | [SMS webhook](https://doc.apibrasil.io/webhooks/api-sms.md) |
| WhatsApp Wpp | 19 | 4 | Messages coming in and going out, read receipts, calls, and the session lifecycle. | [WhatsApp Wpp webhook](https://doc.apibrasil.io/webhooks/api-whatsapp-wpp.md) |
| WhatsApp Baileys | 6 | 1 | Messages, read receipts, contacts and the connection lifecycle — in an envelope that never says which event it is. | [WhatsApp Baileys webhook](https://doc.apibrasil.io/webhooks/api-whatsapp-baileys.md) |
| WhatsApp WhatsMeow | 18 | 1 | Messages, read receipts, calls, groups and connection — with the event name included, and a filter to choose what arrives. | [WhatsApp WhatsMeow webhook](https://doc.apibrasil.io/webhooks/api-whatsapp-whatsmeow.md) |

## Asynchronous sending

> The messaging APIs accept the /queue suffix on the route. With it the gateway does not wait for the send to happen: it records the request, returns an id immediately and publishes to a queue. It is for batch dispatch, where holding a connection open per send is what brings your process down.

### The whole round trip

1. You call the SAME route as the synchronous send, with /queue on the end.
2. The response comes back immediately with an id — not with the send's result.
3. You track the id at /jobs, and the send's result arrives through the device's events.

### The engines that accept a queue

The asynchronous route is the synchronous one with /queue on the end, and the first three go through the same authentication and billing stack as the synchronous route — Bearer, DeviceToken and the charge all happen at enqueue time, not at delivery.

| Engine | Route | Queue | Webhook channel |
| --- | --- | --- | --- |
| WhatsApp Wpp — The WPPConnect engine. It is the only one whose webhook channel has four separate URLs. | `POST /whatsapp/{action}/queue` | yes | [api-whatsapp-wpp](https://doc.apibrasil.io/webhooks/api-whatsapp-wpp.md) |
| WhatsApp Baileys — The Evolution engine. The route's {controller} is the same as the synchronous send's — message, chat, group. | `POST /evolution/{controller}/{action}/queue` | yes | [api-whatsapp-baileys](https://doc.apibrasil.io/webhooks/api-whatsapp-baileys.md) |
| WhatsApp WhatsMeow — The whatsmeow engine. The route accepts slashes in {action}, so compound paths pass through whole. | `POST /whatsmeow/{action}/queue` | yes | [api-whatsapp-whatsmeow](https://doc.apibrasil.io/webhooks/api-whatsapp-whatsmeow.md) |
| Official WhatsApp (WABA and CoEx) — The route EXISTS and refuses at runtime, with queue_not_available and instructions to use the synchronous route. That is worse than not existing: it passes an integrator's smoke test and fails on the first real send. | `POST /waba/{action}/queue · POST /coex/{action}/queue` | no: the route exists and refuses | — |

### What the queue returns

An identifier and a confirmation that the request went in. There is no send result in this response, and there cannot be: the send has not happened yet.

```json
{
  "id": "9f1c8e2a-4b7d-4e61-9a3c-2f5d8e0b1c74",
  "message": "Dados enviados para a fila com sucesso"
}
```

### Track it by id

Two routes, and neither asks for a DeviceToken — they sit in a different middleware group from the send, with Bearer only. That is what lets a tracking dashboard know nothing about devices.

- **Base address:** `https://gateway.apibrasil.io/api/v2`

- `GET /jobs` — **List your jobs**: Returns Laravel's standard pagination with job_id, status, created_at and updated_at, restricted to your account.
- `POST /jobs` — **Look up by id**: The body is an array of uuids under id. Each one must exist in job_statuses: a missing id fails the whole request with a 422 “Job não encontrado”, and the others are not returned.

**Query body**

```json
{
  "id": [
    "9f1c8e2a-4b7d-4e61-9a3c-2f5d8e0b1c74"
  ]
}
```

**Query response**

```json
[
  {
    "id": "9f1c8e2a-4b7d-4e61-9a3c-2f5d8e0b1c74",
    "status": "queued",
    "created_at": "28/08/2026 14:28:50",
    "processed_at": "",
    "diff_in_minutes": ""
  }
]
```

### What comes back for each job

| Name | Type | Meaning |
| --- | --- | --- |
| `id` | string (uuid) | The uuid the enqueue call returned. |
| `status` | string | The job's state. At enqueue time it starts as queued — see the open questions below. |
| `created_at` | string | When the request entered the queue, as DD/MM/YYYY HH:mm:ss. |
| `processed_at` | string | When it was processed, in the same format. An EMPTY STRING while it has not been — not null. |
| `diff_in_minutes` | number \| string | Minutes between entering the queue and being processed. An empty string while the job has not been processed. |

### What usually trips people up

- **The response is not the send's result** — What comes back is a queue id. Treating that response as “message sent” makes your system mark as delivered something that has not even left yet. The send's result shows up in the device's events — SEND_MESSAGE and MESSAGE_STATUS on Wpp — which is what the channel's page documents.
- **Empty is not null** — processed_at and diff_in_minutes come back as an EMPTY STRING when the job has not been processed, not as null. Testing === null never matches; testing for a falsy value does.
- **One bad id fails the whole lookup** — POST /jobs validates every item of id against job_statuses. A uuid that does not exist returns 422 and brings down the entire request — the other ids in the same array are not looked up. In bulk, query in small batches.
- **The charge happens at enqueue time** — The /queue routes go through the SAME middleware stack as the synchronous ones, including billing and quota. The charge is made when the request enters the queue, not when the message goes out.

### What this page does not claim

The gateway records the job as queued and publishes it to the queue. Whatever consumes that queue is not in any available repository, so what follows has not been verified — it is written as an open question instead of being filled in with a plausible-sounding name.

- **Which values status takes besides queued** — The gateway inserts queued and never writes to that row again. No available code updates job_statuses, so the remaining states — if any — come from the queue's consumer.
- **When processed_at gets filled in** — The field is read by the lookup route and is not written anywhere we can read. Until that is confirmed, treat its absence as “I don't know yet”, not as “it has not been processed yet”.

## See also

- Documentation index: https://doc.apibrasil.io/llms.txt
- HTML version of this page: https://doc.apibrasil.io/webhooks
