Nuntis documentação Referência da API

Enviar e receber

Conversas

Uma conversa é o par número seu × contato do outro lado (ou número seu × grupo). O conversationId já está na sua mão: ele vem em toda mensagem e em todo webhook. Aqui você lista as conversas por última atividade — o que uma tela de inbox pede ao abrir — e busca a foto de perfil do contato.

Listar conversas

GET /v1/conversations

Devolve as conversas dos seus números, da que teve atividade mais recente para a mais antiga, cada uma já com a última mensagem inteira — no mesmo formato de GET /v1/messages, pelo mesmo mapeamento. É a consulta que monta um inbox sem você precisar varrer a fila de mensagens do tenant inteiro.

bash
curl -H "x-api-key: $NUNTIS_API_KEY" \
  'https://api.nuntis.com.br/v1/conversations?limit=50'
200 OK — conversa individual
{
  "data": [
    {
      "id": "conv_991",
      "numberId": "num_7",
      "contact": {
        "phoneE164": "+5585999998888",
        "jid": "5585999998888@s.whatsapp.net",
        "pushname": "Ana"
      },
      "createdAt": "2026-08-20T13:02:11.000Z",
      "createdAtUnix": 1787230931,
      "lastActivityAt": "2026-09-02T09:41:03.000Z",
      "lastActivityAtUnix": 1788342063,
      "lastMessage": {
        "id": "msg_18273",
        "conversationId": "conv_991",
        "numberId": "num_7",
        "direction": "inbound",
        "type": "text",
        "status": "received",
        "from": { "phoneE164": "+5585999998888", "pushname": "Ana" },
        "text": { "body": "Bom dia, ainda tem vaga?" },
        "timestamp": "2026-09-02T09:41:03.000Z",
        "timestampUnix": 1788342063,
        "isSystemMessage": false,
        "mentionedIds": [],
        "statusHistory": []
      }
    }
  ],
  "cursor": { "next": "eyJjIjoiMjAyNi0wOS0wMlQwOTo0MTowMy4wMDBaIiwiayI6Ijk5MSJ9" }
}
200 OK — conversa de grupo (resumido)
{
  "id": "conv_1042",
  "numberId": "num_7",
  "group": { "name": "Obra Aldeota" },
  "createdAt": "2026-08-28T10:00:00.000Z",
  "createdAtUnix": 1787911200,
  "lastActivityAt": "2026-09-02T08:15:00.000Z",
  "lastActivityAtUnix": 1788336900,
  "lastMessage": { "id": "msg_18270", "group": { "name": "Obra Aldeota" }, "...": "..." }
}

Como distinguir os dois tipos: conversa individual traz contact e não traz group; conversa de grupo traz group e não traz contact. Não existe campo de “tipo” — a presença do bloco é o significado, exatamente como no recurso de mensagem.

Evento de protocolo do canal não move a conversa

O WhatsApp entrega eventos de protocolo pela mesma tubulação das mensagens (rótulo de membro de grupo, handshake de boas-vindas, sincronismo entre os aparelhos da própria conta). Desde 04/09/2026 eles são descartados na fronteira do canal: não viram mensagem, não aparecem em lastMessage e não mexem em lastActivityAt. Antes dessa data alguns chegaram como type: "text" sem bloco text — o histórico é preservado como foi gravado, nunca reescrito.

Edição de mensagem ainda não é um recurso do contrato: enquanto não for, ela chega marcada com isSystemMessage: true. Filtre por esse campo se não quiser exibi-la — e nunca por “sem conteúdo” no atacado, que comeria eventos legítimos como message.revoked.

Contato sem phoneE164 não é erro — é ausência honesta

Quem chega até você só por LID (participante de grupo que não expõe o telefone) aparece com contact.lid preenchido e sem contact.phoneE164. O Nuntis não deriva telefone a partir de LID em lugar nenhum: um número plausível e falso seria pior que o vazio, porque você não teria como saber que ele é inventado. Para abrir 1:1 com essa pessoa, copie o lid — Abrir conversa com quem só tem lid.

Parâmetros

ParâmetroEfeito
numberIdRestringe às conversas de um número seu (num_...). Número que não é seu responde 404, indistinguível de inexistente.
typeindividual ou group. Sem ele, vêm os dois.
updatedAfterISO 8601 (UTC). Traz as conversas com lastActivityAt >= updatedAfter — inclusive. Veja os avisos abaixo.
qBusca pelo rótulo da outra ponta: telefone, ou parte do nome — do contato ou do grupo, sem diferenciar maiúsculas. Telefone casa por equivalência — digitar sem o nono dígito acha o mesmo contato. Sem acento-insensibilidade: Joao não acha João. Não busca conteúdo de mensagem. Ver Busca por nome.
cursorCursor opaco devolvido em cursor.next. Repasse sem interpretar. Ausente = do topo.
limitItens por página, de 1 a 100. Padrão 50.
Tenant sem número nenhum responde 200 com lista vazia

Nunca 404. A coleção existe para todo cliente válido — um 404 aqui faria você concluir que a rota não existe e mudar o seu código por causa de um estado passageiro do seu cadastro.

A listagem é uma foto, não um feed

Ela nunca duplica — mas pode deixar algo para a próxima varredura

A ordem é por última atividade, e atividade só sobe. Se uma conversa receber mensagem no meio da sua varredura, ela pula para cima do cursor e não aparece nas páginas seguintes daquela passada — ela aparece no topo da próxima. Nada se perde entre varreduras.

Em compensação, a lista nunca repete um item: o que você já viu ficou acima do cursor e não volta. Para reconciliação por mensagem, sem furo, continue usando GET /v1/messages, que é cronológica crescente.

Busca por nome: contato e grupo, no mesmo termo

q procura o rótulo que o operador vê na tela. Se o termo for só dígitos, ele vira busca por telefone (casada por igualdade, com equivalência do nono dígito); qualquer outra coisa vira busca por trecho do nome — e esse trecho alcança tanto o nome do contato de uma conversa individual quanto o assunto do grupo de uma conversa group.

Não há parâmetro para escolher entre os dois: q=Aldeota devolve a pessoa e o grupo que carregam esse nome, ordenados por última atividade como qualquer página. Para separar, combine com type — q=Aldeota&type=group devolve só a conversa de grupo. Os dois filtros se somam.

Grupo sem nome aprendido não casa por nome

O assunto do grupo é resolvido uma vez, quando o grupo entra no Nuntis. Se aquela consulta não respondeu, group.name fica null — e um grupo sem nome não tem por onde casar um termo de texto. Ele continua na listagem e continua alcançável por type=group; só não aparece na busca por nome.

Não há re-tentativa nesta versão, e renomear o grupo no aparelho também não atualiza o rótulo aqui.

Sem acento-insensibilidade

Joao não acha João, e Aldeota não acha Aldeóta. O casamento é por trecho literal, ignorando apenas maiúsculas e minúsculas. Se a sua interface tem um campo de busca, vale repassar o que o usuário digitou sem “corrigir” acento.

Atividade é mensagem — recibo não reordena

lastActivityAt é o instante da última mensagem da conversa (ou a criação dela, quando ainda não houve mensagem — nesse caso lastMessage vem null). Recibo de entrega, recibo de leitura e revogação não movem a conversa: um read de conversa inteira faria o seu inbox se mexer sem ninguém ter dito nada. Essas transições continuam chegando por message.status e message.revoked.

O Nuntis não sabe o que você leu

Não há unread, nem contador, nem “marcar como lida”

Quem sabe o que o seu usuário viu é você. E o recibo de leitura que o canal entrega é do aparelho do operador, não do seu CRM — publicá-lo como “lida” faria o Nuntis afirmar sobre você algo que não é dele.

O sinal que um inbox de fato usa sai de graça: lastMessage.direction igual a inbound significa que a última fala foi do lead — que é como se decide o que destacar na tela.

Sincronismo incremental com updatedAfter

Guarde o maior lastActivityAt que você viu, reenvie-o em updatedAfter e deduplique por id. O filtro é inclusivo de propósito: com um corte exclusivo, a conversa cuja atividade caiu no mesmo milissegundo da borda seria perdida.

updatedAfter não alcança o histórico importado — de propósito

O histórico entra com a data real de cada mensagem. Uma conversa de março que só agora chegou ao Nuntis não teve atividade ontem, e por isso updatedAfter=ontem não a devolve — do contrário um único pareamento devolveria dezenas de milhares de conversas “mudadas” de uma vez.

Para trazer histórico, faça a varredura completa (sem updatedAfter) e deduplique por id. A receita está em Histórico de conversas.

Erros desta rota

CódigoHTTPQuando
VALIDATION_FAILED400Parâmetro desconhecido, type fora do enum, updatedAfter fora do ISO 8601, ou cursor corrompido. Não fabrique cursor — repasse o que veio.
UNAUTHORIZED401Credencial ausente ou inválida.
NOT_FOUND404Só acontece com numberId: nenhum número acessível com esse identificador. Indistinguível de número de outro cliente da plataforma.

Foto de perfil do contato

GET /v1/conversations/{conversationId}/avatar

Devolve a foto de perfil do contato daquela conversa como URL assinada e temporária, no mesmo formato que você já conhece da mídia de mensagem. Serve para a foto aparecer no card do lead na sua interface — e é só para isso que ela existe.

bash
curl -H "x-api-key: $NUNTIS_API_KEY" \
  'https://api.nuntis.com.br/v1/conversations/conv_9931/avatar'
200 OK — o contato tem foto visível
{
  "url": "https://bucket.devari.com.br/nuntis/avatars/9931.jpg?X-Amz-Signature=...",
  "urlExpiresAt": "2026-08-31T20:00:00.000Z",
  "fetchedAt": "2026-08-31T19:00:00.000Z"
}
200 OK — não há foto a mostrar
{
  "url": null,
  "urlExpiresAt": null,
  "fetchedAt": "2026-08-31T19:00:00.000Z"
}
CampoO que é
url URL assinada e temporária da foto. null é resposta de sucesso, não erro — veja abaixo.
urlExpiresAt Quando a url deixa de funcionar (ISO 8601). null quando url é null. Não há rota de renovação: peça o recurso de novo.
fetchedAt Quando o Nuntis consultou o canal pela última vez — não quando esta resposta foi montada. É a idade da informação. null significa que o Nuntis não consultou — ver Conversa de grupo.

Foto ausente é o caso comum, não a exceção

Não desenhe a tela contando com a foto

Numa amostra real de 14 contatos, medida durante a construção desta rota, 5 tinham foto visível. Os outros 9 não: 7 simplesmente não têm foto, e 2 têm privacidade fechada. É amostra pequena e não é promessa estatística — mas a ordem de grandeza é essa, e a conclusão de produto não muda: a maior parte dos seus leads vai cair no caso sem foto.

Uma interface que reserva espaço para a foto e não trata a ausência fica com buraco na maioria das linhas. Trate url: null como o caminho normal: renderize iniciais ou um placeholder, e faça a foto ser o enfeite que ela é.

Dois motivos diferentes levam ao mesmo url: null, e para a sua interface eles são a mesma coisa: o contato não tem foto, ou a privacidade dele esconde a foto de quem não está na agenda dele. Em nenhum dos dois o Nuntis falhou, e em nenhum dos dois adianta pedir de novo em seguida.

Conversa de grupo: 200 com tudo nulo

Pedir a foto de uma conversa de grupo devolve 200 com url, urlExpiresAt e fetchedAt todos null. Não é erro, e não há o que re-tentar: foto de grupo não é entregue nesta versão do contrato.

json
{ "url": null, "urlExpiresAt": null, "fetchedAt": null }

Para a interface, o tratamento é o mesmo do contato sem foto: renderize o placeholder. O que muda é o motivo, e ele viaja em fetchedAt — sem nenhum campo novo no recurso:

urlfetchedAtO que aconteceu
preenchidapreenchidoHá foto. Use a URL antes de urlExpiresAt.
nullpreenchidoPerguntamos ao canal, e ele respondeu que não há foto (o contato não tem, ou a privacidade dele a esconde).
nullnullNão perguntamos. Hoje o único caso é a conversa de grupo.
Por que 200, e não um erro

Porque 422 CHANNEL_CAPABILITY_UNSUPPORTED quer dizer “este canal não sabe entregar foto de perfil” — e o canal sabe: avatar vem true em GET /v1/channels. Quem seguisse essa instrução leria true, reservaria o espaço na tela e ainda assim tomaria erro. A ausência aqui é uma decisão nossa sobre este tipo de conversa, e não uma limitação do canal.

A resposta não depende do estado do número

Como o Nuntis não consulta o canal neste caso, a resposta é a mesma com o número desativado ou sem sessão conectada. Já a conversa de grupo de outro cliente continua respondendo 404, igual a um identificador inexistente — a verificação de posse vem antes.

Por que a rota é por conversa, e não por contato

Porque a visibilidade da foto é do par, não da pessoa. Um contato pode configurar “foto visível só para meus contatos” — e aí a foto aparece ou não dependendo de qual dos seus números está perguntando: se o telefone daquele operador estiver na agenda do lead, a foto aparece; se for outro número seu, que ele não tem salvo, não aparece.

O mesmo lead, portanto, pode ter foto para um número do seu produto e não ter para outro. A conversa carrega exatamente esse par (número, contato) — por isso ela é a âncora. Uma rota “por contato” teria de escolher um número por conta própria, e qualquer escolha implícita daria uma resposta que não corresponde ao que aquele operador enxerga.

Consequência prática

Não faça cache da foto por contato do seu lado. Se você guardar “o lead X não tem foto” e exibir isso em todas as caixas, vai esconder a foto que outro número do seu produto enxerga perfeitamente. A chave é a conversa.

“Não tem foto” e “não consegui buscar agora” são respostas diferentes

Esta é a distinção que o contrato existe para preservar, e ela muda o que o seu código deve fazer:

O canal respondeu que não há fotoNão deu para consultar agora
Você recebe200 com url: nullErro tipado — 503 CHANNEL_UNREACHABLE
É um fato?Sim. Foi o canal que disse.Não. É ignorância nossa, temporária.
Retry ajuda?Não em seguida — a resposta não vai mudar.Sim. retryable: true.
O que exibirIniciais / placeholder.Mantenha o que já havia, ou o placeholder — e tente de novo depois.

Uma falha temporária nunca é devolvida como url: null. Se fosse, uma indisponibilidade de meio minuto apagaria da sua interface, por horas, a foto de um lead que tem foto — sem erro em lugar nenhum para denunciar. Por isso as duas respostas são de tipos diferentes: se você recebeu 200, a ausência é um fato do canal e você pode confiar nela.

A URL é temporária — gere na hora de exibir

Nunca persista a url

Ela é assinada e expira (urlExpiresAt diz quando). Gravá-la no seu banco é gravar uma mentira com hora marcada: em algum momento os cards do seu inbox passam a mostrar imagem quebrada. Guarde o conversationId e peça o recurso quando for renderizar. É a mesma disciplina da mídia de mensagem.

Pedir de novo é barato: a resposta vem do estado que o Nuntis mantém, sem consultar o canal a cada chamada. Em compensação não há promessa de frescor — fetchedAt é a idade da informação, e a foto pode ter mudado no canal depois daquele instante. Foto de perfil é enfeite de interface; nenhuma decisão do seu produto deveria depender de ela estar atualizada ao segundo.

Antes de reservar o espaço na interface

Nem todo canal entrega foto de perfil. Quem responde isso é o campo avatar em GET /v1/channels: onde ele é false, esta rota responde 422 CHANNEL_CAPABILITY_UNSUPPORTED — e o certo é nem chamá-la, escondendo o espaço da foto para números daquele canal.

avatar: true não promete que existe foto

Ele promete que o canal sabe responder a pergunta. A foto em si continua podendo não existir — e, como visto acima, na maioria das vezes é o que acontece. São duas condições diferentes: a capacidade decide se você pergunta; a resposta decide se você desenha.

Erros da foto de perfil

CódigoHTTPRetentávelQuando
UNAUTHORIZED 401 não Credencial ausente ou inválida.
NOT_FOUND 404 não Nenhuma conversa acessível com esse identificador. Indistinguível de conversa que pertence a outro cliente da plataforma — de propósito.
CHANNEL_CAPABILITY_UNSUPPORTED 422 não O canal deste número não declara avatar. Consulte GET /v1/channels.
NUMBER_DEACTIVATED 422 não O número da conversa foi desativado. Reativar não é self-service.
NUMBER_NOT_CONNECTED 422 não O número não tem sessão conectada no canal, e sem sessão não há como perguntar nada sobre um contato. Retry não resolve — o remédio é parear o número e conferir GET /v1/numbers/{id}/status.
CHANNEL_UNREACHABLE 503 sim Não foi possível consultar o canal agora. Tente de novo com backoff — e não trate como “sem foto”.

A forma do erro é a mesma do resto do contrato, e a regra também: decida o retry por retryable, nunca por status HTTP nem por texto. Veja Erros.

Participantes de uma conversa de grupo

GET /v1/conversations/{conversationId}/participants

Devolve quem está no grupo daquela conversa, segundo o canal — inclusive quem nunca falou — com o papel de cada um. É o que a tela de “Dados do grupo” mostra.

bash
curl -H "x-api-key: $NUNTIS_API_KEY" \
  'https://api.nuntis.com.br/v1/conversations/conv_9931/participants'
200 OK
{
  "data": [
    { "jid": "5585999998888@s.whatsapp.net", "lid": "123456789012345@lid",
      "phoneE164": "+5585999998888", "pushname": "Ana", "role": "superadmin" },
    { "jid": "5585988887777@s.whatsapp.net", "lid": "998877665544332@lid",
      "phoneE164": "+5585988887777", "pushname": null, "role": "member" },
    { "jid": null, "lid": "112233445566778@lid",
      "phoneE164": null, "pushname": null, "role": "member" }
  ],
  "fetchedAt": "2026-09-22T14:03:11.000Z"
}
CampoO que é
jid Endereço de telefone do participante no canal (…@s.whatsapp.net). O mesmo significado de from.jid nas mensagens. null quando o canal escondeu o telefone — veja abaixo.
lid Endereço anônimo (…@lid). O mesmo de from.lid: é por ele que você casa o participante com as mensagens que ele mandou.
phoneE164 Telefone em E.164, derivado só de jid. Nunca é inventado a partir do lid.
pushname O nome que a própria pessoa definiu: o último com que ela falou nesta conversa (o mesmo da bolha da mensagem) ou, se nunca falou aqui, como o número a conhece. Nunca o nome salvo na agenda do celular do operador. Pode ser null.
role member, admin ou superadmin (criador/dono do grupo). Enum aditivo: trate valor desconhecido como member.
fetchedAt Quando o Nuntis consultou o canal — a idade da lista, não o instante da resposta.

A lista é consultada ao vivo — e o Nuntis não a guarda

A lista vem do canal, pela sessão do número daquela conversa, e fica em cache do nosso lado por até 10 minutos (fetchedAt diz a idade). Não há promessa de frescor: alguém pode ter entrado ou saído depois. A ordem dos itens não é contratual, e a lista vem inteira, sem paginação.

O Nuntis não guarda a lista

Ela nunca vai para banco, fila, log ou métrica, e some do cache sozinha. Se o seu produto precisa guardar quem está no grupo, guarde você — sob a sua relação com os titulares.

Participante sem telefone (jid: null)

Quando jid e phoneE164 vêm null e só o lid vem preenchido, o canal escondeu o telefone daquela pessoa. É raro — hoje, perto de 1 em cada 6.000 participantes — e é limite do canal: renderize pelo pushname ou pelo lid.

O nome vem nulo em boa parte dos participantes

Hoje cerca de 2/3 dos participantes vêm com pushname: null: o nome só existe se a pessoa falou nesta conversa ou se o número já a conhece. Renderize o telefone ou as iniciais. O nome da agenda do celular do operador não é publicado, por decisão de privacidade: é uma anotação de quem opera o número sobre outra pessoa.

Orçamento de consultas por número

O limite é do NÚMERO, não da sua chave

Consultar grupos em sequência rápida é o que faz o WhatsApp limitar um telefone. Por isso cada número tem um orçamento de consultas ao canal (hoje 5 por minuto e 30 por hora). Estourado, a rota responde 429 RATE_LIMITED com Retry-After e X-RateLimit-Limit/-Remaining/-Reset da janela do número. Resposta do cache não consome orçamento: abrir a mesma tela de novo é de graça.

Mensagem 1:1 a quem nunca falou é abordagem fria

Com o lid ou o jid da lista você consegue enviar 1:1 a um participante — e o anti-ban trata esse envio como abordagem fria (espaçamento cheio e teto frio do número). Usar a lista para disparo em massa é o caminho mais curto para derrubar um número. A relação com o titular é sua.

Antes de mostrar “Dados do grupo”

Consulte gruposParticipantes em GET /v1/channels: onde ele é false, esta rota responde 422 CHANNEL_CAPABILITY_UNSUPPORTED. A foto do grupo continua não sendo entregue nesta versão (veja Conversa de grupo na foto de perfil).

Erros da lista de participantes

CódigoHTTPRetentávelQuando
UNAUTHORIZED401nãoCredencial ausente ou inválida.
NOT_FOUND404nãoNenhuma conversa acessível com esse identificador. Indistinguível de conversa de outro cliente — de propósito.
CHANNEL_CAPABILITY_UNSUPPORTED422nãoO canal do número não declara gruposParticipantes.
CONVERSATION_NOT_GROUP422nãoA conversa é 1:1. Só conversa de grupo tem lista de participantes.
GROUP_MEMBERSHIP_ENDED422nãoO número não é mais participante deste grupo. A conversa e o histórico continuam disponíveis.
NUMBER_DEACTIVATED422nãoO número da conversa foi desativado.
NUMBER_NOT_CONNECTED422nãoO número não tem sessão conectada. Pareie o número; retry não resolve.
RATE_LIMITED429simOrçamento de consultas deste número esgotado. Respeite Retry-After.
CHANNEL_UNREACHABLE503simNão foi possível consultar o canal agora. Nunca vem como lista vazia.

Permissão

Nenhuma credencial nova é necessária

As três rotas desta página já estão cobertas por messages:read — o mesmo escopo que a sua chave usa para listar mensagens. Se você já lê mensagens, você já lista conversas, já lê a foto de perfil e já lê a lista de participantes de um grupo: não é preciso pedir uma chave nova nem uma permissão extra.

Escopos e rotação: Autenticação.