Nuntis documentação Referência da API

Enviar e receber

Enviar mensagens

Um único endpoint envia por qualquer canal. Quem determina o canal é o número remetente, não um parâmetro seu.

POST /v1/messages

O corpo

json — texto
{
  "numberId": "num_4711",
  "to": { "phone": "+5511999990000" },
  "type": "text",
  "text": { "body": "Olá! Vi que você se inscreveu — posso ajudar?" }
}
CampoObrigatórioDescrição
numberId sim Qual número seu envia. É ele que determina o canal. Veja Números.
to sim* Destinatário: { "phone": "+5511999990000" } ou { "jid": "…" }.
conversationId sim* Conversa que já existe. Alternativa a to.
type sim text, image, audio, video ou document.
text quando type: text { "body": "…" }, até 65536 caracteres.
media quando typetext Veja Mídia.
replyTo não id da mensagem que este envio responde.

* to e conversationId são alternativas — mande um dos dois, nunca os dois.

Regras que valem saber antes de codar

  • phone é E.164 estrito: + seguido de 7 a 15 dígitos, sem separadores. +5511999990000 vale; (11) 99999-0000 e 5511999990000 não.
  • Campo desconhecido é recusado. Escrever bodyText em vez de text devolve 400 em vez de ser descartado em silêncio — você precisa saber agora, e não descobrir pela mensagem vazia que chegou ao seu cliente.
  • Confira a capacidade do canal antes de enviar mídia ou usar replyTo. Um canal que não declara a capacidade responde 422 CHANNEL_CAPABILITY_UNSUPPORTED.

A resposta: 202, não 200

202 Accepted
{
  "messageId": "msg_18273",
  "status": "queued",
  "acceptedAt": "2026-08-13T18:12:03.000Z",
  "acceptedAtUnix": 1786126323
}
202 significa “aceito e agendado”, não “enviado”

A mensagem não saiu ainda. Ela entra numa fila de saída com espaçamento para não queimar o número, e só depois vai para o canal. Um contrato que devolvesse 200 aqui estaria mentindo em toda mensagem.

Quem conta o final da história é o webhook de status — ou, se você preferir puxar, GET /v1/messages/{id}. Não faça polling em laço: a informação chega sozinha.

Exemplo completo

bash
curl -X POST https://api.nuntis.com.br/v1/messages \
  -H "x-api-key: $NUNTIS_API_KEY" \
  -H "content-type: application/json" \
  -H "idempotency-key: $(uuidgen)" \
  -d '{
    "numberId": "num_4711",
    "to": { "phone": "+5511999990000" },
    "type": "text",
    "text": { "body": "Olá! Posso te ajudar?" }
  }'
javascript
const { randomUUID } = require('crypto');

async function enviarTexto({ numberId, telefone, corpo, chaveDeEnvio }) {
  const resposta = await fetch(`${process.env.NUNTIS_URL}/v1/messages`, {
    method: 'POST',
    headers: {
      'x-api-key': process.env.NUNTIS_API_KEY,
      'content-type': 'application/json',
      // Gerada junto com a DECISÃO de enviar, não aqui. Veja abaixo.
      'idempotency-key': chaveDeEnvio ?? randomUUID(),
    },
    body: JSON.stringify({
      numberId,
      to: { phone: telefone },
      type: 'text',
      text: { body: corpo },
    }),
  });

  const corpoResposta = await resposta.json();

  if (!resposta.ok) {
    // { code, message, retryable, source } — decida o retry por `retryable`.
    throw Object.assign(new Error(corpoResposta.message), corpoResposta);
  }

  // 202: aceita e agendada. O estado real chega pelo webhook.
  return corpoResposta.messageId;
}

Idempotência

Mande o header Idempotency-Key com um identificador único por intenção de envio (1 a 255 caracteres ASCII visíveis).

header
Idempotency-Key: 7f3c1a9e-2b40-4a1e-9c77-8de0f2a51b33
Sem a chave não há deduplicação nenhuma

O header é opcional, e omiti-lo significa que um retry depois de um timeout de rede vai entregar a mensagem duas vezes ao destinatário. Isso é deliberado, não esquecimento.

Por que não deduplicamos por conteúdo

Numa conversa real, “ok” e “sim” se repetem o tempo todo, legitimamente. Suprimir a segunda ocorrência seria uma falha invisível — o seu sistema acharia que enviou, o destinatário nunca receberia, e ninguém veria erro nenhum. Duplicata incomoda; falso dedupe some. Por isso a deduplicação é opt-in e a chave é sua intenção, não o conteúdo.

Duas mensagens iguais com chaves diferentes são dois envios legítimos — e devem ser.

O que a chave garante

SituaçãoResultado
Mesma chave, mesmo payload, dentro de 24 h Devolve a resposta original, sem reenviar, com o header Idempotency-Replayed: true.
Mesma chave, payload diferente 422 IDEMPOTENCY_KEY_REUSED. Use uma chave nova ou reenvie o payload original.
Mesma chave em voo agora 409 IDEMPOTENCY_IN_PROGRESS, com retryable: true. Tente de novo com backoff — a chave não queimou.
Payload inválido (400) A chave não é consumida. Corrija o payload e reenvie com a mesma chave.
Gere a chave junto com a decisão de enviar

Não a gere na hora da chamada HTTP: se a chamada falhar por timeout e você gerar uma chave nova no retry, a proteção não existe. Crie a chave quando o seu sistema decide enviar, persista-a junto com o seu registro, e reuse-a em toda tentativa daquele envio.

O escopo da chave é a sua API key, e a janela contratual é de 24 horas.

Estados de entrega

A escada é monotônica — só sobe, nunca desce:

queued → sent → delivered → read → played
EstadoSignifica
queuedAceita e agendada na fila de saída. Ainda não saiu.
sentSaiu do Nuntis para o canal.
deliveredChegou ao aparelho do destinatário.
readO destinatário abriu.
playedÁudio ouvido.
failedTerminal, fora da escada. Vem acompanhado de um bloco failure.
receivedEstado terminal de mensagem recebida. Inbound não percorre a escada de saída — nasce entregue.
Nem todo canal reporta tudo

Num canal com deliveryReceipts: "none" — o Telegram, hoje — sent é o estado final de sucesso. Nunca chegará delivered nem read, e um laço esperando por eles nunca termina. Confira GET /v1/channels antes de modelar leitura na sua interface.

Quando falha

json — trecho do recurso da mensagem
{
  "id": "msg_18273",
  "status": "failed",
  "failure": {
    "code": "CHANNEL_DISCONNECTED",
    "retryable": true,
    "at": "2026-08-13T18:12:04.000Z"
  }
}

Decida o retry por failure.retryable, nunca por interpretação do texto. Códigos novos entram de forma aditiva — trate um code desconhecido como falha genérica.

DISPATCH_AMBIGUOUS merece atenção especial

Significa que o Nuntis não obteve confirmação do canal e, por isso, não tentou de novo — a mensagem pode ter sido entregue. Reenviar nesse caso pode duplicar no aparelho do destinatário. Se ela tiver saído, você a verá aparecer na conversa pelo eco do canal (evento message.echo).

Consultar e reconciliar

RotaPara quê
GET /v1/messages/{id} Estado canônico de uma mensagem, com o histórico completo de transições.
GET /v1/messages Lista cronológica ascendente, paginada por cursor. É o caminho de reconciliação.
bash
# primeira página
curl -H "x-api-key: $NUNTIS_API_KEY" \
  'https://api.nuntis.com.br/v1/messages?limit=50'

# próxima página — repasse `cursor.next` sem interpretar
curl -H "x-api-key: $NUNTIS_API_KEY" \
  'https://api.nuntis.com.br/v1/messages?since=eyJjIjoi...&limit=50'
Não interprete o cursor

Ele é opaco e codifica posição mais desempate; o formato pode mudar sem aviso. Fabricar um cursor a partir de um timestamp faz você perder mensagens gravadas no mesmo milissegundo.

Guarde o último cursor.next não-nulo e volte com ele. next: null significa “nada novo agora”, não “acabou para sempre”.

O statusHistory vem preenchido em GET /v1/messages/{id} e vazio na listagem, de propósito: montar o histórico de cada item multiplicaria as consultas na rota mais chamada do contrato.