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
{
"numberId": "num_4711",
"to": { "phone": "+5511999990000" },
"type": "text",
"text": { "body": "Olá! Vi que você se inscreveu — posso ajudar?" }
}
| Campo | Obrigatório | Descriçã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 type ≠ text |
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.+5511999990000vale;(11) 99999-0000e5511999990000não. -
Campo desconhecido é recusado. Escrever
bodyTextem vez detextdevolve400em 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 responde422 CHANNEL_CAPABILITY_UNSUPPORTED.
A resposta: 202, não 200
{
"messageId": "msg_18273",
"status": "queued",
"acceptedAt": "2026-08-13T18:12:03.000Z",
"acceptedAtUnix": 1786126323
}
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
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?" }
}'
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).
Idempotency-Key: 7f3c1a9e-2b40-4a1e-9c77-8de0f2a51b33
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ção | Resultado |
|---|---|
| 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. |
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
| Estado | Significa |
|---|---|
queued | Aceita e agendada na fila de saída. Ainda não saiu. |
sent | Saiu do Nuntis para o canal. |
delivered | Chegou ao aparelho do destinatário. |
read | O destinatário abriu. |
played | Áudio ouvido. |
failed | Terminal, fora da escada. Vem acompanhado de um bloco failure. |
received | Estado terminal de mensagem recebida. Inbound não percorre a escada de saída — nasce entregue. |
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
{
"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.
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
| Rota | Para 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. |
# 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'
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.