Começar
Autenticação
Toda requisição ao contrato /v1 leva a sua API key no header
x-api-key. Não há login, não há token de sessão, não há refresh.
x-api-key: <sua-api-key>
Exemplo
curl https://api.nuntis.com.br/v1/channels \
-H "x-api-key: $NUNTIS_API_KEY"
Credencial em query string é recusada
Enviar a key como parâmetro de URL (?api_key=…) não
funciona, e a recusa é por desenho. Query string vaza em três lugares
que ninguém controla:
- log de proxy e de servidor, onde a URL completa costuma ser gravada em texto puro;
- histórico do navegador, se a URL passar por um;
- o header
Referer, que carrega a URL de origem para terceiros.
Header não vai para nenhum dos três por padrão. É a única forma aceita aqui.
O que a key delimita
A key é o limite. Ela identifica o seu produto, e tudo que você lê ou escreve pertence a ele — clientes, operadores, números, conversas, mensagens, subscriptions de webhook.
Pedir um recurso que não é seu devolve 404 NOT_FOUND, exatamente
igual a pedir um recurso que não existe. Os dois casos são
indistinguíveis de propósito: um 403
confirmaria a existência do recurso alheio, e com identificadores
adivinháveis isso viraria um enumerador da base de outro produto.
Orçamento de requisições
Cada API key tem um orçamento em dois horizontes: por minuto e por hora. Toda resposta autenticada — inclusive as de erro — traz o estado corrente dos dois, para que você não precise adivinhar o ritmo.
| Header | O que é |
|---|---|
X-RateLimit-Limit-Minute | Teto de requisições na janela de um minuto. |
X-RateLimit-Remaining-Minute | Quantas ainda cabem nesta janela de minuto. |
X-RateLimit-Reset-Minute | Instante (ISO 8601) em que a janela de minuto vira. |
X-RateLimit-Limit-Hour | Teto na janela de uma hora. |
X-RateLimit-Remaining-Hour | Quantas ainda cabem nesta janela de hora. |
X-RateLimit-Reset-Hour | Instante em que a janela de hora vira. |
curl -sD - -o /dev/null \
-H "x-api-key: $NUNTIS_API_KEY" \
https://api.nuntis.com.br/v1/channels
# x-ratelimit-limit-minute: 60
# x-ratelimit-remaining-minute: 59
# x-ratelimit-reset-minute: 2026-09-02T16:21:00.000Z
# x-ratelimit-limit-hour: 1000
# x-ratelimit-remaining-hour: 999
# x-ratelimit-reset-hour: 2026-09-02T17:00:00.000Z
A janela é de calendário, não deslizante
Reset é um instante, não um número de segundos.
As janelas viram no relógio: a de minuto no segundo :00, a de
hora no minuto :00. Consequência prática: o orçamento
não se recompõe aos poucos — ele volta inteiro na virada.
Quem supõe janela deslizante programa um backoff que vai devolvendo cota aos
poucos, continua batendo no limite e conclui que “o rate limit está errado”. O
comportamento correto é esperar até o Reset.
Quando estoura
A resposta é 429 RATE_LIMITED, e ela traz o trio
sem sufixo — referente à janela que estourou, que pode ser a
de minuto ou a de hora — mais Retry-After em segundos:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 2026-09-02T16:21:00.000Z
Retry-After: 42
RATE_LIMITED é retryable: true. Respeite
Retry-After — retentar antes só gasta orçamento da janela
seguinte.
Se o nosso contador de requisições ficar indisponível, a requisição
passa e Remaining reporta o orçamento cheio —
preferimos deixar passar a recusar tráfego legítimo por um problema nosso.
Isso significa que Remaining alto não promete
que a próxima chamada será aceita. Trate 429 como possível a
qualquer momento, e faça do Retry-After — e não do
Remaining — a sua fonte de verdade quando ele chegar.
Cada API key tem o próprio orçamento. Uma key gastando a cota dela não afeta outra, e os números que você vê são sempre os da key que fez a chamada.
Escopos
Além do produto, a key carrega escopos: o conjunto de coisas que ela pode fazer. Eles são definidos na emissão — peça os que você precisa, e só eles.
| Escopo | O que libera |
|---|---|
messages:send |
POST /v1/messages |
messages:read |
GET /v1/messages, GET /v1/messages/{id} e a recuperação de mídia (POST /v1/messages/{id}/media/redownload) |
numbers:read |
GET /v1/numbers, GET /v1/numbers/{id}/status e GET /v1/channels |
numbers:session |
POST /v1/numbers/{id}/pairing, POST /v1/numbers/{id}/presence e DELETE /v1/numbers/{id}/session |
webhooks:manage |
POST /v1/webhooks, GET /v1/webhooks e DELETE /v1/webhooks/{id} |
customers:read |
GET /v1/customers e GET /v1/customers/{id} |
customers:write |
POST /v1/customers e DELETE /v1/customers/{id} |
operators:read |
GET /v1/customers/{id}/operators e GET /v1/operators/{id} |
operators:write |
POST /v1/customers/{id}/operators e DELETE /v1/operators/{id} |
numbers:provision |
POST /v1/operators/{id}/numbers e DELETE /v1/numbers/{id} |
contacts:read |
Reservado. Nenhuma rota desta versão o utiliza. |
Tudo que está em Provisionamento — criar e
desativar clientes, operadores e números — só responde a uma key que tenha o
escopo correspondente. Sem ele a resposta é 401 UNAUTHORIZED,
mesmo com uma key perfeitamente válida para enviar mensagens.
A escolha é deliberada: capacidade nova não é concedida por omissão. Se você passou a precisar de provisionamento, peça uma key com os escopos novos — não é algo que apareça sozinho numa key antiga.
DELETE de número são separados
numbers:session permite encerrar a sessão
(DELETE /v1/numbers/{id}/session), e só. Desativar o
número (DELETE /v1/numbers/{id}) exige
numbers:provision. Uma key de operação diária não desativa
cadastro por acidente.
Uma key por produto, por ambiente
Não emita uma key por cliente final nem por operador. A separação entre os seus clientes é feita pelo modelo de recursos, não por credenciais — multiplicar keys só multiplica o que você precisa rotacionar quando uma vazar.
Guarde a key como segredo
- Só no servidor. Nunca em aplicativo móvel, front-end web, repositório ou build de cliente — qualquer coisa que rode na máquina do usuário final é pública, mesmo compilada.
- Em variável de ambiente (ou no cofre que você já usa), lida em tempo de execução. A key precisa ser trocável sem recompilar nada.
-
Fora dos logs. Se o seu cliente HTTP registra headers ao
depurar, filtre
x-api-keyexplicitamente.
// A key é lida do ambiente, nunca escrita no código.
const NUNTIS_URL = process.env.NUNTIS_URL; // https://api.nuntis.com.br
const NUNTIS_API_KEY = process.env.NUNTIS_API_KEY;
async function nuntis(caminho, opcoes = {}) {
const resposta = await fetch(`${NUNTIS_URL}/v1${caminho}`, {
...opcoes,
headers: {
'x-api-key': NUNTIS_API_KEY,
'content-type': 'application/json',
...(opcoes.headers || {}),
},
});
if (!resposta.ok) {
// O corpo de erro é sempre { code, message, retryable, source }.
const erro = await resposta.json();
throw Object.assign(new Error(erro.message), erro);
}
return resposta.status === 204 ? null : resposta.json();
}
Rotação
Trocar a key é uma mudança de variável de ambiente, não de código. Se você suspeitar de vazamento, peça uma key nova e descarte a antiga — como toda chamada é autenticada por header, não há sessão pendurada para expirar.
Erros de autenticação
code | HTTP | Quando acontece |
|---|---|---|
UNAUTHORIZED |
401 | Key ausente, inválida, expirada, sem permissão para a rota, ou enviada por um meio não aceito (por exemplo, query string). |
NOT_FOUND |
404 | A key é válida, mas o recurso pedido não é do seu produto — ou não existe. Os dois são indistinguíveis. |
RATE_LIMITED |
429 | Limite de requisições da key excedido. Respeite o header Retry-After. Ver Orçamento de requisições. |
A forma completa do corpo de erro e a lista inteira de códigos estão em Erros.