---
title: "Webhook de WhatsApp Baileys"
description: "Mensagens, confirmações de leitura, contatos e o ciclo da conexão — num envelope que não diz qual evento é."
lang: pt-BR
canonical: https://doc.apibrasil.io/webhooks/api-whatsapp-baileys
markdown: https://doc.apibrasil.io/webhooks/api-whatsapp-baileys.md
source: apibrasil-documentation
---

# Webhook de WhatsApp Baileys

> Mensagens, confirmações de leitura, contatos e o ciclo da conexão — num envelope que não diz qual evento é.

O Baileys avisa tudo o que acontece no seu número, mas o aviso não vem etiquetado: o tipo do evento se perde no repasse e você o descobre pelo formato do data. Esta página é a tabela que faz essa leitura.

## Como receber

Tudo chega numa URL só, registrada em webhook_wh_message no dispositivo. O POST vem com User-Agent APIBRASIL/2.0 e dois cabeçalhos próprios — device e version —, e sem assinatura. O receptor abaixo é a tabela de reconhecimento em código: ele descobre o tipo pelo formato, na ordem que evita falso positivo.

**Onde registrar a URL e o que cai nela**

| Campo | O que recebe | Eventos |
| --- | --- | --- |
| `webhook_wh_message` | TODOS os eventos do canal, na mesma URL — mensagens, status, contatos, QR Code e conexão. | todos os eventos do canal |

Os eventos deste canal são independentes: não há ordem entre eles.

## Eventos (6)

### `messages.upsert` — Mensagem recebida

**Quando dispara:** Quando alguém manda mensagem para o seu número. RECONHECE-SE por data.key e data.message existirem, com data.key.fromMe igual a false.

**Campos**

| Nome | Tipo | Significado |
| --- | --- | --- |
| `key.remoteJid` | string | Quem mandou, COM o sufixo @s.whatsapp.net. Diferente do Wpp, aqui não é cortado. |
| `key.fromMe` | boolean | false aqui. É o que separa recebida de enviada. |
| `key.id` | string | O id da mensagem no WhatsApp. |
| `pushName` | string | O nome que a pessoa exibe no WhatsApp. |
| `message` | object | O conteúdo. Em texto simples vem como message.conversation; outros tipos usam outras chaves. |
| `messageType` | string | O tipo, como o Baileys o nomeia: "conversation" para texto simples. |
| `messageTimestamp` | number | Unix, em segundos. |
| `instanceId` | string | O UUID da instância na Evolution. |

**Corpo de exemplo, como chega**

```json
[
  {
    "session": "<device_token>",
    "channelName": "<sua conta>",
    "data": {
      "key": {
        "remoteJid": "5531994359434@s.whatsapp.net",
        "fromMe": false,
        "id": "3EB0C767D26B8B5A1C2F"
      },
      "pushName": "João da Silva",
      "message": {
        "conversation": "Olá! Gostaria de saber mais sobre a API."
      },
      "messageType": "conversation",
      "messageTimestamp": 1751551282,
      "instanceId": "802fdb80-8c48-4c76-94d3-2118f3d3a69b"
    }
  }
]
```

### `send.message` — Mensagem enviada

**Quando dispara:** Quando o seu número envia — pela API ou pelo aparelho. MESMO FORMATO da recebida; o que muda é data.key.fromMe igual a true.

**Campos**

| Nome | Tipo | Significado |
| --- | --- | --- |
| `key.remoteJid` | string | Para quem foi, com o sufixo. |
| `key.fromMe` | boolean | true aqui. É a ÚNICA diferença de formato para a mensagem recebida. |
| `key.id` | string | O id da mensagem. |
| `message` | object | O conteúdo enviado. |
| `messageTimestamp` | number | Unix, em segundos. |

**Corpo de exemplo, como chega**

```json
[
  {
    "session": "<device_token>",
    "channelName": "<sua conta>",
    "data": {
      "key": {
        "remoteJid": "5531994359434@s.whatsapp.net",
        "fromMe": true,
        "id": "3F6ED9C4DD811BFC2FD9"
      },
      "message": {
        "conversation": "Claro! Já vou gerar para você."
      },
      "messageType": "conversation",
      "messageTimestamp": 1751551340,
      "instanceId": "802fdb80-8c48-4c76-94d3-2118f3d3a69b"
    }
  }
]
```

### `messages.update` — Entregue ou lida

**Quando dispara:** Quando uma mensagem sua muda de estado. RECONHECE-SE por data.status junto de data.messageId — e pela AUSÊNCIA de data.message, que é o que o separa de uma mensagem.

**Campos**

| Nome | Tipo | Significado |
| --- | --- | --- |
| `messageId` | string | O id interno da Evolution — NÃO é o mesmo id que veio em key.id na mensagem. |
| `keyId` | string | ESTE é o que casa com o key.id da mensagem original. É por ele que se junta. |
| `remoteJid` | string | A conversa, com sufixo. |
| `fromMe` | boolean | Aqui no nível raiz de data, não dentro de key. |
| `participant` | string | Em grupo, de quem é o estado. |
| `status` | string | O estado, em maiúsculas: DELIVERY_ACK, READ, SERVER_ACK, PLAYED. |
| `instanceId` | string | O UUID da instância. |

**Corpo de exemplo, como chega**

```json
[
  {
    "session": "<device_token>",
    "channelName": "<sua conta>",
    "data": {
      "messageId": "cmb6unurm0pm4ps5xjinc8383",
      "keyId": "3F6ED9C4DD811BFC2FD9",
      "remoteJid": "553194359434:9@s.whatsapp.net",
      "fromMe": true,
      "participant": "553194359434:9@s.whatsapp.net",
      "status": "DELIVERY_ACK",
      "instanceId": "802fdb80-8c48-4c76-94d3-2118f3d3a69b"
    }
  }
]
```

### `qrcode.updated` — QR Code para parear

**Quando dispara:** Quando a instância precisa ser autenticada. RECONHECE-SE por data.qrcode existir — é a checagem mais simples do canal.

**Campos**

| Nome | Tipo | Significado |
| --- | --- | --- |
| `qrcode.instance` | string | O nome da instância. |
| `qrcode.pairingCode` | string \| null | O código de pareamento, quando pedido. Nulo no fluxo por QR. |
| `qrcode.code` | string | O conteúdo do QR, para gerar a imagem você mesmo. |
| `qrcode.base64` | string | A imagem pronta, como data URL PNG. |

**Corpo de exemplo, como chega**

```json
[
  {
    "session": "<device_token>",
    "channelName": "<sua conta>",
    "data": {
      "qrcode": {
        "instance": "apibrasilbaileys",
        "pairingCode": null,
        "code": "2@uze42H2e6c…",
        "base64": "data:image/png;base64,…"
      }
    }
  }
]
```

### `connection.update` — A conexão mudou de estado

**Quando dispara:** A cada mudança da conexão: connecting, open, close. RECONHECE-SE por data.state ser uma string. Quando o estado é open, o aviso traz também wuid e a foto do perfil — nos outros, só state e statusReason.

**Campos**

| Nome | Tipo | Significado |
| --- | --- | --- |
| `instance` | string | O nome da instância. |
| `state` | string | connecting, open ou close. É o campo que decide se você precisa reconectar. |
| `statusReason` | number | O código do motivo. 200 quando é normal. |
| `wuid` | string | Só quando state é open: o número que acabou de conectar, com sufixo. |
| `profilePictureUrl` | string | Só quando state é open. URL da foto de perfil, temporária. |

**Corpo de exemplo, como chega**

```json
[
  {
    "session": "<device_token>",
    "channelName": "<sua conta>",
    "data": {
      "instance": "apibrasilbaileys",
      "wuid": "553171599698@s.whatsapp.net",
      "profilePictureUrl": "https://pps.whatsapp.net/v/t61.24694-24/…",
      "state": "open",
      "statusReason": 200
    }
  }
]
```

### `contacts.upsert` — Contatos sincronizados

**Quando dispara:** Quando a instância sincroniza a agenda — normalmente logo depois de conectar. É o ÚNICO evento em que data é um ARRAY, e é por isso que a verificação de array vem primeiro na tabela.

**Campos**

| Nome | Tipo | Significado |
| --- | --- | --- |
| `[].remoteJid` | string | O contato, com sufixo @s.whatsapp.net. |
| `[].pushName` | string | O nome que o contato exibe. |
| `[].profilePicUrl` | string \| null | A foto, quando visível. |
| `[].instanceId` | string | O UUID da instância. |

**Corpo de exemplo, como chega**

```json
[
  {
    "session": "<device_token>",
    "channelName": "<sua conta>",
    "data": [
      {
        "remoteJid": "5515996139173@s.whatsapp.net",
        "pushName": "Camillo Alex",
        "profilePicUrl": null,
        "instanceId": "802fdb80-8c48-4c76-94d3-2118f3d3a69b"
      },
      {
        "remoteJid": "553187840265@s.whatsapp.net",
        "pushName": "Marcos Noronha",
        "profilePicUrl": null,
        "instanceId": "802fdb80-8c48-4c76-94d3-2118f3d3a69b"
      }
    ]
  }
]
```

## O que costuma pegar

- **Não existe campo de tipo. O tipo é o formato.** — O corpo é { session, channelName, data } e mais nada. A Evolution envia o nome do evento em event, mas o repasse da APIBrasil entrega só o miolo de data. Não há como rotear por um campo — é preciso reconhecer o formato, na ordem da tabela de eventos.
- **session é o token do dispositivo; channelName é a sua conta** — Os nomes enganam. No repasse, session recebe o device_token e channelName recebe o identificador da sua conta. Nenhum dos dois é o nome da instância — esse fica em data.instance, e só em alguns eventos.
- **data às vezes é um array** — Em contatos, data é uma LISTA de contatos, não um objeto. Ler data.remoteJid devolve undefined; o caminho é data[0].remoteJid. É a primeira checagem que o seu roteador precisa fazer, antes de qualquer acesso a propriedade.
- **Mensagem recebida e mensagem enviada têm o MESMO formato** — As duas chegam com data.key e data.message. O que as separa é data.key.fromMe — true é sua, false é do contato. Sem essa checagem, o seu sistema responde às próprias mensagens.
- **Um destino só, e ele se chama webhook_wh_message** — Diferente do WhatsApp Wpp, que tem quatro URLs, aqui tudo cai numa. O nome do campo sugere mensagens, mas por ele passam também QR Code, conexão e contatos.
- **Sem assinatura. A identificação vem em cabeçalhos.** — Não há HMAC neste canal — o Wpp tem, este não. O POST chega com User-Agent APIBRASIL/2.0, Origin https://webhook.apibrasil.cloud e dois cabeçalhos próprios: device, com o token do dispositivo, e version, com a versão do servidor. É por eles que se confere a origem.

## Boas práticas

- **A ORDEM das checagens não é opcional** — Array primeiro, senão contatos quebra qualquer acesso a propriedade. Depois qrcode e state, que são inequívocos. Só então status (que exige a ausência de message) e por último key/message. Inverter as duas últimas classifica um ACK como mensagem.
- **Trate o desconhecido, não o descarte** — A Evolution publica 33 eventos e só seis formatos são conhecidos aqui. Um formato novo cai no ramo final — registre-o em vez de ignorar, senão a primeira funcionalidade nova do canal chega como silêncio.
- **Responda 200, e em menos de 30 segundos** — Qualquer status diferente de 200 é registrado como falha e gera notificação. O cliente HTTP do repasse espera 120s, mas o job que o executa é morto aos 30 — na prática, o seu limite é 30 segundos.
- **São duas tentativas, não três** — O job de repasse declara tries = 2. Se a segunda falhar, o aviso é perdido — não há reentrega depois disso.

## Receptores de exemplo

**Node**

```node
app.post("/webhook/baileys", express.json(), (req, res) => {
  const { session, channelName, data } = req.body

  // Confira a origem pelos cabeçalhos: não há assinatura neste canal.
  if (req.get("device") !== DEVICE_TOKEN) return res.sendStatus(401)

  res.sendStatus(200) // você tem 30 segundos

  // A ORDEM IMPORTA — do formato mais específico ao mais genérico.
  if (Array.isArray(data)) {
    console.log(`${data.length} contatos sincronizados`)
  } else if (data.qrcode) {
    exibirQrCode(data.qrcode.base64)
  } else if (typeof data.state === "string") {
    if (data.state === "close") alertarQueCaiu(session)
  } else if (data.status && !data.message) {
    // keyId é o que casa com o key.id da mensagem original.
    console.log(`${data.keyId} agora está ${data.status}`)
  } else if (data.key) {
    if (data.key.fromMe) return // mensagem sua: não responda a si mesmo
    console.log(`${data.pushName}: ${data.message?.conversation}`)
  } else {
    console.warn("Formato Baileys desconhecido", data)
  }
})
```

**Python**

```python
@app.post("/webhook/baileys")
def webhook_baileys():
    if request.headers.get("device") != DEVICE_TOKEN:
        return "", 401

    corpo = request.get_json(force=True)
    data = corpo["data"]

    # A ORDEM IMPORTA — do formato mais específico ao mais genérico.
    if isinstance(data, list):
        print(f"{len(data)} contatos sincronizados")
    elif "qrcode" in data:
        exibir_qrcode(data["qrcode"]["base64"])
    elif isinstance(data.get("state"), str):
        if data["state"] == "close":
            alertar_que_caiu(corpo["session"])
    elif data.get("status") and "message" not in data:
        # keyId é o que casa com o key.id da mensagem original.
        print(f"{data['keyId']} agora está {data['status']}")
    elif "key" in data:
        if data["key"]["fromMe"]:
            return "", 200  # mensagem sua
        print(f"{data.get('pushName')}: {data['message'].get('conversation')}")
    else:
        print("Formato Baileys desconhecido", data)

    return "", 200
```

**PHP**

```php
<?php

if (($_SERVER['HTTP_DEVICE'] ?? null) !== DEVICE_TOKEN) {
    http_response_code(401);
    exit;
}

http_response_code(200); // você tem 30 segundos

$corpo = json_decode(file_get_contents('php://input'), true);
$data = $corpo['data'];

// A ORDEM IMPORTA — do formato mais específico ao mais genérico.
if (array_is_list($data)) {
    error_log(count($data) . ' contatos sincronizados');
} elseif (isset($data['qrcode'])) {
    exibirQrCode($data['qrcode']['base64']);
} elseif (is_string($data['state'] ?? null)) {
    if ($data['state'] === 'close') {
        alertarQueCaiu($corpo['session']);
    }
} elseif (isset($data['status']) && !isset($data['message'])) {
    // keyId é o que casa com o key.id da mensagem original.
    error_log("{$data['keyId']} agora está {$data['status']}");
} elseif (isset($data['key'])) {
    if ($data['key']['fromMe']) {
        exit; // mensagem sua
    }
    error_log(($data['pushName'] ?? '') . ': ' . ($data['message']['conversation'] ?? ''));
} else {
    error_log('Formato Baileys desconhecido');
}
```

## Ver também

- Webhooks: [Webhooks](https://doc.apibrasil.io/webhooks.md)
- Índice da documentação: https://doc.apibrasil.io/llms.txt
- Versão HTML desta página: https://doc.apibrasil.io/webhooks/api-whatsapp-baileys
