A API da plataforma Asender: contas (tenants) e sub-contas, membros, convites, API keys, plano e a superfície pública de mensageria por API key. É o que atravessa as ferramentas — a identidade e a conta são compartilhadas; cada produto (Mail, CRM, Páginas) tem a sua própria API num endereço próprio.
Base URL:https://api.asender.net
Duas superfícies, dois modos de auth:
/v1/* — API pública do cliente, autenticada por API key e com rate limit. É a que uma integração de máquina usa.
/api/* — API do painel, autenticada por sessão (OIDC/cookie). É a que o front do painel usa em nome do usuário logado.
Formato: JSON. Os envelopes seguem o estilo SES (chaves PascalCase).
Envelope de erro desta API:{ "Error": { "Type", "Code", "Message", "Details" }, "RequestId" }. A Mail API (api.mail.asender.net) usa outro, mais simples: { "error", "message" }. Não trate os dois com o mesmo parser — veja as Convenções.
Rota que não existe aqui:404 e 405 em /v1/* também vêm no envelope (CodeNotFound / MethodNotAllowed), com uma dica quando o caminho é de outra API (por exemplo POST /v1/emails é da Mail API; /v1/dominios é da API de Páginas).
Autenticação
/v1/* — API key ou token de app
A superfície pública de máquina aceita duas credenciais, sempre em Authorization: Bearer:
credencial
formato
quando usar
API key (chave de servidor)
sk_live_… ou sk_test_…
integração da sua própria conta, de servidor para servidor. Envia mensagens e lê dados. Veja o guia de chaves de API
Token de app (OAuth 2.1)
JWT emitido por auth.asender.net
um app seu ou de terceiro que age em nome de um usuário, na conta que ele escolheu ("Entrar com Asender"). Só leitura (veja os escopos abaixo). Veja o guia Entrar com Asender
O tenant_id vem da credencial, nunca de parâmetro, header ou corpo. Esta superfície é rate-limited (ver abaixo).
Só Authorization: Bearer é aceito. X-Api-Key e ?api_key= foram removidos (2026-10-07) e respondem 401.
sk_live_ e sk_test_. O prefixo registra o modo da chave, escolhido na criação (o padrão é live). Hoje a única diferença é o rótulo: o modo aparece no painel e nos logs (filtro mode), o que ajuda a separar tráfego de teste do de produção. Não existe sandbox: uma chave sk_test_ autentica, tem os mesmos escopos e o mesmo rate limit, e a API não trata o envio feito com ela de forma diferente. Para testar sem entregar a ninguém, use destinatários seus. Um sandbox de verdade é planejado.
Chave de servidor não vai ao navegador. Pedido com Origin ou Sec-Fetch-* (sinais de navegador) às rotas de envio de push, WhatsApp, Telegram e canais não oficiais é recusado com 403 ServerKeyInBrowser. Para registrar web push a partir do site existe a chave pública pk_push_…, que só abre POST /v1/push/site/devices.
Servidor MCP. Assistentes de IA acessam estes mesmos dados pelo servidor MCP em https://api.asender.net/mcp (em implantação) — veja o guia MCP.
/api/* — sessão do usuário
A superfície do painel. O usuário autentica no Authorization Server (https://auth.asender.net, OAuth 2.1 + PKCE) e a sessão trafega em cookie HttpOnly — o token nunca vai para o navegador em JavaScript. O tenant da requisição é resolvido da sessão; o header X-Asender-Tenant do front só é aceito depois de provada a pertinência do usuário àquela conta. Conta alheia responde 404, nunca 403 (a existência da conta é informação).
Como obter e usar uma API key
No painel, em Administração → Chaves de API (https://admin.asender.net/t/<conta>/api-keys): crie a chave, escolha os escopos e copie o segredo, que aparece uma única vez. Passo a passo no guia de chaves de API.
O painel usa a Platform API na superfície de sessão, que também serve a quem automatiza a emissão:
# Criar (sessão do usuário; devolve a chave em claro UMA vez)
curl -X POST 'https://api.asender.net/api/tenants/TENANT_ID/api-keys' \
-H 'Content-Type: application/json' \
--cookie 'asender_session=...' \
-d '{ "name": "integração-2fa", "scopes": ["email:send"], "mode": "live" }'
# `scopes` é obrigatório (veja a tabela de escopos); sem ele a emissão responde 400.
# `mode` é opcional: "live" (padrão) ou "test".
# Listar / revogar
curl 'https://api.asender.net/api/tenants/TENANT_ID/api-keys' --cookie '...'
curl -X DELETE 'https://api.asender.net/api/tenants/TENANT_ID/api-keys/KEY_ID' --cookie '...'
A chave emitida aqui autentica as superfícies /v1/* desta API e as APIs de produto (por exemplo POST https://api.mail.asender.net/v1/emails). Guarde-a no servidor — nunca no bundle do navegador.
Rate limits (/v1/*)
A superfície pública é limitada por principal, por janela fixa de 1 segundo: a chave (cada sk_… tem o seu balde) ou, para token de app, o par app × conta (um app barulhento numa conta não consome a cota do mesmo app em outra). O teto padrão é de 100 requisições por segundo. Toda resposta traz o estado do balde:
header
significado
X-RateLimit-Limit
teto de requisições na janela
X-RateLimit-Remaining
quantas restam na janela atual
X-RateLimit-Reset
epoch (segundos) em que a janela reabre
Retry-After
(só no 429) segundos até poder tentar de novo, no mínimo 1
Em 429, espere o Retry-After; sem ele, use backoff exponencial com jitter. Se o limitador estiver indisponível, a API não bloqueia (o controle é de abuso, não de autorização): trate os headers como informativos, não como garantia.
GET /v1/logs tem um teto próprio, menor, de 5 req/s por chave, somado ao limite geral.
A superfície de sessão (/api/auth/*) tem um teto por IP próprio, contra brute-force de login/2FA/recuperação. Não é um limite de integração — é uma defesa da borda de autenticação.
Erros
Todos os erros usam o envelope estilo SES:
{
"Error": {
"Type": "Sender",
"Code": "ValidationError",
"Message": "One or more fields failed validation.",
"Details": { "Destination.ToAddresses": "must contain at least one address" }
},
"RequestId": "req_a1b2c3"
}
Type é Sender para 4xx (o cliente errou) e Receiver para 5xx (o serviço falhou).
Code é a classe estável (ValidationError, AuthorizationError, Throttling, InternalError, …).
Details só aparece em validação, com o campo e o motivo.
RequestId acompanha o pedido no rastreamento — cite-o ao abrir suporte.
código HTTP
Code típico
quando
400
ValidationError
corpo inválido; Details traz os campos. JSON com campo que a API não conhece nomeia o campo na mensagem (unknown field "X")
401
AuthorizationError
credencial ausente/ inválida (chave ou sessão)
403
InsufficientScope
a API key não tem o escopo da rota; a mensagem nomeia o escopo exigido (ex.: API key lacks the required scope: sms:send.) — ver Escopos da API key
403
ServerKeyInBrowser
chave sk_… usada de um navegador nas rotas de push, WhatsApp, Telegram e canais não oficiais — chame do seu servidor
404
NotFound
recurso fora do escopo do principal (inclui conta alheia)
409
IdempotencyConflict
a Idempotency-Key já foi usada com um corpo diferente — ver Idempotência
422
ValidationError
o corpo é válido, mas o pedido não tem destino possível (ninguém sobra após opt-out, aparelho inexistente) ou a Idempotency-Key é malformada
422
RemetenteNaoVerificado
o Source do e-mail não é de um domínio verificado da conta — ver Remetente
429
Throttling
rate limit da chave (ver X-RateLimit-*)
503
VerificacaoIndisponivel
não deu para confirmar o domínio do Source agora; nada foi enviado — tente de novo (Retry-After)
502
InternalError
o serviço de origem falhou ao responder (conta, uso)
5xx
InternalError
falha do serviço ou de um upstream
O corpo é lido com contrato estrito: campo desconhecido é recusado com 400.
Paginação
As coleções da plataforma paginam por cursor (NextCursor / HasMore), nunca por offset — offset pula ou repete item quando o dado muda entre chamadas. Passe o NextCursor de volta para a próxima página.
As rotas de dados (/v1/leads, /v1/campaigns etc.) e os logs usam snake_case (next_cursor, has_more) e limit de 1 a 200; veja Dados.
Superfície da API
/v1/* — pública (API key ou token de app)
Todas as rotas abaixo ficam em https://api.asender.net, exigem um escopo e respondem no envelope de erro desta API. Rota que não está nesta lista não existe para chave (403, nunca "liberada por omissão").
grupo
rotas
escopo
E-mail
POST /v1/emails/send, POST /v1/emails/batch, GET /v1/senders
email:send
GET /v1/emails, GET /v1/emails/{id}
email:read
SMS
POST /v1/sms/send
sms:send
WhatsApp (oficial, Meta)
POST /v1/whatsapp/send
whatsapp:send
Canais não oficiais
POST /v1/telegram/send
telegram:send
POST /v1/whatsweb/send
whatsweb:send
POST /v1/telegramconta/send
telegramconta:send
Push
POST /v1/push/send
push:send
GET/POST /v1/push/devices
push:devices
POST /v1/push/site/devices (navegador)
chave pública pk_push_…
Conta
GET /v1/account
qualquer chave válida (token de app: account:read)
GET /v1/account/usage
account:read
GET /v1/me
só token de app, escopo openid
Logs
GET /v1/logs, GET /v1/logs/{id}
logs:read
Dados (leitura)
GET /v1/leads, /v1/leads/{id}
leads:read
Leads (escrita)
POST /v1/leads, PATCH /v1/leads/{id}
leads:write (só chave)
GET /v1/forms, /v1/forms/{id}/submissions
forms:read
GET /v1/conversions
conversions:read
GET /v1/campaigns, /v1/campaigns/{id}, /v1/campaigns/{id}/report, /v1/reports/email/{overview,timeseries,top-emails,domains}
campaigns:read
GET /v1/messaging/{sms,whatsapp,push}/messages, /v1/messaging/{sms,whatsapp,push}/reports/overview
messaging:read
GET /v1/analytics/{overview,breakdown,timeseries,forms}
analytics:read
GET /v1/conversations, /v1/conversations/{id}/messages
conversations:read
GET /v1/journeys, /v1/journeys/{id}
journeys:read
GET/POST /v1/webhooks, GET/PATCH/DELETE /v1/webhooks/{id}, POST /v1/webhooks/{id}/test
webhooks:manage
DNS
GET /v1/dns/zones, GET /v1/dns/zones/{zona}/records, GET /v1/dns/changes
dns:read
POST /v1/dns/zones/{zona}/records, PATCH/DELETE /v1/dns/zones/{zona}/records/{rid}
dns:write
Os grupos são detalhados nas seções seguintes. As rotas de dados têm um resumo abaixo e o detalhe campo a campo no guia Entrar com Asender (seção 9) e na referência OpenAPI.
Escopos da API key
Cada rota exige um escopo; a chave que não o tem recebe 403 InsufficientScope. Uma chave de e-mail (email:send,email:read) não envia SMS, push nem WhatsApp. Rota sem escopo declarado também é recusada. Existem 28 escopos; a coluna "token de app" diz quais um app OAuth também pode exercer (só leitura; *:send, mail:admin e *:write são exclusivos de chave).
escopo
libera
token de app
email:send
POST /v1/emails/send, POST /v1/emails/batch, GET /v1/senders
não
email:read
GET /v1/emails, GET /v1/emails/{id}
sim
sms:send
POST /v1/sms/send
não
push:send
POST /v1/push/send
não
push:devices
GET/POST /v1/push/devices
não
whatsapp:send
POST /v1/whatsapp/send
não
telegram:send
POST /v1/telegram/send (bot do Telegram)
não
whatsweb:send
POST /v1/whatsweb/send (WhatsApp por sessão, canal não oficial)
não
telegramconta:send
POST /v1/telegramconta/send (conta pessoal do Telegram, canal não oficial)
não
account:read
GET /v1/account/usage
sim
logs:read
GET /v1/logs, GET /v1/logs/{id} (log das requisições da própria conta)
sim
mail:admin
administração de domínios e caixas do e-mail da conta, na API do mail (GET /orgs, POST/GET /orgs/{orgID}/domains, verificação de domínio, POST/GET /domains/{id}/mailboxes…). Não lê mensagens (email:read) nem envia (email:send)
não
forms:read
GET /v1/forms e GET /v1/forms/{id}/submissions em api.asender.net, e a leitura de formulários em api.pages.asender.net
sim
forms:write
criar, editar e apagar formulários e enviar leads (submissões), em api.pages.asender.net
não
pages:read
ler páginas, versões, rascunho, SEO, domínios e mídia, em api.pages.asender.net (não são rotas de api.asender.net)
não
pages:write
criar e editar páginas (versões, rascunho, SEO, importar, duplicar, mudar de domínio, mídia), em api.pages.asender.net
não
pages:publish
publicar e despublicar páginas (ação visível ao público; escopo separado do pages:write), em api.pages.asender.net
não
leads:read
GET /v1/leads, GET /v1/leads/{id} (contatos do CRM)
sim
leads:write
POST /v1/leads, PATCH /v1/leads/{id} (criar e atualizar contatos; não inclui leitura)
não
conversions:read
GET /v1/conversions
sim
analytics:read
GET /v1/analytics/*
sim
campaigns:read
GET /v1/campaigns*, GET /v1/reports/email/*
sim
messaging:read
GET /v1/messaging/* (SMS, WhatsApp e push)
sim
conversations:read
GET /v1/conversations*
sim
journeys:read
GET /v1/journeys*
sim
webhooks:manage
/v1/webhooks* (cadastro e teste de endpoints de webhook; o segredo whsec_… sai uma vez, no POST)
não
dns:read
GET /v1/dns/zones, /v1/dns/zones/{zona}/records, /v1/dns/changes (DNS da conta pela Cloudflare)
não
dns:write
POST /v1/dns/zones/{zona}/records, PATCH/DELETE …/records/{rid}; não inclui leitura
não
GET /v1/account aceita qualquer chave válida. Emita chaves novas com os escopos mínimos da integração (painel de chaves): escopo é obrigatório e só vale o catálogo acima — a emissão sem escopos, ou com um valor fora do catálogo, responde 400 com a lista válida. Uma chave sem escopo do catálogo recebe 403 InsufficientScope em toda rota.
leads:write vale para a chave de servidor (POST /v1/leads, PATCH /v1/leads/{id}, veja Escrever leads). Token de app OAuth nunca recebe esse escopo: o consentimento não o concede e as rotas respondem 403.
DNS (/v1/dns/*). Só chave sk_* (app OAuth recebe 403). A conexão com a Cloudflare (o token) é feita por uma pessoa no painel, em DNS; a API não cria nem lê conexões. Campos e erros em inglês; o tenant é o da chave. Controles de escrita: ?dry_run=1 devolve o plano sem alterar nada; X-Asender-Confirmar: <nome do registro> é obrigatório no DELETE e em MX/SPF/DMARC do apex (428 ConfirmationRequired sem ele); Idempotency-Key (24 h). Fora de produção toda escrita responde 403 NonProductionBlocked. Toda tentativa de escrita entra em GET /v1/dns/changes com o before, para desfazer.
Exemplo — e-mail pela plataforma. Serve para código de verificação/2FA, e-mail transacional, lote e campanha: aceita qualquer remetente de um domínio verificado na conta, com o escopo email:send (não precisa criar caixa):
O Source de POST /v1/emails/send e de cada item de /v1/emails/batch tem de ser de um domínio verificado nesta conta (SPF e DKIM conferidos em Admin → Domínios, https://admin.asender.net/t/<conta>/dominios). O e-mail sai assinado com o DKIM do seu domínio.
Atenção: aqui basta o domínio estar verificado. Na Mail API (POST /v1/emails) o endereço também precisa ser uma caixa criada no domínio.
Ver ou verificar o domínio por API: não existe /v1/dominios nesta API. Pelo painel, em Admin → Domínios; por API, na Mail API com uma chave de escopo mail:admin: GET https://api.mail.asender.net/orgs, GET …/orgs/{id}/domains e POST …/domains/{id}/verify. (/v1/dominios existe só na API de Páginas, para domínios de sites.)
Domínio não verificado (ou verificado em outra conta): 422 RemetenteNaoVerificado. A Message diz o domínio e traz o link para Domínios. Nada é enfileirado nem cobrado. No lote, o item sai com Status: "failed" e o mesmo Code/Message; os outros itens seguem.
Verificação fora do ar: 503 VerificacaoIndisponivel com Retry-After. Nada é enviado; repita o pedido (com a mesma Idempotency-Key).
Se o domínio perder a verificação depois do 202 (DNS mudou antes do envio), a mensagem não sai: ela fica failed com o código RemetenteNaoVerificado (veja GET /v1/emails/{id}). Ela nunca é enviada com outro remetente.
{
"Error": {
"Type": "Sender",
"Code": "RemetenteNaoVerificado",
"Message": "O domínio empresa.com.br não está verificado nesta conta (SPF e DKIM). Verifique o domínio em Admin → Domínios (https://admin.asender.net/t/acc_123/dominios), ou use um remetente de um domínio já verificado. Nada foi enviado."
},
"RequestId": "req_…"
}
Mudança de contrato (2026-10-02). Até esta data, um Source de domínio não verificado era aceito com 202 e o e-mail saía com o remetente da plataforma (no-reply@…), com o seu endereço em Reply-To. Agora o pedido é recusado com 422 RemetenteNaoVerificado. Se você integrava mandando um Source não verificado, verifique o domínio em Admin → Domínios ou troque o Source por um endereço de domínio verificado. Veja o registro de mudanças.
Qual e-mail usar? Para código de verificação/2FA (OTP) e e-mail transacional, use POST https://api.asender.net/v1/emails/send (aqui): basta o domínio estar verificado. A Mail API (POST https://api.mail.asender.net/v1/emails) é para enviar a partir de uma caixa corporativa que já existe. | | api.asender.net/v1/emails/send | api.mail.asender.net/v1/emails | | --- | --- | --- | | corpo | Source, Destination.ToAddresses, Message (PascalCase) | from, to[], subject, text/html (minúsculas) | | remetente | qualquer endereço de domínio verificado | caixa existente em domínio verificado | | sem a condição do remetente | 422 RemetenteNaoVerificado | 422 CaixaRemetenteInexistente | | escopo | email:send | email:send | | envelope de erro | { "Error": {…}, "RequestId" } | { "error", "message" } |
Descubra remetentes válidos (GET /v1/senders)
Antes de enviar, pergunte o que a chave pode usar como from. Escopo email:send (o mesmo de quem envia); a conta é a da chave, não há parâmetro de conta.
Domínio não verificado é listado (Verified: false, UsableFor: []) para você ver o que falta; usá-lo como Source dá 422 RemetenteNaoVerificado.
DnsStatus vem da verificação automática de DNS: ok, pending (esperando propagar), outdated (um registro mudou depois de verificado) ou unknown.
Em POST /v1/emails/send qualquer endereço de um domínio com emails/send serve; as Mailboxes mostram as caixas que existem (exigidas só na Mail API).
Só caixas ativas aparecem.
Mail API fora do ar: 503 SendersUnavailable com Retry-After; nunca uma lista parcial.
Logs de requisições (GET /v1/logs)
O log responde "o que a minha integração mandou e o que a API respondeu?". Cada requisição feita à API pública com uma chave da sua conta vira um registro, inclusive as recusadas (400, 403, 429). Escopo logs:read; a conta é a da chave (não existe parâmetro de conta: um tenant_id na query é descartado). Teto próprio de 5 req/s por chave, somado ao limite geral da API.
O que é gravado. Método, caminho e padrão de rota, status, latência, chave (id e nome) e modo (live/test), Idempotency-Key, código de erro do envelope, ids de mensagem devolvidos pelo envio (message_ids), request_id e trace_id. O detalhe traz também os corpos da requisição e da resposta, depois da redação (abaixo).
Redação. Segredo não é gravado, em nenhum campo, em qualquer nível do JSON: tokens de API (sk_…/pk_…), JWT, Bearer …, Authorization, senhas, códigos de verificação (code, otp), PIN, CVV, números de cartão (com checagem de Luhn) e CPF/CNPJ viram [REDACTED]. O conteúdo das mensagens (html, text, subject, body, variáveis de template) vira um marcador [redacted len=<bytes> sha256=<16 hex>]: dá para saber que existiu e conferir se dois envios tinham o mesmo conteúdo, sem guardar o texto. Destinatários (to, cc, bcc, phone…) aparecem mascarados (c***@cliente.com.br). Na query só aparecem parâmetros conhecidos. Cada registro informa em redaction_version qual conjunto de regras valeu. Corpos redigidos acima de 16 KiB são cortados (request_body_truncated / response_body_truncated).
Retenção. Os registros ficam guardados por tempo indeterminado; não há rotina de expiração hoje. Um aviso antecipado precede qualquer mudança nisso.
Filtros (todos opcionais, combináveis): status_class (2xx,3xx,4xx,5xx), status, method, path (padrão de rota exato, ex. /v1/emails/send), api_key_id (vírgula para várias), error_code, idempotency_key, message_id, mode (live/test), since e until (RFC 3339). Sem janela, valem as últimas 24 h; uma janela maior que 31 dias é reduzida a 31 dias antes de until.
Paginação por cursor. A lista vem do mais novo para o mais antigo, limit de 1 a 100 (padrão 20). Enquanto has_more for true, repita a chamada com after=<next>; before=<cursor> volta para os mais novos (after e before juntos dão 400). O cursor é opaco e assinado para a sua conta: alterado ou de outra conta, dá 400. (Esta rota usa has_more/next em snake_case, não HasMore/NextCursor: o corpo vem do serviço de logs sem reescrita.)
# Falhas de envio das últimas 24 h
curl 'https://api.asender.net/v1/logs?status_class=4xx,5xx&path=/v1/emails/send&limit=20' \
-H 'Authorization: Bearer sk_test_xxxxxxxxxxxxxxxxxxxx'
# Próxima página
curl 'https://api.asender.net/v1/logs?status_class=4xx,5xx&after=cur_eyJ0Ijoi…' \
-H 'Authorization: Bearer sk_test_xxxxxxxxxxxxxxxxxxxx'
Do envio ao log, e de volta. O id de mensagem que o envio devolve entra em message_ids; para achar a requisição que criou uma mensagem, filtre por message_id. Para investigar uma falha, abra o detalhe — a lista não traz corpos:
request_body e response_body do detalhe vêm redigidos; null quando a requisição não tinha corpo.
Erros.400 ValidationError (parâmetro inválido, cursor adulterado, after com before), 401, 403 InsufficientScope (chave sem logs:read; uma chave só com email:send não lê logs), 404 NotFound (id inexistente ou de outra conta, com o mesmo corpo), 429. Serviço de logs fora do ar: 503 ServiceUnavailable com Retry-After; o resto da API continua funcionando. Os 400/404 desta rota vêm no envelope do serviço de logs ({ "error": { "type", "code", "message" } }, chaves em minúsculas), diferente do { "Error": … } do restante da API.
No painel, os mesmos dados (mais séries e resumo por rota e por código de erro) ficam em GET /api/tenants/{id}/logs, …/logs/{logId}, …/logs/series e …/logs/resumo, para quem é owner ou admin da conta.
Lote de e-mail (POST /v1/emails/batch)
Até 500 mensagens independentes numa chamada, cada uma no mesmo formato de /v1/emails/send, dentro da lista Messages. Escopo email:send. Cada item é validado e enfileirado sozinho: um item com erro não derruba os outros, e o resultado vem por índice.
Status do item: queued (aceito), rejected (falhou a validação) ou failed (o serviço recusou; por exemplo RemetenteNaoVerificado).
Lista vazia ou com mais de 500 itens: 400 ValidationError.
O 202 quer dizer que a requisição foi processada, não que todos os itens foram aceitos: sempre confira Status por item.
A Idempotency-Key do cabeçalho vale por item (<chave>:<índice>); repetir o lote inteiro com a mesma chave não reenvia os itens já aceitos.
WhatsApp (POST /v1/whatsapp/send)
Mesmo padrão do SMS: 202 quer dizer enfileirado, não entregue. Mande ouBody (texto livre) ouTemplate (slug de um template aprovado pela Meta e registrado na conta) com Variables posicionais.
Janela de 24h. Texto livre só chega se o destinatário escreveu para o seu número nas últimas 24h; fora disso a Meta recusa e a mensagem termina failed (OutsideWindow) — de forma assíncrona, depois do 202. Para iniciar a conversa, use Template.
Template. Slug inexistente na conta, ou Variables em número diferente do que o template exige: 422 ValidationError, sem cobrança.
Opt-out. Quem saiu do canal não recebe; a resposta traz Suppressed com a contagem. Se ninguém sobra, 422.
Escopo da chave:whatsapp:send. Chave de servidor usada de um navegador (Origin/Sec-Fetch-*) é recusada com 403 ServerKeyInBrowser.
Credencial. A conta precisa ter o WhatsApp (Meta Cloud API) configurado em Admin → WhatsApp; sem isso a mensagem fica failed.
SMS
POST /v1/sms/send responde 202 com enfileirado, não entregue. Corpo: To (lista de números em E.164), Body (até 1600 caracteres), From, Tags e IdempotencyKey opcionais. Escopo sms:send.
Push exige DeviceTokens (pelo menos um aparelho registrado em POST /v1/push/devices). Envio por tópico não é suportado: o campo Topic é recusado com 400 ValidationError e Details.Topic, com ou sem tokens.
Se nenhum token é um aparelho ativo da conta, ou ninguém sobra depois do opt-out, a resposta é 422 ValidationError e nada é cobrado.
Escopos: push:send para enviar, push:devices para o registro e a listagem.
Registrar e listar aparelhos (do seu servidor, com a chave sk_…, escopo push:devices):
# Registrar (upsert: o mesmo token duas vezes não cria dois registros)
curl -X POST 'https://api.asender.net/v1/push/devices' \
-H 'Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxx' \
-H 'Content-Type: application/json' \
-d '{ "Token": "<token FCM/APNs>", "Platform": "android", "UserRef": "cliente-77" }'
# Listar (paginação por cursor: Devices, NextCursor, HasMore; query limit e cursor)
curl 'https://api.asender.net/v1/push/devices?limit=50' \
-H 'Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxx'
Platform é ios, android ou web; para web mande Endpoint, P256dh e Auth (do PushSubscription.toJSON() do navegador) em vez de Token.
Registro direto do navegador (web push). Nunca ponha a chave sk_… no site. O site usa a chave públicapk_push_…, emitida no painel (tela de push, junto do manifest.json, do service worker e do snippet já preenchidos) e atrelada às origens do site que você declarou. Ela só abre uma rota:
Origem não declarada, chave adulterada ou plataforma diferente de web são recusadas. A rota tem teto por IP (30 por minuto) e por chave (1200 por minuto). A chave de site não é segredo, mas só vale para registrar inscrições. O guia do navegador mostra o snippet completo.
Canais não oficiais (Telegram e WhatsApp por sessão)
Três rotas de envio por canais que não são a API oficial da Meta. Todas respondem 202 com { "MessageId", "Status": "queued" } (enfileirado, não entregue), aceitam Idempotency-Key, recusam chave de servidor vinda de navegador (403 ServerKeyInBrowser) e respondem 503 com Retry-After se o serviço do canal estiver fora. Cada uma tem o seu escopo, para você liberar só o que a integração usa.
rota
escopo
corpo
regra do canal
POST /v1/telegram/send
telegram:send
ChatId (texto ou inteiro), Text (até 4096), BotId opcional (tgb_…)
o destinatário precisa ter aberto o bot e enviado /start; senão 422 ChatNaoIniciado
POST /v1/whatsweb/send
whatsweb:send
To (E.164, + e 8 a 15 dígitos), Text (até 4096), SessionId opcional (wws_…)
só responde a chats que já existem; senão 422 ChatInexistente
POST /v1/telegramconta/send
telegramconta:send
To (@usuario, telefone ou id), Text (até 4096), SessionId opcional (tgc_…)
só escreve a quem a conta já tem conversa; senão 422 DialogoInexistente
Sem BotId/SessionId, a conta usa o bot ou a sessão configurados. Bot, sessão e número se conectam no painel da respectiva ferramenta. Erros: 400 ValidationError (campos em Details), 404 (bot ou sessão não encontrado), 409 IdempotencyConflict. Como são canais não oficiais, estão sujeitos às regras e aos bloqueios das plataformas; prefira o WhatsApp oficial para campanhas.
Conta, uso e identidade
# Dados da conta da chave (qualquer chave válida)
curl 'https://api.asender.net/v1/account' -H 'Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxx'
# { "TenantId", "Name", "Plan", "Status", "ApiKeyId", "ApiKeyScope", "RequestId" }
# Consumo do mês (escopo account:read)
curl 'https://api.asender.net/v1/account/usage' -H 'Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxx'
# { "Usage": { "Period", "EmailsSent", "SmsSent", "PushSent", "ApiCalls" }, "RequestId" }
GET /v1/me é só para token de app com o escopo openid: devolve quem autorizou o app e em que conta, nunca os dados de outra:
email e name só aparecem se o token foi emitido com os escopos email e profile. Uma chave sk_… recebe 403 nesta rota.
Dados (leitura do que a conta tem)
As rotas de dados são de leitura (a única escrita é a de leads, abaixo) e servem a integrações de relatório, sincronização com BI/CRM externo e a apps de terceiro. Aceitam a chave sk_… com o escopo da rota e o token de app (neste caso o usuário que autorizou precisa ter acesso ao produto da conta; remover o acesso dele passa a valer em até 60 s).
escopo
rotas
produto exigido do usuário (token de app)
leads:read
GET /v1/leads, /v1/leads/{id}
CRM
leads:write
POST /v1/leads, PATCH /v1/leads/{id} (só chave, nunca token de app)
—
forms:read
GET /v1/forms, /v1/forms/{id}/submissions
Páginas
conversions:read
GET /v1/conversions
CRM
campaigns:read
GET /v1/campaigns, /{id}, /{id}/report, GET /v1/reports/email/overview, /timeseries, /top-emails, /domains
E-mail Marketing
messaging:read
GET /v1/messaging/{sms,whatsapp,push}/messages, …/reports/overview
o produto do canal
analytics:read
GET /v1/analytics/overview, /breakdown, /timeseries, /forms
Páginas
conversations:read
GET /v1/conversations, /v1/conversations/{id}/messages
Atendimento
journeys:read
GET /v1/journeys, /v1/journeys/{id}
CRM
Convenções (diferentes das rotas de mensageria acima):
snake_case nos campos e envelope de erro { "error": { "type", "message" } } (minúsculo), não o { "Error": … } das rotas de envio.
Lista:{ "data": [...], "next_cursor": "…" | null }. limit de 1 a 200 (padrão 50); cursor é o next_cursor da página anterior, opaco.
Incremental:updated_after onde o recurso é alterável, created_after onde é imutável (RFC 3339). Faça upsert pelo id.
Recurso único vem como objeto, sem envelope. Id inexistente ou de outra conta é sempre 404 idêntico, nunca 403.
Origem do dado fora do ar: 503 com Retry-After, só na rota afetada.
Os campos de cada recurso, a sincronização incremental recomendada e os erros estão no guia Entrar com Asender (seção 9) e na referência OpenAPI. As conversões enviadas a Meta, Google e TikTok (GET /v1/conversions) são explicadas em Conversões e conectores.
Escrever leads
POST /v1/leads cria e PATCH /v1/leads/{id} atualiza um contato do CRM da conta da chave. Escopo leads:write, só chave. O corpo é fechado (campo desconhecido, como tenant_id, id ou os nomes em português, é 400):
campo
observação
email, phone
ao menos um na criação; guardados cifrados; outro lead da conta com o mesmo e-mail/telefone → 409 Conflict
first_name, last_name, job_title, department, country (ISO-2 maiúsculo), state, city, language, linkedin
texto; no PATCH, "" limpa os opcionais
stage
novo, contatado, qualificado, cliente, descartado
tags
até 50; no PATCH a lista substitui a atual
custom_fields
até 60 chaves de texto; no PATCH mescla e "" remove a chave
Idempotency-Key (24 h por conta e rota): repetir a chave com o mesmo corpo devolve o mesmo resultado (Idempotent-Replayed: true) sem criar outro lead; a mesma chave com outro corpo é 422 IdempotencyKeyReused; chamada anterior ainda em curso, 409 IdempotencyInProgress (Retry-After). PATCH em lead inexistente ou de outra conta é 404 idêntico. A origem do lead fica registrada como api. Os mesmos recursos estão no servidor MCP (asender_leads_create, asender_leads_update).
Idempotência (Idempotency-Key)
Mande Idempotency-Key (1 a 128 caracteres de [A-Za-z0-9._:-]; o header vale mais que o campo IdempotencyKey do corpo) em todo envio que possa ser repetido por retry. Chave fora desse formato é recusada com 422 ValidationError (nunca é ignorada em silêncio).
você manda
resposta
chave nova
202, ReusedIdempotency: false
a mesma chave e o mesmo corpo
202 com os ids da mensagem original e ReusedIdempotency: true — nada é reenviado nem cobrado de novo
a mesma chave com corpo diferente
409 IdempotencyConflict — o corpo novo não é enviado e a mensagem original não muda
{
"Error": {
"Type": "Sender",
"Code": "IdempotencyConflict",
"Message": "The Idempotency-Key was already used with a different request body. Send a new key for a new message, or resend the original payload to replay it."
},
"RequestId": "req_…"
}
O "mesmo corpo" ignora a ordem dos campos e o campo IdempotencyKey. No lote de e-mail a chave vale por item (<chave>:<índice>) e cada item traz ReusedIdempotency. A comparação de corpo vale hoje para SMS, push, WhatsApp e os canais não oficiais; no e-mail, até o canal migrar para a ferramenta dedicada, o replay devolve o 202 original sem comparar o corpo — use uma chave nova por mensagem nova.
Membros — GET/POST /api/tenants/{id}/members, DELETE …/{userId}.
Convites — GET/POST /api/tenants/{id}/invitations, DELETE …/{token}, POST /api/invitations/accept (não exige pertinência — quem aceita ainda não pertence).
API keys — GET/POST /api/tenants/{id}/api-keys, DELETE …/{keyId}.
Logs da API — GET /api/tenants/{id}/logs, …/logs/{logId}, …/logs/series, …/logs/resumo (somente owner e admin; developer e viewer levam 403).
Plano — GET/PUT /api/tenants/{id}/plano, créditos avulsos, uso.
Identidade da sessão — /api/auth/me, e-mails de recuperação, dispositivos, segundo fator, sessões (piso 17).
Em toda rota /api/tenants/{id}/… o {id} vem do cliente e é autorizado contra a pertinência do usuário antes de qualquer hop ao core; conta alheia responde 404.
Superfície interna (não pública). O controle de plataforma — suporte, moderação, risco e quota — é plano de controle, serviço-a-serviço na malha privada. Não é uma superfície de integração e não é documentado aqui; ele existe atrás do BFF, que é o que responde em api.asender.net.
Registro de mudanças
data
mudança
quem é afetado
2026-10-02
Quebra de contrato.POST /v1/emails/send e /v1/emails/batch recusam Source de domínio não verificado com 422 RemetenteNaoVerificado (antes: 202 e envio com o remetente da plataforma + Reply-To). Verificação fora → 503 VerificacaoIndisponivel. Mensagem cujo domínio perde a verificação depois do 202 fica failed, nunca sai com outro remetente.
Integrações que mandavam Source de domínio não verificado.
2026-10-06
Idempotência e push.Idempotency-Key repetida com corpo diferente passa a dar 409 IdempotencyConflict em SMS, push e WhatsApp (antes: 202 com o id da 1ª mensagem); ReusedIdempotency passa a vir true no replay de SMS e push (antes: sempre false). POST /v1/push/send recusa Topic com 400 ValidationError (envio por tópico nunca funcionou: respondia 422). JSON com campo desconhecido nomeia o campo na mensagem do 400.
Integrações que reusavam a chave para corpos diferentes ou enviavam push por Topic.
2026-10-07
Quebra de contrato: chaves sem escopo deixam de funcionar. Toda API key precisa de escopos do catálogo. Chave sem lista de escopos, full_access, send_only ou com emails:* recebe 403 InsufficientScope em toda rota (exceto GET /v1/account); a emissão de chave sem escopos, ou com valor fora do catálogo, responde 400 ValidationError com a lista válida.
Quem usa chave emitida antes desta data sem escopo do catálogo: gere outra no painel de chaves, com os escopos mínimos.
2026-10-07
Erros de rota em /v1/*.404 e 405 agora vêm no envelope { "Error": { "Type", "Code", "Message" }, "RequestId" } (CodeNotFound / MethodNotAllowed), com Allow no 405 e uma dica quando o caminho é de outra API (POST /v1/emails é da Mail API; /v1/dominios é da API de Páginas). Antes: 404 page not found em texto e 405 sem corpo.
Clientes que tratavam o corpo do 404/405 como texto.
2026-10-07
Novo: GET /v1/senders. Lista os domínios (com Verified e DnsStatus) e as caixas ativas que a chave pode usar como from; escopo email:send.
Quem precisava descobrir remetentes válidos antes de enviar.
2026-10-07
Novo: log de requisições da API.GET /v1/logs e GET /v1/logs/{id} mostram o que a sua integração mandou e o que a API respondeu (corpos redigidos, filtros, paginação por cursor, ligação com message_ids). Novo escopo logs:read; chaves existentes não o ganham — emita uma chave com o escopo para ler.
Quem quer depurar integrações sem pedir logs ao suporte.
2026-10-20
Guia completo. Escopos telegram:send, whatsweb:send, telegramconta:send e os de leitura de dados (leads:read, conversions:read, analytics:read, campaigns:read, messaging:read, conversations:read, journeys:read) documentados; rotas de canais não oficiais, GET /v1/me, /v1/push/site/devices, rotas de dados, token de app OAuth e o MCP (em implantação) acrescentados. Sem mudança de contrato.
Quem integra: o catálogo agora tem 24 escopos.
2026-10-20
Novo: escrita de leads.POST /v1/leads e PATCH /v1/leads/{id} (escopo leads:write, só chave, com Idempotency-Key); o MCP ganha asender_emails_send, asender_leads_create e asender_leads_update. Aditivo: nada existente muda.
Quem integra: o catálogo passa a ter 28 escopos; app OAuth continua sem escrita.
Referência
Toda operação registrada no código, com o que faz, o que recebe e o que devolve.
bff-auth
GET/api/auth/2fa
Estado do segundo fator do usuário da sessão.
Onde é usada
tela de segurança do painel.
Efeitos
uma leitura no asender-auth. Responde o estado CONFIRMADO, e não "existe segredo": um setup interrompido deixa segredo gravado sem confirmação, e mostrar "2FA ligado" aí faria a pessoa acreditar numa proteção que o login não exige.
Respostas
código
significado
200
OK
401
credencial ausente ou inválida
Exemplo
curl -X GET 'https://api.asender.net/api/auth/2fa' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/auth/2fa
Segunda etapa do login — confirma o código do segundo fator.
Onde é usada
tela /2fa, para onde o login manda quem tem segundo fator.
Efeitos
cria sessão no asender-auth. # Por que esta rota é PÚBLICA Quem chega aqui ainda não tem sessão — é justamente o que ela está tentando obter. Exigir sessão tornaria o 2FA impossível de completar: a armadilha de aplicar a mesma guarda em toda rota "porque é mais seguro". O que protege é o par (TICKET de desafio, código): o ticket é assinado pelo Login e prova que a senha bateu neste fluxo (um user_id cru não abre mais nada — rodada 3, H-1), e o código vale 30 segundos. Sem esta rota, o login de quem tem 2FA ficava sem passo seguinte — a conta ficava inacessível pelo painel.
Respostas
código
significado
200
OK
401
`TwoFactorFailed` (código errado), `TwoFactorSuperseded` (código
substituído por um mais novo — use o e-mail mais recente),
`TwoFactorCodeExpired` (código vencido — peça outro) ou
`TwoFactorExpired` (o ticket do login venceu — recomece o login).
429
`TooManyAttempts` — freio do código; `Retry-After` (segundos) e
`Details.RetryAfterSeconds` quando o prazo é conhecido.
Reenvia o código de e-mail do 2º fator (método `email`).
Público como o /api/auth/2fa: a identidade vem do TICKET de desafio (corpo {TwoFactorToken}). 401 sem ticket; 429 no cap de emissões.
Onde é usada
botão "reenviar" da tela /2fa (método 'email').
Efeitos
um e-mail (o guard do auth decide se sai fora de prd); rate-limited no auth (cap por janela). # PÚBLICA, pelo mesmo motivo do /api/auth/2fa Quem está no passo do código ainda não tem sessão. O que protege é o TICKET de desafio (prova a senha deste login) — sem ele, 401. O 429 do upstream é repassado como 429: quem pediu demais precisa saber que basta esperar.
Respostas
código
significado
200
Código reenviado.
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
grava o segredo PENDENTE de confirmação. # A senha é a invariante do piso 17, e ela para AQUI se faltar Para mexer num fator é preciso apresentar um fator diferente dele. O serviço recusa senha vazia, e o gateway recusa antes — não por desconfiança do serviço, mas porque um corpo sem senha é pedido malformado, e mandá-lo adiante gastaria uma viagem para receber a mesma recusa.
Revoga a sessão do Bearer apresentado. IDEMPOTENTE.
Revoga no asender_auth_api; a validação de sessão passou a consultar o estado, então o token deixa de valer (401 no /api/auth/me seguinte).
Esta rota fica FORA do grupo SessionAuth, de propósito — é o único caso em /api/*. Dentro do grupo, o segundo logout (retry, aba duplicada, refresh) morria em 401 no middleware, quebrando a idempotência de uma operação que descreve um ESTADO desejado ("estar fora"), não uma transição.
Consequência observável, e é por isso que a classe declarada é bearer-unverified e não session: o handler exige o header Authorization (sem ele, 401) mas não valida a sessão. Qualquer string não vazia como Bearer — inclusive uma API key ou lixo — recebe 200. Não abre nada (revogar um token que não existe é no-op no asender_auth_api), mas o 401 desta rota vem do HANDLER, não do middleware: quem for auditar a superfície não pode contá-la como autenticada.
Onde é usada
chamado pelas rotas /api/auth/logout dos DOIS frontends (dashboard e backoffice), que depois apagam o próprio cookie HttpOnly.
Entradas
header Authorization: Bearer <jwt de sessão>.
Saídas
200 {Status:"ok"} com a sessão revogada; 401 sem Bearer; 502 LogoutFailed quando o asender_auth_api não confirmou.
Efeitos
marca auth.sessions.revoked_at no asender-auth. DEFEITO QUE ISTO FECHA (mesma família do item 17 do _INTEGRACAO-pendente): o handler logava a falha em nível Warn e respondia **200 {"Status":"ok"} assim mesmo. A premissa embutida era "logout é best-effort, o cliente só precisa apagar o cookie" — errada pelo mesmo motivo que a do backoffice: com a revogação por sid no asender_auth_api, é a resposta desta rota que diz se a sessão morreu ou não. Afirmar ok sem revogar dá ao chamador (e à tela) a garantia de que a sessão acabou quando ela continua aceita, e ainda apaga o rastro: quem lê 200 não procura o Warn no log. Por que 502 e não 500: a falha é do upstream (asender-auth fora, ou erro de banco na revogação), não desta camada — mesmo tratamento dado a todo erro de upstream do BFF. Falta de credencial continua sendo 401 do próprio handler: esta rota fica FORA do grupo SessionAuth de propósito (ver server.go).
Respostas
código
significado
200
Revogado, ou já estava — inclusive para um Bearer que nunca foi sessão.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
TenantId vem do asender_auth_api e costuma vir vazio — a resolução real do tenant é feita por rota (resolveSessionTenant), a partir dos vínculos no core. Não use este campo como escopo.
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
Exemplo
curl -X GET 'https://api.asender.net/api/auth/me' \
-H 'Authorization: Bearer SEU_TOKEN'
PATCH/api/auth/me
Altera o NOME do usuário da sessão.
Só Name. Email no corpo → 400 UseEmailsEndpoint (achado F1 da "Minha conta"): o e-mail só muda por /api/me/emails*, com reautenticação e verificação. Campo desconhecido → 400.
Onde é usada
tela de perfil do painel.
Efeitos
escreve no asender_auth_api. # E-mail não muda por aqui (achado F1 do plano da "Minha conta") Este PATCH trocava o e-mail PRIMÁRIO sem reautenticação: sessão sequestrada → troca o e-mail → "esqueci a senha" chega ao atacante. O e-mail agora só muda pelo fluxo /api/me/emails* (reauth por fator ≠ senha, verificação do endereço novo, aviso a todos os verificados). Email no corpo → 400 UseEmailsEndpoint, antes de qualquer hop — inclusive junto com um Name válido (aceitar o nome e ignorar o e-mail em silêncio faria o painel achar que trocou).
Respostas
código
significado
200
OK
400
`UseEmailsEndpoint` (veio `Email`), `ValidationError` (Name vazio) ou campo desconhecido.
Reenvia o e-mail de verificação do usuário da sessão.
Onde é usada
aviso "confirme seu e-mail" do painel.
Efeitos
um e-mail (o guard do auth decide se ele sai fora de prd). O 429 do upstream é REPASSADO como 429: quem pediu demais precisa saber que basta esperar, e um 502 aqui mandaria a pessoa procurar defeito onde não há.
Começa o login SEM senha com chave de acesso (público, teto por IP do auth).
Corpo {origem} (o authfront põe a origem do browser, conferida contra o host). Sem e-mail: o autenticador escolhe a conta. Resposta do auth repassada: {desafio_id, opcoes}.
Onde é usada
botão "Entrar com chave de acesso" do authfront (via route handler server-side).
Respostas
código
significado
200
`{desafio_id, opcoes}`.
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
Inicia a recuperação de senha (resposta neutra, não vaza se a conta existe).
Onde é usada
tela pública /forgot-password.
Efeitos
um e-mail de recuperação (o guard do auth decide se sai fora de prd, piso 18). Nenhuma sessão é criada. # PÚBLICA e com resposta NEUTRA Quem pede recuperação não tem sessão — é o ponto. A resposta é sempre a mesma, exista ou não o e-mail: dizer "esse e-mail não existe" entregaria a base de usuários a quem testa endereços (enumeração). O token do link NUNCA volta no corpo — o AuthClient.PasswordResetRequest o descarta ainda no cliente; quem recebe o link é a caixa de e-mail. Vive no MESMO teto por IP das demais rotas públicas de auth (server.go), que fecha o brute-force/spam deste endpoint.
Redefine a senha pelo token do link (+ código 2FA quando a conta tem segundo fator).
Onde é usada
tela pública /reset-password, que recebe ?token= do link do e-mail.
Efeitos
grava a senha nova e DERRUBA todas as sessões abertas do usuário (lá no auth). Nenhuma sessão nova é criada — a pessoa faz login em seguida. # PÚBLICA, e os 401 são separados pelo Code Quem redefine a senha não tem sessão (esqueceu a senha). O auth responde 401 para TRÊS situações distintas — token inválido/expirado, 2FA exigido e 2FA incorreto — cada uma com um Code próprio no envelope. Separá-las é o que permite à tela pedir o código quando falta, sem transformar "código errado" em "link inválido". Senha fraca vem como 400 (mesma política do cadastro: mínimo 12 + HIBP) e sobe como 422 com a razão, para a pessoa saber o que corrigir.
Respostas
código
significado
200
senha redefinida; sessões do usuário derrubadas
400
token ou nova senha ausentes
401
token inválido/expirado, 2FA exigido ou 2FA incorreto (distinguidos pelo Error.Code)
422
nova senha não atende à política (mínimo 12 + HIBP)
Dispara o código do 2º fator (canal e-mail) do reset em curso.
Pública como o resto do fluxo de reset: quem esqueceu a senha não tem sessão. Recebe o Token do reset em andamento e envia o OTP ao e-mail primário do dono do token. Resposta neutra — não confirma se o token vale. Disparo de e-mail (piso 18) e alvo de spam/amplificação, então no MESMO teto de rate-limit por IP das outras rotas de auth públicas.
Onde é usada
tela pública /reset-password, ao montar com um ?token= válido e no botão "reenviar".
Efeitos
um e-mail com o código (o guard do auth decide se sai fora de prd, piso 18). Nenhuma sessão é criada. # PÚBLICA, resposta NEUTRA, no MESMO teto por IP Como o resto do reset, é pública e responde sempre igual (não confirma se o token/conta existe — anti-enumeração). Vive no rate-limit por IP das rotas públicas de auth (server.go), que — somado ao cap de reenvio do próprio auth — impede que vire amplificador de e-mail (piso 18). Falha do upstream vira 502 (idêntico para qualquer token, não vaza enumeração) e vai ao log.
Respostas
código
significado
200
Aceito (resposta neutra `{"Status":"sent"}`; não confirma o token).
400
Token ausente.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Cria o usuário e, quando pode, o tenant raiz dele.
O status distingue os dois desfechos, e isso é contrato.
201 = usuário E conta criados. 202 = usuário criado, conta não — com um Reason do conjunto fechado DomainClaimed | TenantNameTaken | ProvisioningFailed | Invited (cadastro por convite, sem equipe de propósito: o próximo passo é o aceite). O cliente ramifica pelo Reason e leva a pessoa ao onboarding; tratar 202 como sucesso liso produz conta ÓRFÃ (relato do dono, 2026-09-17): usuário autenticado que não pertence a conta nenhuma, invisível para qualquer admin.
DomainClaimed significa que o domínio do e-mail pertence a uma conta que já o verificou. Não nasce workspace paralelo: nasce um pedido de entrada, e AccessRequestId o identifica.
O campo Note em prosa saiu: texto serve para mostrar, não para ramificar.
Respostas
código
significado
201
Usuário e conta criados.
202
Usuário criado, conta NÃO. O `Reason` diz por quê e o cliente
encaminha para o onboarding.
400
`400 ValidationError` — JSON inválido, campo desconhecido, e-mail ou
senha fora da regra. Com `ConviteToken`: `InviteInvalid` (convite
inexistente, aceito ou expirado) ou `InviteEmailMismatch` (e-mail do
cadastro diferente do convite). Nos dois, nenhum usuário é criado.
409
`409 Conflict` — email já registrado, slug em uso.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
uma leitura no asender-auth. O token repassado é o da REQUISIÇÃO, e é ele que define de quem é a lista. Não existe parâmetro de usuário: um ?user_id= seria a rota mais barata do sistema para descobrir de onde outra pessoa se conecta.
Respostas
código
significado
200
OK
401
credencial ausente ou inválida
Exemplo
curl -X GET 'https://api.asender.net/api/auth/sessions' \
-H 'Authorization: Bearer SEU_TOKEN'
DELETE/api/auth/sessions/{id}
Revoga uma sessão específica.
Onde é usada
botão "remover" da tela de segurança.
Efeitos
a sessão para de autenticar imediatamente. # Por que a sessão de suporte NÃO passa por aqui A sessão de suporte é somente leitura (ADR-0017), e o middleware de suporte já recusa escrita. Isto é uma escrita — e derrubar o dispositivo de um cliente durante uma sessão de suporte é exatamente o poder que o ADR tira de quem entra na conta alheia.
carimba email_verified_at no auth. # Também é PÚBLICA Quem abre o link do e-mail pode estar em outro navegador, ou nem ter sessão. Exigir login para verificar o e-mail cria a dependência circular clássica — e a pessoa que mais precisa verificar é justamente a que ainda não conseguiu entrar.
Respostas
código
significado
200
OK
401
link inválido
409
email_em_uso — o endereço já está verificado em outra conta (token intacto)
Reenvia o link de confirmação sem sessão (202 neutro, teto por IP).
Onde é usada
tela /verify quando o link falhou e o navegador não tem sessão.
Efeitos
no máximo um e-mail (guard do piso 18 no auth). # PÚBLICA, resposta NEUTRA, no MESMO teto por IP 202 idêntico exista ou não pendência para o endereço (o auth só envia se houver pendência viva). O 429 é por IP, igual para qualquer endereço.
Prévia do convite pelo token do link — PÚBLICA, teto por IP.
Pública por desenho: quem abre o link do convite ainda não tem sessão. O token (CSPRNG) é a credencial — só o portador vê o e-mail do PRÓPRIO convite. Não devolve token, id da conta nem quem convidou. Está na allowlist publicRoutes do routes_test.go.
HasAccount diz se o e-mail do convite já tem conta (a tela oferece "Entrar" ou "Criar conta com este e-mail"). Só é consultado com Status: pending; nos outros estados, e quando o asender_auth_api não responde (degradação graciosa), vem null.
Onde é usada
a tela do link de convite dos fronts, ANTES de login/cadastro; registrada no grupo público com teto por IP em server.go.
Entradas
{token} do caminho — o token do link.
Saídas
200 {Email, TenantName, Role, Status, HasAccount}; 404 NotFound para token desconhecido; 502 quando o core não respondeu.
Efeitos
uma leitura no core e, se pendente, uma no auth. Por que é pública: quem abre o link ainda não tem sessão. O token é CSPRNG e é a própria credencial do convite — só quem o recebeu por e-mail vê o e-mail do PRÓPRIO convite. Por isso a resposta não devolve o token, nem o id público da conta, nem quem convidou: só o necessário para a tela decidir o botão. HasAccount é complemento, não requisito (piso 10): auth fora → null, e a tela oferece os dois caminhos. Convite não pendente também leva null — não há decisão de botão a tomar, e não se consulta o auth à toa.
Parâmetros
nome
em
obrigatório
descrição
token
path
sim
Respostas
código
significado
200
Convite encontrado.
404
`404 NotFound` — token desconhecido.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/api/invitations/token_AQUI/preview' \
-H 'Authorization: Bearer SEU_TOKEN'
bff-configuracao
GET/api/flags
As feature flags avaliadas para a conta da sessão.
A conta vem do token verificado. Core fora = {Flags: null, Degradado: true}: a tela usa o padrão dela (piso 10).
Respostas
código
significado
200
OK
Exemplo
curl -X GET 'https://api.asender.net/api/flags' \
-H 'Authorization: Bearer SEU_TOKEN'
bff-dns
GET/api/tenants/{id}/dns/alteracoes
Histórico das alterações de DNS da conta.
Painel de DNS (ADR-0131, A3). O {id} é autorizado por pertinência (authorizeTenantParam; conta alheia responde 404) e por papel: só owner e admin (deny-default; developer, viewer, suporte e papel indeterminado levam 403 AdminRole). Tenant = o {id} autorizado, ator = o usuário da sessão, origem = tela; nada disso vem do cliente. Status e corpo do serviço de DNS voltam como vieram (RFC 9457 com codigo). Cache-Control: no-store.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
zona
query
não
Id da zona.
pagina
query
não
por_pagina
query
não
Respostas
código
significado
200
OK: `{alteracoes, total, pagina, por_pagina}`.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Membro sem papel owner/admin (`403 AdminRole`).
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
dns_api fora do ar (`ServiceUnavailable`, com `Retry-After`).
default
Erro RFC 9457 (`application/problem+json`) do serviço de DNS, repassado com o `codigo` estável (ZonaNaoEncontrada, ForaDePrd, RegistroProtegido, ConfirmacaoNecessaria, LimiteExcedido, ...).
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/dns/alteracoes' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/tenants/{id}/dns/conexoes
Lista as conexões Cloudflare da conta (nunca devolve o token).
Painel de DNS (ADR-0131, A3). O {id} é autorizado por pertinência (authorizeTenantParam; conta alheia responde 404) e por papel: só owner e admin (deny-default; developer, viewer, suporte e papel indeterminado levam 403 AdminRole). Tenant = o {id} autorizado, ator = o usuário da sessão, origem = tela; nada disso vem do cliente. Status e corpo do serviço de DNS voltam como vieram (RFC 9457 com codigo). Cache-Control: no-store.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Membro sem papel owner/admin (`403 AdminRole`).
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
dns_api fora do ar (`ServiceUnavailable`, com `Retry-After`).
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/dns/conexoes' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/tenants/{id}/dns/conexoes
Conecta um token da Cloudflare à conta.
Painel de DNS (ADR-0131, A3). O {id} é autorizado por pertinência (authorizeTenantParam; conta alheia responde 404) e por papel: só owner e admin (deny-default; developer, viewer, suporte e papel indeterminado levam 403 AdminRole). Tenant = o {id} autorizado, ator = o usuário da sessão, origem = tela; nada disso vem do cliente. Status e corpo do serviço de DNS voltam como vieram (RFC 9457 com codigo). Cache-Control: no-store. Corpo {rotulo, token} (token de 20 a 512 caracteres). O serviço verifica o token na Cloudflare, lista as zonas que ele alcança, cifra e grava; o token nunca volta em resposta e nunca é logado (nem no BFF, nem no log de requisições da API: o campo token é redigido). Corpo até 64 KiB.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Idempotency-Key
header
não
Chave de idempotência da escrita de DNS (1 a 128 caracteres de `[A-Za-z0-9._:-]`; malformada é
`400`). Vale 24 h por conta: a mesma chave com o mesmo pedido devolve o resultado original sem
tocar a Cloudflare de novo; a mesma chave com outro pedido é `422 IdempotencyKeyReused`.
Respostas
código
significado
201
Criada: a conexão, sem o token.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Membro sem papel owner/admin (`403 AdminRole`).
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
413
`413 PayloadTooLarge` — corpo acima de 2 MiB.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
dns_api fora do ar (`ServiceUnavailable`, com `Retry-After`).
default
Erro RFC 9457 (`application/problem+json`) do serviço de DNS, repassado com o `codigo` estável (ZonaNaoEncontrada, ForaDePrd, RegistroProtegido, ConfirmacaoNecessaria, LimiteExcedido, ...).
Painel de DNS (ADR-0131, A3). O {id} é autorizado por pertinência (authorizeTenantParam; conta alheia responde 404) e por papel: só owner e admin (deny-default; developer, viewer, suporte e papel indeterminado levam 403 AdminRole). Tenant = o {id} autorizado, ator = o usuário da sessão, origem = tela; nada disso vem do cliente. Status e corpo do serviço de DNS voltam como vieram (RFC 9457 com codigo). Cache-Control: no-store.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
cid
path
sim
Id da conexão.
Idempotency-Key
header
não
Chave de idempotência da escrita de DNS (1 a 128 caracteres de `[A-Za-z0-9._:-]`; malformada é
`400`). Vale 24 h por conta: a mesma chave com o mesmo pedido devolve o resultado original sem
tocar a Cloudflare de novo; a mesma chave com outro pedido é `422 IdempotencyKeyReused`.
Respostas
código
significado
200
OK: `{apagada: true, id}`.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Membro sem papel owner/admin (`403 AdminRole`).
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
dns_api fora do ar (`ServiceUnavailable`, com `Retry-After`).
default
Erro RFC 9457 (`application/problem+json`) do serviço de DNS, repassado com o `codigo` estável (ZonaNaoEncontrada, ForaDePrd, RegistroProtegido, ConfirmacaoNecessaria, LimiteExcedido, ...).
Painel de DNS (ADR-0131, A3). O {id} é autorizado por pertinência (authorizeTenantParam; conta alheia responde 404) e por papel: só owner e admin (deny-default; developer, viewer, suporte e papel indeterminado levam 403 AdminRole). Tenant = o {id} autorizado, ator = o usuário da sessão, origem = tela; nada disso vem do cliente. Status e corpo do serviço de DNS voltam como vieram (RFC 9457 com codigo). Cache-Control: no-store.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Membro sem papel owner/admin (`403 AdminRole`).
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
dns_api fora do ar (`ServiceUnavailable`, com `Retry-After`).
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/dns/zonas' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/tenants/{id}/dns/zonas/{zona}/registros
Registros de uma zona.
Painel de DNS (ADR-0131, A3). O {id} é autorizado por pertinência (authorizeTenantParam; conta alheia responde 404) e por papel: só owner e admin (deny-default; developer, viewer, suporte e papel indeterminado levam 403 AdminRole). Tenant = o {id} autorizado, ator = o usuário da sessão, origem = tela; nada disso vem do cliente. Status e corpo do serviço de DNS voltam como vieram (RFC 9457 com codigo). Cache-Control: no-store.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
zona
path
sim
Id da zona (o `id` de `GET /v1/dns/zones`).
tipo
query
não
nome
query
não
pagina
query
não
por_pagina
query
não
Respostas
código
significado
200
OK: `{registros, total, pagina, por_pagina}`.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Membro sem papel owner/admin (`403 AdminRole`).
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
dns_api fora do ar (`ServiceUnavailable`, com `Retry-After`).
default
Erro RFC 9457 (`application/problem+json`) do serviço de DNS, repassado com o `codigo` estável (ZonaNaoEncontrada, ForaDePrd, RegistroProtegido, ConfirmacaoNecessaria, LimiteExcedido, ...).
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/dns/zonas/zona_AQUI/registros' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/tenants/{id}/dns/zonas/{zona}/registros
Cria um registro (aceita `?seco=1`).
Painel de DNS (ADR-0131, A3). O {id} é autorizado por pertinência (authorizeTenantParam; conta alheia responde 404) e por papel: só owner e admin (deny-default; developer, viewer, suporte e papel indeterminado levam 403 AdminRole). Tenant = o {id} autorizado, ator = o usuário da sessão, origem = tela; nada disso vem do cliente. Status e corpo do serviço de DNS voltam como vieram (RFC 9457 com codigo). Cache-Control: no-store. Controles: ?seco=1 (plano sem alterar), X-Asender-Confirmar (nome do registro; MX/SPF/DMARC do apex) e Idempotency-Key, repassados ao serviço.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
zona
path
sim
Id da zona (o `id` de `GET /v1/dns/zones`).
seco
query
não
Devolve o plano sem tocar a Cloudflare.
X-Asender-Confirmar
header
não
Nome completo do registro. **Obrigatório** no `DELETE` e em qualquer escrita em `MX`, SPF ou
DMARC do apex (`428 ConfirmationRequired` sem ele). Um valor diferente do nome também é recusado.
Idempotency-Key
header
não
Chave de idempotência da escrita de DNS (1 a 128 caracteres de `[A-Za-z0-9._:-]`; malformada é
`400`). Vale 24 h por conta: a mesma chave com o mesmo pedido devolve o resultado original sem
tocar a Cloudflare de novo; a mesma chave com outro pedido é `422 IdempotencyKeyReused`.
Respostas
código
significado
200
OK (plano com `seco`); sem `seco` o serviço responde 201 com `{registro}`.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Membro sem papel owner/admin (`403 AdminRole`).
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
413
`413 PayloadTooLarge` — corpo acima de 2 MiB.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
dns_api fora do ar (`ServiceUnavailable`, com `Retry-After`).
default
Erro RFC 9457 (`application/problem+json`) do serviço de DNS, repassado com o `codigo` estável (ZonaNaoEncontrada, ForaDePrd, RegistroProtegido, ConfirmacaoNecessaria, LimiteExcedido, ...).
Painel de DNS (ADR-0131, A3). O {id} é autorizado por pertinência (authorizeTenantParam; conta alheia responde 404) e por papel: só owner e admin (deny-default; developer, viewer, suporte e papel indeterminado levam 403 AdminRole). Tenant = o {id} autorizado, ator = o usuário da sessão, origem = tela; nada disso vem do cliente. Status e corpo do serviço de DNS voltam como vieram (RFC 9457 com codigo). Cache-Control: no-store. Exige X-Asender-Confirmar com o nome do registro (428 ConfirmacaoNecessaria sem ele); aceita ?seco=1 e Idempotency-Key.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
zona
path
sim
Id da zona (o `id` de `GET /v1/dns/zones`).
rid
path
sim
Id do registro (o `id` de `GET /v1/dns/zones/{zona}/records`).
seco
query
não
Devolve o plano sem tocar a Cloudflare.
X-Asender-Confirmar
header
não
Nome completo do registro. **Obrigatório** no `DELETE` e em qualquer escrita em `MX`, SPF ou
DMARC do apex (`428 ConfirmationRequired` sem ele). Um valor diferente do nome também é recusado.
Idempotency-Key
header
não
Chave de idempotência da escrita de DNS (1 a 128 caracteres de `[A-Za-z0-9._:-]`; malformada é
`400`). Vale 24 h por conta: a mesma chave com o mesmo pedido devolve o resultado original sem
tocar a Cloudflare de novo; a mesma chave com outro pedido é `422 IdempotencyKeyReused`.
Respostas
código
significado
200
OK: `{apagado: registro}` ou o plano.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Membro sem papel owner/admin (`403 AdminRole`).
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
dns_api fora do ar (`ServiceUnavailable`, com `Retry-After`).
default
Erro RFC 9457 (`application/problem+json`) do serviço de DNS, repassado com o `codigo` estável (ZonaNaoEncontrada, ForaDePrd, RegistroProtegido, ConfirmacaoNecessaria, LimiteExcedido, ...).
Altera parcialmente um registro (aceita `?seco=1`).
Painel de DNS (ADR-0131, A3). O {id} é autorizado por pertinência (authorizeTenantParam; conta alheia responde 404) e por papel: só owner e admin (deny-default; developer, viewer, suporte e papel indeterminado levam 403 AdminRole). Tenant = o {id} autorizado, ator = o usuário da sessão, origem = tela; nada disso vem do cliente. Status e corpo do serviço de DNS voltam como vieram (RFC 9457 com codigo). Cache-Control: no-store. Controles: ?seco=1, X-Asender-Confirmar e Idempotency-Key, repassados ao serviço.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
zona
path
sim
Id da zona (o `id` de `GET /v1/dns/zones`).
rid
path
sim
Id do registro (o `id` de `GET /v1/dns/zones/{zona}/records`).
seco
query
não
Devolve o plano sem tocar a Cloudflare.
X-Asender-Confirmar
header
não
Nome completo do registro. **Obrigatório** no `DELETE` e em qualquer escrita em `MX`, SPF ou
DMARC do apex (`428 ConfirmationRequired` sem ele). Um valor diferente do nome também é recusado.
Idempotency-Key
header
não
Chave de idempotência da escrita de DNS (1 a 128 caracteres de `[A-Za-z0-9._:-]`; malformada é
`400`). Vale 24 h por conta: a mesma chave com o mesmo pedido devolve o resultado original sem
tocar a Cloudflare de novo; a mesma chave com outro pedido é `422 IdempotencyKeyReused`.
Respostas
código
significado
200
OK: `{registro}` ou o plano.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Membro sem papel owner/admin (`403 AdminRole`).
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
413
`413 PayloadTooLarge` — corpo acima de 2 MiB.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
dns_api fora do ar (`ServiceUnavailable`, com `Retry-After`).
default
Erro RFC 9457 (`application/problem+json`) do serviço de DNS, repassado com o `codigo` estável (ZonaNaoEncontrada, ForaDePrd, RegistroProtegido, ConfirmacaoNecessaria, LimiteExcedido, ...).
Minha conta: usuário, fatores e reauth vigente (repasse de GET /v1/me).
Onde é usada
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Respostas
código
significado
200
Corpo do contrato §4.2 do auth, repassado sem reinterpretação.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ReauthRequired` (com `Error.Details.metodos_aceitos`, `Error.Details.metodos` — cada método da política com `disponivel` e o `motivo` do indisponível, ex. `EmailNaoVerificado` (P5) — e, quando `email_otp` é aceito, `Error.Details.destinos` [{email_id, mascarado, primario}]), `ReauthIndisponivel`, `SessaoNecessaria` (leitura de segurança — e-mails, fatores, sessões, trilha — com credencial sem sessão do auth) ou `ImpersonationReadOnly` (sessão de suporte não escreve).
404
Rota fora da allowlist do contrato, ou recurso inexistente/alheio.
502
auth indisponível.
Exemplo
curl -X GET 'https://api.asender.net/api/me' \
-H 'Authorization: Bearer SEU_TOKEN'
PATCH/api/me
Troca o nome (repasse de PATCH /v1/me).
Onde é usada
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Respostas
código
significado
200
Corpo do contrato §4.2 do auth, repassado sem reinterpretação.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ReauthRequired` (com `Error.Details.metodos_aceitos`, `Error.Details.metodos` — cada método da política com `disponivel` e o `motivo` do indisponível, ex. `EmailNaoVerificado` (P5) — e, quando `email_otp` é aceito, `Error.Details.destinos` [{email_id, mascarado, primario}]), `ReauthIndisponivel`, `SessaoNecessaria` (leitura de segurança — e-mails, fatores, sessões, trilha — com credencial sem sessão do auth) ou `ImpersonationReadOnly` (sessão de suporte não escreve).
404
Rota fora da allowlist do contrato, ou recurso inexistente/alheio.
Gera 10 códigos de recuperação (reauth totp|email_otp).
Onde é usada
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Respostas
código
significado
200
`{codigos:[10]}` no formato XXXX-XXXX-XXXX (auth MeCodigos), mostrados uma vez; os anteriores deixam de valer.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ReauthRequired` (com `Error.Details.metodos_aceitos`, `Error.Details.metodos` — cada método da política com `disponivel` e o `motivo` do indisponível, ex. `EmailNaoVerificado` (P5) — e, quando `email_otp` é aceito, `Error.Details.destinos` [{email_id, mascarado, primario}]), `ReauthIndisponivel`, `SessaoNecessaria` (leitura de segurança — e-mails, fatores, sessões, trilha — com credencial sem sessão do auth) ou `ImpersonationReadOnly` (sessão de suporte não escreve).
404
Rota fora da allowlist do contrato, ou recurso inexistente/alheio.
Convites pendentes/expirados para os e-mails VERIFICADOS do usuário.
Onde é usada
seção Convites da "Minha conta" (auth_front /conta/convites).
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
503
`IdentityUnavailable` — auth fora (nunca lista vazia mentindo); `InvitationsUnavailable` — core fora.
Exemplo
curl -X GET 'https://api.asender.net/api/me/convites' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/me/convites/{id}/aceitar
Aceita convite por public_id (idempotente; herda acessos).
Onde é usada
cartão do convite. Idempotente (duplo clique = mesmo member).
Efeitos
pertinência no core + herança de acessos no auth (falha da herança não desfaz o aceite — mesma regra do link) + linha de auditoria no log.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Respostas
código
significado
200
Idempotente. `user_id` é o da SESSÃO e os e-mails são os verificados do auth — nada vem do corpo. Depois do aceite, a mesma herança de acessos do link (owner/admin herdam as ferramentas).
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
404
Inexistente, de outro e-mail, id fora do formato `cvt_…` ou usuário sem e-mail verificado (mesma resposta).
409
`Declined` ou `AlreadyAccepted`.
410
`Expired` ou `Revoked`.
429
`Throttling` — teto de 30 decisões de convite (aceitar+recusar) por usuário por hora, com `Retry-After`.
badge do AppHeader e faixa do hub (Frente E), em polling de 60s. QUALQUER falha (auth, core, sessão) vira 200 {pendentes:null}: o badge some e nada mais quebra (piso 10). Duas idas leves (auth + COUNT indexado).
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
Exemplo
curl -X GET 'https://api.asender.net/api/me/convites/contagem' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/me/emails
E-mails da conta.
Onde é usada
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Respostas
código
significado
200
Corpo do contrato §4.2 do auth, repassado sem reinterpretação.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ReauthRequired` (com `Error.Details.metodos_aceitos`, `Error.Details.metodos` — cada método da política com `disponivel` e o `motivo` do indisponível, ex. `EmailNaoVerificado` (P5) — e, quando `email_otp` é aceito, `Error.Details.destinos` [{email_id, mascarado, primario}]), `ReauthIndisponivel`, `SessaoNecessaria` (leitura de segurança — e-mails, fatores, sessões, trilha — com credencial sem sessão do auth) ou `ImpersonationReadOnly` (sessão de suporte não escreve).
404
Rota fora da allowlist do contrato, ou recurso inexistente/alheio.
502
auth indisponível.
Exemplo
curl -X GET 'https://api.asender.net/api/me/emails' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/me/emails
Adiciona e-mail — 202 neutro sempre (reauth totp se tiver, senão email_otp).
Onde é usada
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Respostas
código
significado
202
Neutro: `{ok:true}` exista ou não o endereço.
400
`ValidationError` (endereço inválido) ou `InvalidRequest` (campo desconhecido).
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ReauthRequired` (com `Error.Details.metodos_aceitos`, `Error.Details.metodos` — cada método da política com `disponivel` e o `motivo` do indisponível, ex. `EmailNaoVerificado` (P5) — e, quando `email_otp` é aceito, `Error.Details.destinos` [{email_id, mascarado, primario}]), `ReauthIndisponivel`, `SessaoNecessaria` (leitura de segurança — e-mails, fatores, sessões, trilha — com credencial sem sessão do auth) ou `ImpersonationReadOnly` (sessão de suporte não escreve).
404
Rota fora da allowlist do contrato, ou recurso inexistente/alheio.
429
`TooManyAttempts` — 10 e-mails adicionados por dia.
Remove e-mail (reauth totp se tiver, senão email_otp).
Onde é usada
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Respostas
código
significado
200
Corpo do contrato §4.2 do auth, repassado sem reinterpretação.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ReauthRequired` (com `Error.Details.metodos_aceitos`, `Error.Details.metodos` — cada método da política com `disponivel` e o `motivo` do indisponível, ex. `EmailNaoVerificado` (P5) — e, quando `email_otp` é aceito, `Error.Details.destinos` [{email_id, mascarado, primario}]), `ReauthIndisponivel`, `SessaoNecessaria` (leitura de segurança — e-mails, fatores, sessões, trilha — com credencial sem sessão do auth) ou `ImpersonationReadOnly` (sessão de suporte não escreve).
404
Rota fora da allowlist do contrato, ou recurso inexistente/alheio.
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Respostas
código
significado
200
Corpo do contrato §4.2 do auth, repassado sem reinterpretação.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ReauthRequired` (com `Error.Details.metodos_aceitos`, `Error.Details.metodos` — cada método da política com `disponivel` e o `motivo` do indisponível, ex. `EmailNaoVerificado` (P5) — e, quando `email_otp` é aceito, `Error.Details.destinos` [{email_id, mascarado, primario}]), `ReauthIndisponivel`, `SessaoNecessaria` (leitura de segurança — e-mails, fatores, sessões, trilha — com credencial sem sessão do auth) ou `ImpersonationReadOnly` (sessão de suporte não escreve).
404
Rota fora da allowlist do contrato, ou recurso inexistente/alheio.
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Respostas
código
significado
204
Link reenviado (ou nada a fazer: já verificado).
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ReauthRequired` (com `Error.Details.metodos_aceitos`, `Error.Details.metodos` — cada método da política com `disponivel` e o `motivo` do indisponível, ex. `EmailNaoVerificado` (P5) — e, quando `email_otp` é aceito, `Error.Details.destinos` [{email_id, mascarado, primario}]), `ReauthIndisponivel`, `SessaoNecessaria` (leitura de segurança — e-mails, fatores, sessões, trilha — com credencial sem sessão do auth) ou `ImpersonationReadOnly` (sessão de suporte não escreve).
404
Rota fora da allowlist do contrato, ou recurso inexistente/alheio.
422
`EnderecoNaoRecebe` — o servidor do destinatário recusou o endereço de vez.
429
`TooManyAttempts` — cap de reenvio por janela.
502
auth indisponível, ou `EnvioRecusado` (recusa permanente por política do destino).
Trilha de segurança paginada (só `antes` e `limite` atravessam).
Onde é usada
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Parâmetros
nome
em
obrigatório
descrição
antes
query
não
limite
query
não
Respostas
código
significado
200
Corpo do contrato §4.2 do auth, repassado sem reinterpretação.
400
`InvalidRequest` — cursor `antes` ou `limite` inválido.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ReauthRequired` (com `Error.Details.metodos_aceitos`, `Error.Details.metodos` — cada método da política com `disponivel` e o `motivo` do indisponível, ex. `EmailNaoVerificado` (P5) — e, quando `email_otp` é aceito, `Error.Details.destinos` [{email_id, mascarado, primario}]), `ReauthIndisponivel`, `SessaoNecessaria` (leitura de segurança — e-mails, fatores, sessões, trilha — com credencial sem sessão do auth) ou `ImpersonationReadOnly` (sessão de suporte não escreve).
404
Rota fora da allowlist do contrato, ou recurso inexistente/alheio.
502
auth indisponível.
Exemplo
curl -X GET 'https://api.asender.net/api/me/eventos' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/me/fatores
Inventário de fatores + remoções pendentes (leitura).
Onde é usada
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Respostas
código
significado
200
`{fatores:{senha,email_otp,totp,codigos_restantes,passkeys}, remocoes_pendentes:[{id,fator,executar_em}]}` (auth MeFatores; array vazio quando não há).
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
404
Rota fora da allowlist do contrato.
502
auth indisponível.
Exemplo
curl -X GET 'https://api.asender.net/api/me/fatores' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/me/passkeys
Chaves de acesso (passkeys) da conta (leitura; sem chave pública nem credential id).
Onde é usada
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`SessaoNecessaria` (credencial sem sessão do auth).
404
Rota fora da allowlist do contrato.
502
auth indisponível.
Exemplo
curl -X GET 'https://api.asender.net/api/me/passkeys' \
-H 'Authorization: Bearer SEU_TOKEN'
DELETE/api/me/passkeys/{id}
Remove uma chave de acesso (reauth email_otp|totp|codigo_recuperacao; último forte agenda 24h).
Onde é usada
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Respostas
código
significado
200
`{removida:true}` — saiu na hora.
202
`{id, fator:"passkey", agendado_para, passkey_id}` — último fator forte: remoção em 24h, cancelável.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ReauthRequired` ou `ImpersonationReadOnly`.
404
Inexistente ou de outra pessoa (indistinguíveis), ou rota fora da allowlist.
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Respostas
código
significado
200
`{passkey}` (repassado).
400
`ValidationError`.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`SessaoNecessaria` ou `ImpersonationReadOnly`.
404
Inexistente ou de outra pessoa (indistinguíveis), ou rota fora da allowlist.
Grava a chave de acesso (resposta do autenticador).
Onde é usada
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Respostas
código
significado
201
`{passkey}` (repassado).
400
`DesafioExpirado`, `PasskeyRecusada` ou `ValidationError`.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
Começa o cadastro de uma chave de acesso (reauth email_otp|senha|totp|passkey).
Onde é usada
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Respostas
código
significado
200
`{desafio_id, opcoes}` para navigator.credentials.create (repassado).
400
`OrigemRecusada` / corpo inválido.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ReauthRequired` (com `Details.metodos_aceitos`/`metodos`) ou `ImpersonationReadOnly`.
Confirma a reautenticação na sessão (repasse; 429 com Retry-After).
Onde é usada
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Respostas
código
significado
200
Corpo do contrato §4.2 do auth, repassado sem reinterpretação.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ReauthRequired` (com `Error.Details.metodos_aceitos`, `Error.Details.metodos` — cada método da política com `disponivel` e o `motivo` do indisponível, ex. `EmailNaoVerificado` (P5) — e, quando `email_otp` é aceito, `Error.Details.destinos` [{email_id, mascarado, primario}]), `ReauthIndisponivel`, `SessaoNecessaria` (leitura de segurança — e-mails, fatores, sessões, trilha — com credencial sem sessão do auth) ou `ImpersonationReadOnly` (sessão de suporte não escreve).
404
Rota fora da allowlist do contrato, ou recurso inexistente/alheio.
Corpo {"metodo": "...", "email_id": "<uuid opcional>"} — email_id só para email_otp: o e-mail VERIFICADO que recebe o código (conferido no auth: dono + verificado; outro → 400 MetodoIndisponivel). Resposta 202 (repassada): {metodo, destinos?: [{email_id, mascarado, primario}], enviado_para?} — destinos/enviado_para só para email_otp; primário primeiro, o 1º é o padrão.
Onde é usada
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Respostas
código
significado
202
Prova preparada; `destinos` e `enviado_para` para `email_otp`; `desafio_id` e `opcoes` (WebAuthn, base64url) para `passkey` (formato no description).
400
`MetodoIndisponivel` (método que a conta não tem, ou `email_id` alheio/pendente/inexistente).
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ReauthRequired` (com `Error.Details.metodos_aceitos`, `Error.Details.metodos` — cada método da política com `disponivel` e o `motivo` do indisponível, ex. `EmailNaoVerificado` (P5) — e, quando `email_otp` é aceito, `Error.Details.destinos` [{email_id, mascarado, primario}]), `ReauthIndisponivel`, `SessaoNecessaria` (leitura de segurança — e-mails, fatores, sessões, trilha — com credencial sem sessão do auth) ou `ImpersonationReadOnly` (sessão de suporte não escreve).
404
Rota fora da allowlist do contrato, ou recurso inexistente/alheio.
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Respostas
código
significado
200
Corpo do contrato §4.2 do auth, repassado sem reinterpretação.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ReauthRequired` (com `Error.Details.metodos_aceitos`, `Error.Details.metodos` — cada método da política com `disponivel` e o `motivo` do indisponível, ex. `EmailNaoVerificado` (P5) — e, quando `email_otp` é aceito, `Error.Details.destinos` [{email_id, mascarado, primario}]), `ReauthIndisponivel`, `SessaoNecessaria` (leitura de segurança — e-mails, fatores, sessões, trilha — com credencial sem sessão do auth) ou `ImpersonationReadOnly` (sessão de suporte não escreve).
404
Rota fora da allowlist do contrato, ou recurso inexistente/alheio.
Troca a senha (reauth senha atual|email_otp|totp|passkey|codigo_recuperacao — P4).
Onde é usada
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Respostas
código
significado
200
Corpo do contrato §4.2 do auth, repassado sem reinterpretação.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ReauthRequired` (com `Error.Details.metodos_aceitos`, `Error.Details.metodos` — cada método da política com `disponivel` e o `motivo` do indisponível, ex. `EmailNaoVerificado` (P5) — e, quando `email_otp` é aceito, `Error.Details.destinos` [{email_id, mascarado, primario}]), `ReauthIndisponivel`, `SessaoNecessaria` (leitura de segurança — e-mails, fatores, sessões, trilha — com credencial sem sessão do auth) ou `ImpersonationReadOnly` (sessão de suporte não escreve).
404
Rota fora da allowlist do contrato, ou recurso inexistente/alheio.
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Respostas
código
significado
200
Corpo do contrato §4.2 do auth, repassado sem reinterpretação.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ReauthRequired` (com `Error.Details.metodos_aceitos`, `Error.Details.metodos` — cada método da política com `disponivel` e o `motivo` do indisponível, ex. `EmailNaoVerificado` (P5) — e, quando `email_otp` é aceito, `Error.Details.destinos` [{email_id, mascarado, primario}]), `ReauthIndisponivel`, `SessaoNecessaria` (leitura de segurança — e-mails, fatores, sessões, trilha — com credencial sem sessão do auth) ou `ImpersonationReadOnly` (sessão de suporte não escreve).
404
Rota fora da allowlist do contrato, ou recurso inexistente/alheio.
502
auth indisponível.
Exemplo
curl -X GET 'https://api.asender.net/api/me/sessoes' \
-H 'Authorization: Bearer SEU_TOKEN'
DELETE/api/me/sessoes/{id}
Revoga uma sessão própria.
Onde é usada
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Respostas
código
significado
200
Corpo do contrato §4.2 do auth, repassado sem reinterpretação.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ReauthRequired` (com `Error.Details.metodos_aceitos`, `Error.Details.metodos` — cada método da política com `disponivel` e o `motivo` do indisponível, ex. `EmailNaoVerificado` (P5) — e, quando `email_otp` é aceito, `Error.Details.destinos` [{email_id, mascarado, primario}]), `ReauthIndisponivel`, `SessaoNecessaria` (leitura de segurança — e-mails, fatores, sessões, trilha — com credencial sem sessão do auth) ou `ImpersonationReadOnly` (sessão de suporte não escreve).
404
Rota fora da allowlist do contrato, ou recurso inexistente/alheio.
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Respostas
código
significado
200
Corpo do contrato §4.2 do auth, repassado sem reinterpretação.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ReauthRequired` (com `Error.Details.metodos_aceitos`, `Error.Details.metodos` — cada método da política com `disponivel` e o `motivo` do indisponível, ex. `EmailNaoVerificado` (P5) — e, quando `email_otp` é aceito, `Error.Details.destinos` [{email_id, mascarado, primario}]), `ReauthIndisponivel`, `SessaoNecessaria` (leitura de segurança — e-mails, fatores, sessões, trilha — com credencial sem sessão do auth) ou `ImpersonationReadOnly` (sessão de suporte não escreve).
404
Rota fora da allowlist do contrato, ou recurso inexistente/alheio.
Agenda a remoção do TOTP em 24h (reauth email_otp|codigo_recuperacao).
Onde é usada
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ReauthRequired` (com `Error.Details.metodos_aceitos`, `Error.Details.metodos` — cada método da política com `disponivel` e o `motivo` do indisponível, ex. `EmailNaoVerificado` (P5) — e, quando `email_otp` é aceito, `Error.Details.destinos` [{email_id, mascarado, primario}]), `ReauthIndisponivel`, `SessaoNecessaria` (leitura de segurança — e-mails, fatores, sessões, trilha — com credencial sem sessão do auth) ou `ImpersonationReadOnly` (sessão de suporte não escreve).
404
Rota fora da allowlist do contrato, ou recurso inexistente/alheio.
409
`NotEnabled` ou `MinimoDeFatores` (ficaria com menos de 2 fatores).
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Respostas
código
significado
200
`{ok, codigos_recuperacao}` (auth MeTOTPConfirmado): 10 códigos XXXX-XXXX-XXXX na 1ª ativação, vazio se já havia códigos vivos.
400
`InvalidCode`, `TOTPNaoIniciado` ou `InvalidRequest`.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ReauthRequired` (com `Error.Details.metodos_aceitos`, `Error.Details.metodos` — cada método da política com `disponivel` e o `motivo` do indisponível, ex. `EmailNaoVerificado` (P5) — e, quando `email_otp` é aceito, `Error.Details.destinos` [{email_id, mascarado, primario}]), `ReauthIndisponivel`, `SessaoNecessaria` (leitura de segurança — e-mails, fatores, sessões, trilha — com credencial sem sessão do auth) ou `ImpersonationReadOnly` (sessão de suporte não escreve).
404
Rota fora da allowlist do contrato, ou recurso inexistente/alheio.
409
`AlreadyEnabled`.
429
`TooManyAttempts` (freio por usuário, o mesmo do login).
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Respostas
código
significado
200
`{otpauth_url, secret}` (auth MeTOTPSetup), repassado sem reinterpretação.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ReauthRequired` (com `Error.Details.metodos_aceitos`, `Error.Details.metodos` — cada método da política com `disponivel` e o `motivo` do indisponível, ex. `EmailNaoVerificado` (P5) — e, quando `email_otp` é aceito, `Error.Details.destinos` [{email_id, mascarado, primario}]), `ReauthIndisponivel`, `SessaoNecessaria` (leitura de segurança — e-mails, fatores, sessões, trilha — com credencial sem sessão do auth) ou `ImpersonationReadOnly` (sessão de suporte não escreve).
404
Rota fora da allowlist do contrato, ou recurso inexistente/alheio.
Produto marketing (exige entitlement). Tenant resolvido da sessão. A query aceita SÓ limit (allowlist) — parâmetro fora da lista é 422 nomeando-o, nunca listagem sem filtro em silêncio.
Onde é usada
dashboard de marketing.
Entradas
limit (allowlist). Parâmetro fora da lista é 422 nomeando-o, nunca listagem sem filtro em silêncio.
Saídas
200 com {Campaigns:[...]} (array, nunca null).
Parâmetros
nome
em
obrigatório
descrição
limit
query
não
Respostas
código
significado
200
OK — `{Campaigns:[...]}` (array, nunca null).
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Exemplo
curl -X GET 'https://api.asender.net/api/campaigns' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/campaigns
Cria uma campanha em rascunho (draft).
NÃO dispara nada — a campanha nasce em draft. O envio real é do worker (piso 18), no horário do schedule.
Remetente obrigatório (2026-10-02):FromAddress vazio numa campanha de e-mail é 422 RemetenteObrigatorio (antes virava o remetente padrão e a campanha saía com todos os lotes recusados). O seletor GET /api/campaigns/remetentes lista os domínios aceitos.
Remetente:FromAddress de e-mail só de domínio VERIFICADO da conta (SPF+DKIM, no e-mail corporativo). Recusa repassada INTACTA do asender_mkt_api: 422 RemetenteNaoVerificado (mensagem para o usuário, com o domínio) e 503 VerificacaoIndisponivel — o front mostra aviso com o atalho para Admin → Domínios.
Onde é usada
wizard "nova campanha" da tela de campanhas.
Saídas
201 com {Campaign}; 422 em validação (campo ruim ou template/lista inexistente). NÃO dispara nada — campanha nasce em draft.
Efeitos
escreve em messages.campaigns.
Respostas
código
significado
201
Criada — `{Campaign}` em draft.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
503
`VerificacaoIndisponivel` — o domínio do remetente não pôde ser conferido agora; nada foi gravado.
Campanha de outro tenant também é 404 — não confirma existência alheia. Pausada pelo scheduler, traz PauseReason: remetente_nao_verificado (o domínio do From perdeu a verificação antes do disparo; nada saiu com outro remetente) ou a frase de dependência ausente.
Onde é usada
tela de detalhe/progresso da campanha.
Saídas
200 com {Campaign}; 404 quando o id não existe NO TENANT — campanha de outro tenant também é 404, para não confirmar existência alheia (§22.8).
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Respostas
código
significado
200
OK — `{Campaign}`.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
Exemplo
curl -X GET 'https://api.asender.net/api/campaigns/id_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
PATCH/api/campaigns/{id}
Edita uma campanha (só enquanto em draft ou paused).
A garantia "só em draft/paused" é do repo do asender_mkt_api, atômica no WHERE. A pausada é editável para o dono resolver o motivo (ex.: trocar o remetente cujo domínio perdeu a verificação) antes de reagendar.
Onde é usada
edição na tela de campanhas.
Saídas
200 com {Campaign}; 404 se não existir; 422 se não estiver em draft ou se algum campo/ref for inválido.
Efeitos
escreve em messages.campaigns.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Respostas
código
significado
200
OK — `{Campaign}`.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
Fora de draft, campo/ref inválido, ou `RemetenteNaoVerificado` (domínio do From não verificado nesta conta).
503
`VerificacaoIndisponivel` — o domínio do remetente não pôde ser conferido agora; nada foi gravado.
Agenda a campanha (draft/paused→scheduled). NÃO envia.
Piso 18: só marca a intenção — quem entrega no horário é o worker, com o guard de efeito externo. A borda valida a FORMA de ScheduledAt (RFC 3339) aqui, sem hop; o asender_mkt_api valida a REGRA (data futura, lista/ template presentes).
Onde é usada
botão "agendar" da tela de campanhas.
Entradas
ScheduledAt em RFC 3339 (ex.: 2026-09-10T14:00:00Z). A borda valida a FORMA aqui (data não-parseável é 422 sem hop); o asender_mkt_api é quem valida a REGRA (data futura, lista/template presentes).
Saídas
200 com {Campaign}; 404 se não existir; 422 em data malformada, passada/ausente ou campanha fora de draft.
Efeitos
escreve em messages.campaigns.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Respostas
código
significado
200
OK — `{Campaign}` em scheduled.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
Data malformada/passada/ausente, campanha fora de draft/paused, ou `RemetenteNaoVerificado` — o domínio do From perdeu a verificação desde o rascunho (fica como está). Reagendar a `paused` é retomá-la: `PauseReason` é limpo e os lotes já aceitos não saem de novo.
503
`VerificacaoIndisponivel` — o domínio do remetente não pôde ser conferido agora.
Domínios verificados que podem assinar o remetente da campanha (seletor do wizard).
Desde 2026-10-02 o remetente da campanha de e-mail é OBRIGATÓRIO e só de domínio verificado (SPF e DKIM). Esta rota alimenta o seletor: os domínios verificados da conta da SESSÃO (nunca de outra conta), lidos no e-mail corporativo. Lista vazia é válida — a tela mostra LinkDominios para verificar um domínio.
Onde é usada
wizard de campanha do asender_mkt_front.
Saídas
200 {Dominios:[...], LinkDominios} (lista vazia é válida); 503 VerificacaoIndisponivel repassado intacto com a fonte fora.
Efeitos
uma chamada ao asender_mkt_api (que consulta o asender_mail_api).
Respostas
código
significado
200
Os domínios verificados.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
503
`503 VerificacaoIndisponivel` — a verificação do domínio do remetente
não respondeu. Fail-closed: nada foi enfileirado. Repita depois do
`Retry-After` (com a mesma `Idempotency-Key`).
Exemplo
curl -X GET 'https://api.asender.net/api/campaigns/remetentes' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/contacts
Audiência do tenant.
Exige marketing OU emailmarketing (PDP, nesta ordem; negado => 404).
Onde é usada
tela de audiência do dashboard.
Entradas
q, list_id, tag, cursor, limit (allowlist).
Saídas
200 com {Contacts, NextCursor, HasMore}.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
q
query
não
list_id
query
não
tag
query
não
Filtra contatos que têm esta tag.
cursor
query
não
limit
query
não
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Exemplo
curl -X GET 'https://api.asender.net/api/contacts' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/contacts
Cria um contato.
Subscribed ausente vira true — opt-in é o default do cadastro manual.
Onde é usada
formulário de novo contato da tela de audiência.
Saídas
201 com {Contact}; 422 em validação (email/telefone inválidos).
Efeitos
escreve em messages.contacts.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
Respostas
código
significado
201
Criado.
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
413
`413 PayloadTooLarge` — corpo acima de 2 MiB.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Exige marketing OU emailmarketing (PDP, nesta ordem; negado => 404). A semântica de exclusão é do mkt_api; contato de outro tenant responde 404.
Saídas
204; 404 fora do tenant.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
id
path
sim
Respostas
código
significado
204
Removido.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
A ficha de UM contato da audiência, com o consentimento por canal.
Repassa GET /v1/contacts/{id} do mkt_api (produto marketing). Contato de outro tenant → o mesmo 404 de inexistente.
Onde é usada
a ficha /audience/contacts/{id} do asender_mkt_front, aberta pela busca global (hub e cabeçalho do Marketing); registrada em server.go no grupo exigirMarketing.
Saídas
200 {Contact}; 401 sem sessão; 404 se o contato não é do tenant (o mesmo 404 de inexistente); 502 com o upstream fora.
Efeitos
nenhum.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
id
path
sim
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Exemplo
curl -X GET 'https://api.asender.net/api/contacts/id_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/contacts/{id}/consent-proofs
Provas de consentimento do contato.
Exige marketing OU emailmarketing (PDP, nesta ordem; negado => 404).
Saídas
200 {Proofs:[{Source,FormId,Texto,BaseLegal,ConcedidoEm}]}; 404 fora do tenant.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
id
path
sim
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Exemplo
curl -X GET 'https://api.asender.net/api/contacts/id_AQUI/consent-proofs' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/contacts/{id}/lists
Listas a que o contato pertence.
Exige marketing OU emailmarketing (PDP, nesta ordem; negado => 404).
Saídas
200 {Lists}; 404 fora do tenant.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
id
path
sim
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Exemplo
curl -X GET 'https://api.asender.net/api/contacts/id_AQUI/lists' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/contacts/bulk
Importa até 5000 contatos, com upsert pela identidade natural.
Import parcial é resultado legítimo: itens ruins voltam em Errors com o índice de origem, e o lote não é invalidado por causa deles. Lote vazio ou acima do teto é 422.
Onde é usada
importador CSV da tela de audiência.
Saídas
200 com {Created, Updated, Errors, Contacts, Suppressed, AddedToList, Tagged}; 422 quando a lista vem vazia ou acima do teto.
Efeitos
escreve em messages.contacts.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
Respostas
código
significado
200
Lote processado.
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
413
`413 PayloadTooLarge` — corpo acima de 2 MiB.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Remove tags de contatos (DELETE com corpo; até 500 ids e 10 tags).
Exige marketing OU emailmarketing (PDP, nesta ordem; negado => 404).
Onde é usada
seleção múltipla da tela de audiência.
Saídas
200 {Applied|Removed, NotFound}; 422 em corpo vazio/acima do teto.
Efeitos
escreve nas tags dos contatos (no mkt_api).
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
Respostas
código
significado
200
OK
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Exige marketing OU emailmarketing (PDP, nesta ordem; negado => 404). NotFound traz os ids que não existem no tenant.
Onde é usada
seleção múltipla da tela de audiência.
Saídas
200 {Applied|Removed, NotFound}; 422 em corpo vazio/acima do teto.
Efeitos
escreve nas tags dos contatos (no mkt_api).
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
Respostas
código
significado
200
OK
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Quem pode ler (S128): quem acessa o produto marketing OU qualquer uma das ferramentas de despacho — emailmarketing, sns, zapify, push —, decidido pelo PDP do asender_auth_api (/v1/authz/decide), produto a produto nesta ordem; a primeira permissão basta. É a fonte do seletor de público das telas de e-mail, SMS, WhatsApp e push. Nenhum permitido, conta suspensa ou PDP fora sem decisão em cache ⇒ 404 (deny-default; 404 e não 403 para não virar oráculo). Só a LEITURA vale para sns/zapify/push. O resto da audiência (criar lista, membros, contatos, tags) exige marketing OU emailmarketing.
Onde é usada
tela de audiência.
Saídas
200 com {Lists}.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
cursor
query
não
limit
query
não
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Exemplo
curl -X GET 'https://api.asender.net/api/lists' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/lists
Cria uma lista de contatos.
O upstream faz upsert por slug; este BFF responde sempre 201. Exige marketing OU emailmarketing (a LEITURA em GET /api/lists aceita também sns/zapify/push; a escrita, não).
Onde é usada
tela de audiência.
Saídas
201 com {List}; 422 em validação.
Efeitos
escreve em messages.contact_lists.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
Respostas
código
significado
201
Criada (ou atualizada pelo slug).
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Exige marketing OU emailmarketing (PDP, nesta ordem; negado => 404).
Saídas
204; 404 fora do tenant.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
id
path
sim
Respostas
código
significado
204
Removida.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Repassa a paginação do serviço de mensageria sem cortar nada: enquanto HasMore for true, chame de novo com cursor=NextCursor até a última página. limit fora de 1..100 é 422 (nunca reduzido em silêncio). Substitui o uso de GET /api/contacts?list_id= com limit=500, que perdia os membros além do 500º. Lista de outro tenant responde 404.
Onde é usada
expansão de lista da tela de audiência e do composer de envio do asender_mkt_front, via /api/proxy; registrada em server.go no grupo exigirMarketing.
Entradas
{id} (public id da lista); query cursor, limit (allowlist; limit fora de 1..100 é 422 — nunca cortado sem aviso).
Saídas
200 {Contacts, NextCursor, HasMore}; 401 sem sessão; 404 se a lista não é do tenant; 422 em query inválida; 502 com o upstream fora.
Efeitos
nenhum (só leitura).
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
id
path
sim
Public id da lista (`lst_<hex>`).
cursor
query
não
`NextCursor` da página anterior; omita na primeira.
limit
query
não
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/api/lists/id_AQUI/contacts' \
-H 'Authorization: Bearer SEU_TOKEN'
DELETE/api/lists/{id}/members
Tira contatos de uma lista (DELETE com corpo; até 500 ids).
Exige marketing OU emailmarketing (PDP, nesta ordem; negado => 404). NotFound traz os ids que não pertencem à lista no tenant.
Saídas
200 {Removed, NotFound}; 404 lista fora do tenant; 422 corpo vazio ou acima de 500 ids.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
id
path
sim
Public id da lista (`lst_<hex>`).
Respostas
código
significado
200
OK
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
NotFound traz os ids que não existem no tenant — o servidor diz o que NÃO pôde fazer, em vez de descartar em silêncio. Idempotente.
Onde é usada
tela de audiência (seleção múltipla).
Saídas
200 com {Added}; 404 se a lista não é do tenant; 422 se a lista de ids vem vazia.
Efeitos
escreve em messages.contact_list_members.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
id
path
sim
Public id da lista (`lst_<hex>`).
Respostas
código
significado
200
OK
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Channel/Status/Q/Cursor/Limit são aceitos em snake_case (channel, status, q, cursor, limit) e repassados por allowlist — nenhum outro parâmetro atravessa.
Saídas
200 com {Messages, NextCursor, HasMore}; 422 em query malformada.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
channel
query
não
status
query
não
q
query
não
cursor
query
não
limit
query
não
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/api/messages' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/messages/{id}
Detalhe do envio com timeline.
Onde é usada
tela de detalhe do envio.
Saídas
200 com {Message, Events}; 404 quando o id não existe NO TENANT — mensagem de outro tenant também é 404, para não confirmar existência (§22.8).
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
id
path
sim
Public id da mensagem (`msg_<hex>`).
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Exemplo
curl -X GET 'https://api.asender.net/api/messages/id_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/messages/send
Composer do dashboard — envia em qualquer canal.
Corpo em PascalCase, campo a campo espelhando o contrato interno. Campo desconhecido é 400 (DisallowUnknownFields), não silêncio.
Onde é usada
composer da tela de envio do dashboard.
Saídas
202 com {Messages, ReusedIdempotency}; 422 quando o asender_mkt_api recusa a validação (canal inválido, destinatário vazio, corpo ausente).
Efeitos
escreve mensagens no banco de mensageria.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
Respostas
código
significado
202
Enfileirado.
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
413
`413 PayloadTooLarge` — corpo acima de 2 MiB.
422
`422 RemetenteNaoVerificado` — o remetente (`Source`/`From`) de e-mail
não é de domínio verificado nesta conta (inclui domínio verificado em
OUTRA conta). `Message` para o usuário: o domínio e o link para Admin →
Domínios. Nada é enfileirado (decisão de 2026-10-02; antes o e-mail
saía com o remetente da plataforma).
`422 ValidationError` — demais recusas do upstream (template sem
variável, todos suprimidos, portão de qualidade…).
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
503
`503 VerificacaoIndisponivel` — a verificação do domínio do remetente
não respondeu. Fail-closed: nada foi enfileirado. Repita depois do
`Retry-After` (com a mesma `Idempotency-Key`).
Devices de push do tenant, para o seletor da tela de disparo.
Fonte do seletor de dispositivos em /t/{conta}/send/push. Antes de existir, o caminho respondia 404 e o seletor ficava vazio mesmo com devices no banco — a tela levava o usuário a concluir que a conta não tinha dispositivo nenhum.
Onde é usada
dashboard, tela /send/push.
Entradas
cursor e limit, na mesma allowlist dos outros List.
Saídas
200 com {Devices, NextCursor, HasMore} (PascalCase, §51). Antes desta rota o caminho respondia 404 e a tela não tinha como listar: dava para disparar digitando o token à mão, mas não escolher um device existente.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
cursor
query
não
limit
query
não
Respostas
código
significado
200
Página de devices, em PascalCase.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Exemplo
curl -X GET 'https://api.asender.net/api/push/devices' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/push/site-config
Pacote de integração de Web Push para o site do cliente.
Devolve, num JSON só, tudo que o site precisa para receber Web Push: chave pública VAPID do tenant, manifest.json, o service worker e o snippet de inscrição — os três já preenchidos com os valores desta instalação. Gerar no servidor evita o chamado clássico do integrador que esqueceu de substituir um <SUA_CHAVE_AQUI>.
O snippet leva só credencial PÚBLICA (M15, ADR-0057). A chave de site SiteKey (pk_push_…) é assinada para (tenant da sessão, origems pedidas) e só é aceita em POST /v1/push/site/devices, a partir dessas origens. A API key de servidor nunca entra no snippet. Mesmo (tenant, origens) gera sempre a mesma chave; trocar de domínio = gerar de novo.
Onde é usada
tela de push do dashboard (botão de copiar/baixar cada arquivo).
Entradas
origem — repetível, as origens do site (1 a 10).
Saídas
200 com {PublicKey, ApiBase, SiteKey, Origens, Manifest, ServiceWorker, Snippet}; 400 sem origem ou com origem inválida.
Efeitos
pode criar o par VAPID do tenant na primeira chamada (no core). ## A chave do snippet é PÚBLICA (M15) O snippet antigo mandava colar a API key de SERVIDOR num JS público. Agora ele leva uma chave de site (pk_push_…) que só registra inscrição de Web Push e só a partir das origens pedidas aqui — ver internal/sitekey. Sem origem declarada não há chave: uma chave sem allowlist valeria para qualquer página. ## Por que o servidor gera, em vez de documentar Os três arquivos dependem de dois valores que variam por instalação: a chave pública do tenant e a base da API. Documentação com <SUA_CHAVE_AQUI> produz exatamente um tipo de chamado — o do integrador que esqueceu de substituir, e cujo sintoma é "não chega notificação", sem erro em lugar nenhum. Gerando aqui, o que o cliente cola já está correto.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
origem
query
sim
Origem do site (`https://www.loja.com.br`), repetível, 1 a 10. Sem
curinga, sem `null`, sem caminho. Canonizada (minúscula, sem porta
padrão, sem barra final) e gravada DENTRO da chave de site.
Respostas
código
significado
200
Artefatos prontos para o site.
400
`400 ValidationError` com `Error.Details` mapeando campo → motivo.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
503
Segredo de assinatura da chave de site ausente.
Exemplo
curl -X GET 'https://api.asender.net/api/push/site-config' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/tags
Tags do tenant com a contagem de contatos.
Exige marketing OU emailmarketing (PDP, nesta ordem; negado => 404).
Onde é usada
filtro/gestão de tags da ferramenta emailmarketing.
Entradas
limit (allowlist).
Saídas
200 {Tags:[{Name,ContactCount}]}.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
limit
query
não
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Exemplo
curl -X GET 'https://api.asender.net/api/tags' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/templates
Templates do tenant.
Onde é usada
dashboard.
Saídas
200 com {Templates}.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
limit
query
não
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Exemplo
curl -X GET 'https://api.asender.net/api/templates' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/templates
Cria ou atualiza um template pelo par (tenant, slug).
Onde é usada
editor de templates do dashboard.
Saídas
200 com {Template}; 422 em validação do asender_messages.
Efeitos
escreve em messages.templates.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
Respostas
código
significado
200
Gravado (upsert — 200 tanto na criação quanto na atualização).
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
413
`413 PayloadTooLarge` — corpo acima de 2 MiB.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
A trilha de ATIVIDADE da própria conta (admin/owner), ADR-0114.
Mesma leitura de /api/admin/atividade, mas o tenant_id enviado aos serviços é SEMPRE o {id} autorizado pela sessão; conta/tenant_id na query são ignorados. Conta alheia 404; papel sem governança (viewer/developer) 403.
Onde é usada
aba "Atividade" da tela /auditoria do admin.asender.
Efeitos
só leitura.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
ator
query
não
servico
query
não
recurso_tipo
query
não
recurso_id
query
não
acao
query
não
de
query
não
RFC3339 ou AAAA-MM-DD.
ate
query
não
RFC3339 ou AAAA-MM-DD (dia inclusivo).
limite
query
não
cursor
query
não
formato
query
não
csv exporta (anexo; células que começam com = + - @ ganham aspa simples).
Respostas
código
significado
200
OK
400
filtro ou cursor inválido
403
papel sem governança da conta
404
conta alheia
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/atividade' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/tenants/{id}/supressao-global/consulta
A conta consulta se um e-mail está em supressão global (só leitura).
Onde é usada
selo "Supressão global" dos fronts de conta (mail e e-mail marketing).
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
email
query
sim
Respostas
código
significado
200
suprimido, desde quando, origem e teste_disponivel_em
400
e-mail inválido
404
conta alheia
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/supressao-global/consulta' \
-H 'Authorization: Bearer SEU_TOKEN'
bff-public
GET/api/public/planos
Catálogo único de planos — vitrine pública (ADR-0075).
Anônima por desenho: é o que a LP (www.asender.net) e o hub (/contratar) mostram — nome, descrição, preço, CTA e limites de cada plano publicado. Sem tenant, sem sessão, sem segredo. Está na allowlist publicRoutes do routes_test.go.
Fonte única: core.planos + core.plano_limites, lidas do core em GET /v1/planos. Corpo em snake_case (contrato ADR-0075), não o PascalCase do resto do /api — a mesma forma do core, tipada (campo inesperado do core não vaza).
preco_centavos: null = sob consulta (nunca 0).
limites traz todo domínio de core.plano_limites do plano; dia/mes
null = sem limite naquela escala.
Cache: memória do BFF por PLANOS_CACHE_TTL (60 s) e Cache-Control: public, max-age=60. Stale-on-error: com o core fora, serve a última cópia boa (warn no log com RequestId); só a primeira leitura sem cópia responde 503 (Cache-Control: no-store).
Teto por IP próprio: PUBLIC_RATE_LIMIT_PER_IP (120) por PUBLIC_RATE_LIMIT_WINDOW (1 min).
Onde é usada
LP (www, server-side com ISR) e hub /contratar. Anônima.
Respostas
código
significado
200
Planos publicados, em `ordem`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
`503 ServiceUnavailable` — o core não respondeu e não há cópia em
cache. A LP cai no fallback estático dela.
Exemplo
curl -X GET 'https://api.asender.net/api/public/planos' \
-H 'Authorization: Bearer SEU_TOKEN'
bff-reports
GET/api/reports/overview
Funil do período com quebra por canal.
Os três canais SEMPRE aparecem em ByChannel, zerados quando não houve tráfego. As chaves de ByChannel (email/sms/push) ficam em minúsculas porque são DADO, não nome de campo.
Taxas em [0,1], arredondadas a 4 casas, grampeadas no teto (a base pode ter bounced > sent, porque bounce não exige sent_at).
Onde é usada
dashboard de relatórios (cards do topo).
Entradas
from/to em ISO-8601 UTC; ausentes = últimos 30 dias (default resolvido pelo asender_mkt_api, não duplicado aqui).
Saídas
200 com {Totals, Rates, ByChannel} — as chaves de ByChannel seguem sendo email/sms/push, porque ali são dado e não nome de campo.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
from
query
não
ISO-8601 (`YYYY-MM-DD` ou RFC 3339). Default: 30 dias antes de `to`.
to
query
não
ISO-8601. Default: agora. **Fim EXCLUSIVO**; data pura cobre o dia inteiro.
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/api/reports/overview' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/reports/timeseries
Série temporal por bucket e canal.
Um ponto por (bucket, canal), buckets vazios inclusos. O campo continua se chamando Day mesmo com interval=hour — é o início do bucket; a granularidade vem em Period.Interval.
Este BFF aceita hour|day|week|month, mas o asender_mkt_api só implementa day|hour: week/month passam a validação daqui e são recusados com 422 pelo upstream.
Onde é usada
dashboard de relatórios (gráfico principal).
Entradas
from, to, channel (email|sms|push), interval (hour|day|week|month; default day). Valor fora da allowlist é 422 — filtro descartado em silêncio mente para quem consulta.
Saídas
200 com {Points:[...]}.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
from
query
não
ISO-8601 (`YYYY-MM-DD` ou RFC 3339). Default: 30 dias antes de `to`.
to
query
não
ISO-8601. Default: agora. **Fim EXCLUSIVO**; data pura cobre o dia inteiro.
channel
query
não
interval
query
não
`week` e `month` são aceitos aqui e recusados pelo upstream (422).
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/api/reports/timeseries' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/reports/top-templates
Ranking de templates por volume, com taxas.
limit aceito de 1 a 100 nesta borda; o teto REAL aplicado pelo repositório do asender_mkt_api é 50.
Onde é usada
dashboard de relatórios (tabela lateral).
Entradas
from, to, limit (1..100).
Saídas
200 com {Templates:[...]}.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
from
query
não
ISO-8601 (`YYYY-MM-DD` ou RFC 3339). Default: 30 dias antes de `to`.
to
query
não
ISO-8601. Default: agora. **Fim EXCLUSIVO**; data pura cobre o dia inteiro.
limit
query
não
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/api/reports/top-templates' \
-H 'Authorization: Bearer SEU_TOKEN'
bff-resumo
GET/api/busca
Busca federada do hub: um termo procurado em todas as ferramentas da pessoa na conta.
O BFF pergunta ao PDP (/v1/authz/eu) quais ferramentas a pessoa acessa na conta (resolvida por pertinência; conta alheia → 404) e chama, em PARALELO e com prazo curto por ferramenta (BUSCA_PRAZO, default 1,5 s), as buscas que cada uma já faz — sempre com a credencial da PRÓPRIA pessoa: o Bearer da sessão (crm: /v1/contatos?q=, /v1/empresas?q=; pages: /v1/paginas filtrada, /v1/links?busca=; sns, zapify, push e emailmarketing: GET /v1/busca?q=&limit= → {templates, campaigns}; ferramentas novas: o endpoint comum GET /v1/busca?q=&limite= → {itens:[…]}) ou o caminho de serviço com o usuário verificado (mail: /mailboxes/minhas + /mailboxes/{id}/messages?todas=1&q=, até 3 caixas, com o PEP e as concessões do usuário no mail_api; marketing: /v1/contacts?q= e /v1/campaigns filtradas, o mesmo caminho de /api/contacts). Ferramenta lenta ou fora vem como {indisponivel: true} e o resto responde (piso 10); ferramenta que RECUSA (403/404) some; ferramenta sem resultado não aparece. Até 5 itens por fonte e 10 por ferramenta. caminho é sempre relativo à ferramenta, com o marcador {tenant} (o BFF descarta o resto).
Onde é usada
a busca do cabeçalho do hub (asender_plataforma_front, components/busca-do-app.tsx).
Erros
400 termo; 401 sem sessão; 404 conta alheia (resolveSessionTenant — nunca 403, que seria oráculo); 502 se o PDP não responde (sem ele não há como saber o que a pessoa pode ver: nega).
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
q
query
sim
Respostas
código
significado
200
OK
400
`ValidationError` — q com menos de 2 ou mais de 100 caracteres, ou com caractere de controle.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`NoTenant` — o usuário ainda não pertence a conta nenhuma.
404
Conta alheia no `X-Asender-Tenant` (mesma resposta de inexistente).
429
`Throttling` — teto por principal.
502
core ou PDP (auth) indisponível — sem eles não há como saber o que a pessoa vê.
Exemplo
curl -X GET 'https://api.asender.net/api/busca' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/resumo
Home do hub: o resumo de cada ferramenta que a pessoa acessa na conta.
O BFF pergunta ao PDP (/v1/authz/eu) quais ferramentas a pessoa acessa na conta (resolvida por pertinência; Ferramenta que falha, expira ou responde fora do contrato vem como {ferramenta, nome, subdominio, indisponivel: true} — o resto responde normalmente. Se a ferramenta RECUSA o acesso (403 membro sem o produto, 404), o bloco fica ausente. atendimento não vira bloco: quem tem CRM e o produto atendimento no PDP recebe até 3 indicadores do chat DENTRO do bloco do CRM (modulo: "chat", link para as conversas do CRM; falha do atendimento só tira o chat, ADR-0080). Links de indicador são sempre caminhos relativos à ferramenta (o BFF descarta os demais).
Onde é usada
a home do hub (asender_plataforma_front, /t/<conta>).
Erros
400 período inválido; 401 sem sessão; 404 conta alheia (via resolveSessionTenant — nunca 403, que seria oráculo); 502 se o PDP não responde (sem ele não há como saber o que a pessoa pode ver: nega).
Parâmetros
nome
em
obrigatório
descrição
periodo
query
não
Respostas
código
significado
200
OK
400
`ValidationError` — periodo fora de 7d|30d|90d.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`NoTenant` — o usuário ainda não pertence a conta nenhuma.
404
Conta alheia no `X-Asender-Tenant` (mesma resposta de inexistente).
429
`Throttling` — teto por principal.
502
core ou PDP (auth) indisponível — sem eles não há como saber o que a pessoa vê.
Exemplo
curl -X GET 'https://api.asender.net/api/resumo' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/resumo/marketing
Home do marketing: o `/v1/resumo` do asender_mkt_api, repassado sem reinterpretação.
Rota do produto marketing (PEP: sem acesso à ferramenta → recusa do PEP). O corpo segue o contrato comum de resumo em snake/minúsculo, sem PascalMap: {ferramenta, periodo{de,ate,chave}, indicadores[{chave,rotulo,valor,anterior,formato,link}], series[{chave,rotulo,tipo,pontos[{data,valor}]}]}.
Onde é usada
rota dentro do PEP do produto marketing (server_resumo.go) — quem não acessa o marketing nem chega aqui.
Erros
400 período; 404 conta alheia; os do mkt_api propagados por writeUpstreamError.
Parâmetros
nome
em
obrigatório
descrição
periodo
query
não
Respostas
código
significado
200
O payload do `/v1/resumo` do marketing, verbatim.
400
`ValidationError` — periodo fora de 7d|30d|90d.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`NoTenant`.
404
Conta alheia, ou sem acesso ao produto `marketing` (PEP).
429
`Throttling` — teto por principal.
502
mkt_api ou core indisponível.
Exemplo
curl -X GET 'https://api.asender.net/api/resumo/marketing' \
-H 'Authorization: Bearer SEU_TOKEN'
bff-suporte
GET/api/suporte/catalogo
Categorias, produtos, impactos e limites do formulário de chamado.
Repassa o catálogo do atendimento. Sessão impersonada recebe o catálogo (estático, sem conta).
Onde é usada
tela de abrir chamado (admin e app).
Respostas
código
significado
200
`{categorias, produtos, impactos, limites}`.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
502
atendimento indisponível
Exemplo
curl -X GET 'https://api.asender.net/api/suporte/catalogo' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/suporte/chamados
Chamados do usuário (`escopo=usuario`) ou da conta (`escopo=conta`, exige governança).
A conta vem da pertinência (X-Asender-Tenant, slug ou id, só vale se o usuário pertence a ela; alheia → 404). Cada chamado sai com solicitante {id, nome} e conta {id, nome, slug}. Parâmetros fora da allowlist são descartados. Sessão impersonada: escopo usuario → lista vazia; escopo conta → 403 SemGovernanca.
Onde é usada
a busca do cabeçalho do hub (asender_plataforma_front, components/busca-do-app.tsx).
Erros
400 termo; 401 sem sessão; 404 conta alheia (resolveSessionTenant — nunca 403, que seria oráculo); 502 se o PDP não responde (sem ele não há como saber o que a pessoa pode ver: nega).
Parâmetros
nome
em
obrigatório
descrição
escopo
query
não
estado
query
não
categoria
query
não
produto
query
não
q
query
não
limite
query
não
antes
query
não
Cursor opaco (`proximo` da página anterior).
Respostas
código
significado
200
`{chamados: Chamado[], proximo?}`.
400
`ValidationError` — filtro ou cursor inválido.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`SemGovernanca` (escopo conta sem ser admin) ou `NoTenant`.
404
Conta alheia.
502
atendimento indisponível
Exemplo
curl -X GET 'https://api.asender.net/api/suporte/chamados' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/suporte/chamados
Abre um chamado.
Corpo estrito: campo desconhecido (tenant_id, solicitante_*, autor, estado, prioridade…) → 400 no BFF, sem tocar no atendimento. Qualquer membro da conta pode abrir (inclusive viewer). Idempotency-Key opcional ([A-Za-z0-9._:-]{1,128}); replay do mesmo usuário volta 200. Sessão impersonada → 403 ImpersonationReadOnly.
Parâmetros
nome
em
obrigatório
descrição
Idempotency-Key
header
não
Respostas
código
significado
200
Replay da mesma `Idempotency-Key`.
201
Chamado criado (`Location` aponta para `/api/suporte/chamados/{protocolo}`).
400
`ValidationError` — campo desconhecido ou JSON inválido.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ImpersonationReadOnly` ou `NoTenant`.
404
Conta alheia.
409
`LimiteDeChamadosAbertos` ou `IdempotencyConflict`.
Responde ao chamado (JSON ou multipart com até 3 anexos).
application/json ({corpo}) ou multipart/form-data (campo corpo + até 3 anexo, cada um ≤ 5 MiB). O multipart é reescrito em streaming só com esses campos; outro campo → 400. A resposta do cliente reabre pendente/resolvido. Chamado fechado → 409 ChamadoFechado (use /reabrir).
Onde é usada
o campo de resposta da tela do chamado.
Parâmetros
nome
em
obrigatório
descrição
protocolo
path
sim
Respostas
código
significado
201
`{id, criada_em, estado}`.
400
`ValidationError` — campo não permitido ou anexos demais.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`ImpersonationReadOnly`.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`SemGovernanca`.
404
Conta alheia.
502
atendimento indisponível
Exemplo
curl -X GET 'https://api.asender.net/api/suporte/chamados/contagem' \
-H 'Authorization: Bearer SEU_TOKEN'
bff-tenants
POST/api/contratacao
Auto-contratação de plano (self-service); registra pagamento pendente.
TRANSVERSAL (fora do grupo marketing): é a rota que tira o usuário do dead-end do hub. Concede a governança admin:owner da conta na hora (é grátis) e registra a INTENÇÃO de pagar — o acesso PAGO só é liberado quando o pagamento é aprovado no fluxo de billing manual (MVP).
Corpo NÃO-estrito: campos extras (um user_id malicioso, por ex.) são IGNORADOS — o dono vem sempre da sessão verificada (anti-IDOR). O tenant é resolvido pela sessão (header X-Asender-Tenant quando o usuário pertence à conta), nunca do corpo. Conta com OUTRO dono é recusada antes de qualquer escrita.
Onde é usada
a tela /contratar do dashboard (self-service).
Saídas
200 {Status:"pendente",PagamentoId,Produtos,Plano}; 422 validação; 403 conta com outro dono; 502 quando um upstream falha.
Respostas
código
significado
200
Pagamento pendente registrado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
422
Produto/plano inválido, ou nenhum produto escolhido.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Endereço de um CEP/zipcode (widget CampoEndereco dos fronts).
Sem conta no caminho: o endereço de um CEP é dado público — a guarda é a sessão + o rate limit por principal. País/CEP fora do formato voltam 422 sem hop. 200/404/422/503 do core passam como vieram; qualquer outra resposta (ou core fora) vira 502. Falha nunca bloqueia o formulário: o widget segue com os campos manuais.
Onde é usada
GET /api/enderecos/consulta?pais=BR&cep=01310100.
Efeitos
uma chamada ao core (que tem cache de 30 dias).
Parâmetros
nome
em
obrigatório
descrição
pais
query
sim
cep
query
sim
Respostas
código
significado
200
Achado (sem número/complemento).
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
404
CEP não encontrado ou país sem cobertura.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
503
Provedores fora; campos seguem manuais.
Exemplo
curl -X GET 'https://api.asender.net/api/enderecos/consulta' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/invitations/accept
Aceita um convite e vincula o usuário à conta.
O usuário vem da sessão; quem autoriza é o token do convite (validado no core). A rota não exige pertinência à conta — quem aceita ainda não é membro. O status e o corpo do core são repassados.
Herança de acessos: com o aceite em 200, o BFF pede ao asender_auth_api (POST /v1/tenants/{id}/acessos/herdar) que dono e admin da conta herdem o maior papel de cada ferramenta assinada; developer/viewer não herdam. Falha na herança não desfaz o aceite (continua 200): vai para o log com o request id, e o acesso pode ser concedido na tela Equipe ou religando a ferramenta na Assinatura.
Onde é usada
tela de convite.
Efeitos
uma escrita no core. # Esta rota NÃO passa por contaAutorizada E não pode: quem aceita ainda NÃO pertence à conta — exigir pertinência aqui tornaria o convite impossível de aceitar. Quem autoriza é o TOKEN, que o core valida (existente, não expirado, não usado), e o usuário vem da sessão.
dashboard (topbar/tenant-switcher, settings/account) e backoffice (useTenantForest).
Saídas
200 com {Tenants:[{Id,Name,Slug,Status,...}]}; 502 se o core está fora. DEFEITO QUE ISTO FECHA (T3 §3.5): a rota devolvia o payload do core CRU (tenants[].id), enquanto TODO o resto de /api/* — inclusive a rota vizinha /api/tenants/tree — responde PascalCase (API_DESIGN.md §51). Quem seguia o contrato lia Tenants e recebia undefined; quem "fazia funcionar" passava a ler tenants e ficava preso ao formato interno do core.
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/api/tenants' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/tenants
Cria um tenant RAIZ pertencente ao usuário logado.
Allowlist tipada de dois campos.parent_id, monthly_quota, status, plan, owner_user_id e id são IGNORADOS e registrados — em nível ERROR, como tentativa de escalonamento de privilégio. O owner_user_id vem sempre do principal verificado.
Ignorar (em vez de 422) preserva chamadores antigos que mandam owner_user_id; ignorar aqui não é engolir: fica no log com o usuário que enviou.
Para pendurar um tenant sob outro use POST /api/tenants/{id}/children (autoriza o pai) ou PATCH /api/tenants/{id}/parent (autoriza os dois lados). É o único caminho autorizado para mexer na árvore.
Onde é usada
fluxo de cadastro (AuthHandler.Register faz o equivalente internamente) e recuperação manual quando o provisionamento automático falhou.
Entradas
{Name, Slug} — allowlist explícita, ver tenantCreateInput.
Saídas
201 com {Tenant:{Id,...}} em PascalCase; 400 JSON inválido OU campo fora da allowlist; 422 nome vazio; o status 4xx do core preservado; 502 quando o core está fora.
Efeitos
escreve no asender-core. O owner_user_id vem SEMPRE do principal verificado, nunca do corpo.
Respostas
código
significado
201
Tenant criado. **O objeto vem NO TOPO, sem a chave `Tenant`** — o
`asender_plataforma_api` responde o tenant sem chave de recurso nesta rota e o
BFF só pascaliza o que recebeu. É inconsistente com
`POST /api/tenants/{id}/children` e com `POST /api/auth/register`,
que devolvem `{"Tenant":{...}}`. Documentado como está no ar.
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
413
`413 PayloadTooLarge` — corpo acima de 2 MiB.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
O {id} vem do cliente e é autorizado contra a pertinência do usuário (authorizeTenantParam) antes de qualquer hop; conta alheia responde 404, nunca 403. Espelha /v1/tenants/{id}/api-keys do core — o dashboard não fala /v1 direto porque o segredo de serviço não vive no front.
Onde é usada
GET /api/tenants/{id}/api-keys.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
OK (corpo repassado do asender_plataforma_api).
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/api-keys' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/tenants/{id}/api-keys
Emite uma API key na conta (o texto puro volta UMA vez).
O user_id vem SEMPRE da sessão, nunca do corpo. A chave em texto puro é devolvida uma única vez, no corpo do core; não é persistida nem logada.
Onde é usada
POST /api/tenants/{id}/api-keys.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
201
Chave criada (corpo do core
400
`ValidationError`: `scopes` ausente, vazio ou com valor fora do catálogo; a mensagem lista os escopos válidos.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
Números de WhatsApp da conta (vários por conta; o padrão primeiro).
Espelho de sessão de /v1/tenants/{id}/channels/whatsapp/numbers do core. O {id} é autorizado por pertinência antes de qualquer hop; conta alheia responde 404. O token só aparece mascarado.
Onde é usada
a tela de canais do admin e o seletor de remetente do zapify.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
OK — `{numbers: [...]}` repassado do core.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/channels/whatsapp/numbers' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/tenants/{id}/channels/whatsapp/numbers
Adiciona um número de WhatsApp à conta.
Papel de ESCRITA. Corpo ESTRITO. Sem access_token, o core reaproveita o token de outro número da mesma WABA na conta. 409 NumeroEmUso quando o número já está em alguma conta (nunca diz qual).
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
201
Criado (eco do core, token mascarado).
400
Corpo inválido.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Papel de leitura.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
409
`NumeroEmUso`.
422
phone_id ausente.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Remove um número — pede confirmação se houver conversas abertas.
Sem confirm=true, o BFF pergunta ao atendimento (com o Bearer da sessão) quantas conversas abertas entraram pelo número. Havendo alguma — ou não dando para saber — responde 409 ConversasAbertas com open_conversations (null = desconhecido) e NÃO remove. O histórico das conversas nunca é apagado; só as respostas por este número param.
Onde é usada
"Remover" na tela de canais.
Parâmetros
nome
em
obrigatório
descrição
confirm
query
não
Respostas
código
significado
204
Removido.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
409
`ConversasAbertas` (confirme) ou `PadraoComOutros`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Cria uma sub-conta sob um tenant administrado pelo usuário.
Autoriza {id} por vínculo direto ou herdado (ser membro de um ancestral manda na subárvore). MonthlyQuota ausente/nulo = herda do ancestral mais próximo com valor. O usuário da SESSÃO vira o owner da sub-conta (nunca um campo do corpo): ela nasce com governança e as ferramentas padrão, e aparece no lançador e no admin.
Onde é usada
tela de hierarquia de tenants.
Entradas
{Name, Slug, MonthlyQuota} — MonthlyQuota nulo/ausente significa herdar do ancestral mais próximo com valor.
Saídas
201 com {Tenant}; 404 se {id} não é acessível; 422 quando o core recusa (slug duplicado, ciclo detectado pelo trigger de closure).
Efeitos
escreve no asender-core (conta + closure + membro owner = usuário da sessão + evento platform.tenant.created, numa transação lá).
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
201
Sub-conta criada.
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Papel efetivo na conta não governa a árvore (`GovernanceRole`): só
`owner`/`admin` (direto ou do ancestral concedente) criam, movem ou
mudam cota de sub-conta — `developer`/`viewer` não (2026-10-04).
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
409
`409 Conflict` — email já registrado, slug em uso.
413
`413 PayloadTooLarge` — corpo acima de 2 MiB.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Destinatários dos envios feitos pela API (endereço completo, estado e motivo).
Painel (sessão). O {id} vem do cliente e é autorizado por pertinência (authorizeTenantParam; conta alheia responde 404) e por papel: só owner e admin leem (deny-default; developer, viewer e papel indeterminado levam 403). Lê AO VIVO o serviço dono das mensagens, escopado no {id} validado — o log de requisições guarda destinatários mascarados; este é o dado operacional. Inclui os suprimidos (status: suppressed) com reason e message. Sem ids: página por status, q (endereço), cursor, limit. Com ids (1 a 50 ids de mensagem, nenhum outro parâmetro): só os que existem na conta.
Onde é usada
API pública do cliente.
Entradas
status, q, cursor, limit (allowlist). channel do cliente é IGNORADO: nesta rota o canal é do contrato, não do parâmetro.
Saídas
200 com {Messages, NextCursor, HasMore}; 422 em query malformada.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
status
query
não
Estado da mensagem (ex.: `suppressed`, `sent`, `bounced`).
q
query
não
Busca no endereço do destinatário ou no assunto.
cursor
query
não
Inteiro não negativo (`next_cursor` da página anterior).
limit
query
não
ids
query
não
Até 50 ids de mensagem separados por vírgula.
Respostas
código
significado
200
OK.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 AdminRole` — o usuário pertence à conta mas não é `owner` nem `admin`.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
Parâmetro desconhecido ou inválido.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
Serviço de mensagens indisponível.
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/envios-api' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/tenants/{id}/importaveis
As outras contas que o usuário governa e pode importar para a árvore de {id}.
Contas de que o usuário é membro DIRETO com papel owner/admin, fora da árvore de {id} (ela, a descendência e os ancestrais — já estão nela ou seria ciclo). Cada uma com o pai atual (PaiId/PaiNome) e, quando o usuário não governa esse pai, Bloqueio: ParentNotGoverned (a tela mostra desabilitada e explica). Exige governar {id}.
Onde é usada
seletor flutuante do diálogo "Importar conta existente" (admin).
Saídas
200 {Contas:[...]} em ordem de nome; 403/404 como as mutações.
Efeitos
leituras no core (pertinência, descendência, ancestrais por conta).
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
A lista, em ordem de nome.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`GovernanceRole`: o usuário não governa `{id}`.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/importaveis' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/tenants/{id}/invitations
Convites pendentes da conta.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
OK
401
credencial ausente ou inválida
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/invitations' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/tenants/{id}/invitations
Convida alguém para a conta.
Onde é usada
tela de equipe.
Efeitos
uma escrita no core (e, quando houver envio, um e-mail).
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Imagem do logo da empresa (lançador e cabeçalho dos fronts).
O {id} é autorizado contra a pertinência do usuário (authorizeTenantParam) antes de o core ser perguntado; conta alheia responde 404. Os bytes são o PNG que o core recodificou (migration 031); a resposta leva Content-Type: image/png FIXO, nosniff, CSP sandbox e Cache-Control: private — imutável por um ano quando ?v= é a versão atual (a lista GET /api/tenants traz LogoQuadrado/LogoHorizontal). Só leitura: enviar/remover é governança no admin.asender.
Onde é usada
GET /api/tenants/{id}/logo/{variante}. Cache: private (é imagem atrás de sessão — um cache compartilhado não pode guardá-la). Com ?v= igual à versão atual, a URL é imutável e vale por um ano; sem ?v= (ou com versão velha), uma hora.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
variante
path
sim
v
query
não
Respostas
código
significado
200
PNG
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/logo/variante_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/tenants/{id}/logs
Log de requisições da API da conta (painel, sem corpos).
Painel (sessão). O {id} vem do cliente e é autorizado por pertinência (authorizeTenantParam; conta alheia responde 404) e por papel: só owner e admin leem o log da conta (deny-default; developer, viewer e papel indeterminado levam 403). O tenant enviado ao serviço de logs é o {id} já validado. Mesmos filtros e corpo de GET /v1/logs.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
limit
query
não
Itens por página.
after
query
não
Cursor opaco (`next` da página anterior): devolve os registros mais antigos. Adulterado ou de outra conta → 400. Não combine com `before`.
before
query
não
Cursor opaco: devolve os mais novos. `after` e `before` juntos → 400.
status_class
query
não
Lista separada por vírgula de `2xx`, `3xx`, `4xx`, `5xx`.
status
query
não
Status HTTP exato.
method
query
não
Verbo HTTP.
path
query
não
Padrão de rota exato (não o caminho com ids).
api_key_id
query
não
Id de uma ou mais chaves da conta, separados por vírgula.
error_code
query
não
Código do envelope de erro devolvido.
idempotency_key
query
não
Chave de idempotência efetiva da requisição.
message_id
query
não
Id de mensagem devolvido pelo envio (liga o log ao envio).
mode
query
não
Modo da chave usada.
since
query
não
RFC 3339. Padrão: agora menos 24 h.
until
query
não
RFC 3339. Padrão: agora. Janela maior que 31 dias é reduzida a 31 dias antes de `until`.
Respostas
código
significado
200
OK (corpo repassado do serviço de logs, em snake_case).
400
`400 ValidationError` — parâmetro inválido, cursor adulterado ou de outra
conta, `after` com `before`. Corpo repassado do serviço de logs
(`LogsErrorEnvelope`).
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 AdminRole` — o usuário pertence à conta mas não é `owner` nem `admin`.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
`503 ServiceUnavailable` — o serviço de logs não respondeu. Repita depois
do `Retry-After`; o resto da API segue funcionando.
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/logs' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/tenants/{id}/logs/{logId}
Detalhe de uma requisição (corpos redigidos).
Painel (sessão). O {id} vem do cliente e é autorizado por pertinência (authorizeTenantParam; conta alheia responde 404) e por papel: só owner e admin leem o log da conta (deny-default; developer, viewer e papel indeterminado levam 403). O tenant enviado ao serviço de logs é o {id} já validado. Mesmo corpo de GET /v1/logs/{id}; id de outra conta = 404 idêntico ao inexistente.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
logId
path
sim
Respostas
código
significado
200
OK (corpo repassado do serviço de logs, em snake_case).
400
`400 ValidationError` — parâmetro inválido, cursor adulterado ou de outra
conta, `after` com `before`. Corpo repassado do serviço de logs
(`LogsErrorEnvelope`).
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 AdminRole` — o usuário pertence à conta mas não é `owner` nem `admin`.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
`503 ServiceUnavailable` — o serviço de logs não respondeu. Repita depois
do `Retry-After`; o resto da API segue funcionando.
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/logs/logId_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/tenants/{id}/logs/resumo
Resumo agregado da conta: total, taxa de erro, p50/p95, por rota e por código.
Painel (sessão). O {id} vem do cliente e é autorizado por pertinência (authorizeTenantParam; conta alheia responde 404) e por papel: só owner e admin leem o log da conta (deny-default; developer, viewer e papel indeterminado levam 403). O tenant enviado ao serviço de logs é o {id} já validado. p95_ms é estimado pelos buckets fixos do histograma.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
since
query
não
until
query
não
mode
query
não
Respostas
código
significado
200
OK (corpo repassado do serviço de logs, em snake_case).
400
`400 ValidationError` — parâmetro inválido, cursor adulterado ou de outra
conta, `after` com `before`. Corpo repassado do serviço de logs
(`LogsErrorEnvelope`).
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 AdminRole` — o usuário pertence à conta mas não é `owner` nem `admin`.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
`503 ServiceUnavailable` — o serviço de logs não respondeu. Repita depois
do `Retry-After`; o resto da API segue funcionando.
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/logs/resumo' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/tenants/{id}/logs/series
Série temporal de requisições por classe de status.
Painel (sessão). O {id} vem do cliente e é autorizado por pertinência (authorizeTenantParam; conta alheia responde 404) e por papel: só owner e admin leem o log da conta (deny-default; developer, viewer e papel indeterminado levam 403). O tenant enviado ao serviço de logs é o {id} já validado. Lê o rollup (não as partições): pontos ordenados por t, buckets vazios com zeros.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
bucket
query
não
Largura do ponto.
since
query
não
until
query
não
mode
query
não
Respostas
código
significado
200
OK (corpo repassado do serviço de logs, em snake_case).
400
`400 ValidationError` — parâmetro inválido, cursor adulterado ou de outra
conta, `after` com `before`. Corpo repassado do serviço de logs
(`LogsErrorEnvelope`).
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 AdminRole` — o usuário pertence à conta mas não é `owner` nem `admin`.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
`503 ServiceUnavailable` — o serviço de logs não respondeu. Repita depois
do `Retry-After`; o resto da API segue funcionando.
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/logs/series' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/tenants/{id}/members
Membros da conta.
Onde é usada
tela de equipe.
Efeitos
uma leitura no core.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
OK
401
credencial ausente ou inválida
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/members' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/tenants/{id}/members
Vincula um usuário à conta.
Onde é usada
tela de equipe.
Efeitos
uma escrita no core.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Prévia de mover/importar uma conta, sem escrever nada.
?ParentId=acc_x (ausente = promover a raiz). A MESMA autorização dos três lados do PATCH — a prévia não é oráculo sobre conta alheia. Devolve, do core: a conta, o pai atual, o destino, NoOp (já está lá), Ciclo, ContasMovidas e a cota efetiva antes/depois. Usada pelo passo de confirmação de "Importar conta existente" (admin).
Onde é usada
passo "confirmar" do diálogo "Importar conta existente" do admin. Segurança: a MESMA autorização do PATCH — a prévia não pode virar oráculo sobre contas que o usuário não governa.
Saídas
200 {Preview:{...}}; 403/404/422 como no PATCH.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
ParentId
query
não
Respostas
código
significado
200
A prévia.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`GovernanceRole` ou `ParentNotGoverned` (ver o PATCH /parent).
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/movimento/previa' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/tenants/{id}/oauth-apps
Lista os apps OAuth ("Entrar com Asender") da conta.
Portal "Aplicativos" (ADR-0130, D4). O {id} é autorizado por pertinência (authorizeTenantParam; conta alheia responde 404) e o BFF repassa o Bearer da sessão ao auth (GET /v1/oauth/meus-apps?conta={id}), que confere owner/admin no core (membro sem papel: 403 PapelInsuficiente). Resposta Cache-Control: no-store.
Onde é usada
a busca do cabeçalho do hub (asender_plataforma_front, components/busca-do-app.tsx).
Erros
400 termo; 401 sem sessão; 404 conta alheia (resolveSessionTenant — nunca 403, que seria oráculo); 502 se o PDP não responde (sem ele não há como saber o que a pessoa pode ver: nega).
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
OK: `{apps:[app]}` (corpo do auth).
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Membro sem papel owner/admin (`PapelInsuficiente`).
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/oauth-apps' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/tenants/{id}/oauth-apps
Cria um app OAuth na conta (o client_secret volta UMA vez).
O campo conta do corpo é SEMPRE sobrescrito pelo {id} da rota. Corpo até 64 KiB. Para app confidencial a resposta 201 traz client_secret uma única vez (Cache-Control: no-store); nada é guardado nem logado no BFF. Erros do auth intactos: 409 LimiteDeApps (10 por conta), 429 MuitasCriacoes (com Retry-After), 422 validação.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
201
Criado: `{app, client_secret?, aviso}`.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Sem papel owner/admin.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
Move o tenant para outro pai, importa uma conta como sub-conta, ou promove a raiz.
Autoriza os TRÊS lados (ADR-0098): {id} (quem entrega), o pai ATUAL de {id} quando ele não é raiz (quem perde — sem isto o admin direto de uma sub-conta de agência a arrancaria da árvore) e o ParentId de destino (quem recebe — sem isto o tenant iria parar na árvore de outro cliente, que passaria a vê-lo em rollup). Todos exigem papel EFETIVO owner/admin, direto ou herdado pela árvore.
Corpo com EXATAMENTE um campo ParentId. null promove a raiz; campo ausente é 422 (um {} acidental não pode promover uma sub-conta). Ciclo → 422 (o trigger de closure do core levanta check_violation), nunca 500. Já estar sob o destino → 200 com Moved:false, nada gravado.
O core grava a trilha (core.tenant_movimentos, ator = usuário da sessão, origem sessao) e o evento platform.tenant.moved.
Onde é usada
tela "Contas e sub-contas" do admin (mover e "Importar conta existente"). Segurança: autoriza os TRÊS lados (ver o doc do arquivo).
Saídas
200 com {Tenant, Moved, PaiAnterior} (Moved:false = já estava lá, nada gravado); 404 se a conta ou o destino não é acessível; 403 GovernanceRole/ParentNotGoverned; 409 movimento concorrente; 422 ciclo, auto-pai ou destino inativo.
Efeitos
reescreve a closure no core e grava a trilha (ator = usuário da sessão, origem sessao) + evento platform.tenant.moved.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
Movido (a closure foi reescrita) ou já estava lá (`Moved:false`).
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`GovernanceRole`/`ReadOnlyRole`: o papel efetivo na conta ou no
destino não governa a árvore (só `owner`/`admin`).
`ParentNotGoverned`: a conta está sob uma conta que o usuário não
governa — só quem governa o pai atual a tira de lá.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
409
Outro movimento mudou o pai da conta entre a leitura e a escrita (`Conflict`); nada foi gravado — recarregue.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Corpo com EXATAMENTE um campo MonthlyQuota. null volta a herdar do ancestral mais próximo. Campo ausente é 422 — um nome digitado errado não pode limpar a quota em silêncio.
Onde é usada
tela de hierarquia de tenants.
Entradas
{MonthlyQuota} — número define o teto; null volta a herdar do ancestral. A chave é OBRIGATÓRIA: aceitar corpo sem ela faria um nome de campo digitado errado limpar a quota em silêncio.
Saídas
200 com {Tenant}; 404 se {id} não é acessível; 422 em valor inválido.
Efeitos
escreve no asender-core.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
Quota gravada; `EffectiveQuota` já recalculada.
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Papel efetivo na conta não governa a árvore (`GovernanceRole`): só
`owner`/`admin` (direto ou do ancestral concedente) criam, movem ou
mudam cota de sub-conta — `developer`/`viewer` não (2026-10-04).
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Supressões da conta e as globais que afetam seus envios (ADR-0153 D7).
Sessão. {id} autorizado por pertinência (alheia = 404) e papel owner/admin (deny-default; demais 403, sem hop ao core). Só state, origin, q, cursor, limit viajam (valor até 256 caracteres, 400 acima). corpo e status do core voltam como vieram.
Onde é usada
aba "Atividade" da tela /auditoria do admin.asender.
Efeitos
só leitura.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
state
query
não
origin
query
não
q
query
não
cursor
query
não
limit
query
não
Respostas
código
significado
200
`{itens: SupressaoConta[], proximo_cursor?, globais: SupressaoEntrada[]}` (globais só sem cursor).
400
valor de filtro longo demais
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`AdminRole` — não é owner nem admin.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
502
core indisponível
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/suppressions' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/tenants/{id}/suppressions/{sid}
Detalhe de uma supressão da conta com a trilha (owner/admin).
Onde é usada
detalhe na tela de supressões da conta.
Efeitos
uma leitura ao core com o ator.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
sid
path
sim
Respostas
código
significado
200
`{entrada, eventos[]}`.
400
sid inválido
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`AdminRole`.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
502
core indisponível
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/suppressions/sid_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/tenants/tree
Subárvore do tenant corrente, em pré-ordem.
Onde é usada
dashboard (settings/tenants) e backoffice.
Entradas
depth — inteiro positivo; valor não numérico é 422, não "sem limite" silencioso.
Saídas
200 com {TenantId, Tree:[...]}; 404 se o tenant pedido não é acessível.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
depth
query
não
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/tree' \
-H 'Authorization: Bearer SEU_TOKEN'
contacts
PATCH/api/contacts/{id}sem schema na spec
altera parcialmente um contato do tenant da SESSÃO — hoje o caminho do editor de consentimento por canal.
Onde é usada
tela de audiência do hub, ao revogar ou restaurar um canal.
Saídas
200 com {Contact}; 404 quando o contato não é do tenant; 422 em validação (canal fora dos quatro, corpo vazio).
Efeitos
escreve em messages.contacts e no consentimento por canal. # O tenant vem da SESSÃO, e o id vai na ROTA resolveSessionTenant resolve a conta do token verificado; o corpo nunca escolhe em quem se escreve (piso 3). É o que faz PATCH /api/contacts/<id do vizinho> ser 404 em vez de edição alheia — a checagem é do asender_mkt_api, com o tenant que ESTE handler passou. # Por que map[string]any e não uma struct PATCH é PARCIAL. Uma struct com campos zero-value não distingue "não mandou" de "mandou vazio", e aqui a diferença decide se o consentimento de um canal muda ou fica como está. Repassar o corpo cru deixa a decisão com quem tem a regra — o asender_mkt_api — em vez de duplicá-la no BFF, que é como as duas cópias do filtro de descadastro divergiram antes (ver o contrato da audiência).
Protected Resource Metadata (RFC 9728) da API REST `https://api.asender.net`.
Documento público para clientes OAuth descobrirem o servidor de autorização (auth.asender.net), os escopos e a documentação. É o alvo do resource_metadata do WWW-Authenticate dos 401 de /v1. Cache de 5 minutos; CORS liberado (Access-Control-Allow-Origin: *, preflight OPTIONS → 204).
Onde é usada
server.registrarMCP (raiz e /mcp).
Respostas
código
significado
200
OK
Exemplo
curl -X GET 'https://api.asender.net/.well-known/oauth-protected-resource' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/.well-known/oauth-protected-resource/mcp
Protected Resource Metadata (RFC 9728) do servidor MCP `https://api.asender.net/mcp`.
Igual ao da raiz, com resource = MCP_RECURSO. 404 quando o MCP está desligado (MCP_ATIVO=false).
Onde é usada
server.registrarMCP (raiz e /mcp).
Respostas
código
significado
200
OK
404
MCP desligado.
Exemplo
curl -X GET 'https://api.asender.net/.well-known/oauth-protected-resource/mcp' \
-H 'Authorization: Bearer SEU_TOKEN'
curl -X GET 'https://api.asender.net/mcp' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/mcp
Servidor MCP remoto (Streamable HTTP stateless, JSON-RPC 2.0, somente leitura).
Um POST por mensagem JSON-RPC (sem sessão, sem SSE, sem lote). Métodos: initialize, notifications/initialized (→ 202), ping, tools/list, tools/call. Versões aceitas em MCP-Protocol-Version: 2025-11-25, 2025-06-18, 2025-03-26 (ausente = 2025-03-26).
Autenticação:Authorization: Bearer com JWT de app OAuth cujo aud contém https://api.asender.net/mcpouhttps://api.asender.net, ou chave sk_*. O escopo é por ferramenta: tools/list só mostra as que o token/chave pode usar e tools/call sem escopo devolve isError. Cada ferramenta roda o mesmo handler da rota REST (mesmo escopo, produto e PDP do usuário). Falha de negócio (400/403/404/503 da rota) volta como resultado com isError: true; só erro de protocolo usa códigos JSON-RPC (-32700, -32600, -32601, -32602).
Origin presente e fora da lista (API, AS, loopback, MCP_ORIGENS_PERMITIDAS) → 403. Corpo ≤ 1 MiB. 401 leva WWW-Authenticate: Bearer [error="invalid_token", ]resource_metadata="…/oauth-protected-resource/mcp".
Onde é usada
server.routes (depois de auth e rate limit).
Respostas
código
significado
200
Resposta JSON-RPC (`result` ou `error`).
202
Notificação ou resposta do cliente aceita
400
JSON inválido
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Origin não permitido.
413
Corpo acima de 1 MiB.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
allowlist → corpo → auth `/v1/me/<resto>` → status e corpo como vieram (inclusive o 403 `ReauthRequired` com `metodos_aceitos`, que a tela precisa ler intacto) + `Retry-After` do 429.
Onde é usada
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
Exemplo
curl -X GET 'https://api.asender.net/api/me/senhas-app' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/me/senhas-appsem schema na spec
allowlist → corpo → auth `/v1/me/<resto>` → status e corpo como vieram (inclusive o 403 `ReauthRequired` com `metodos_aceitos`, que a tela precisa ler intacto) + `Retry-After` do 429.
Onde é usada
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
allowlist → corpo → auth `/v1/me/<resto>` → status e corpo como vieram (inclusive o 403 `ReauthRequired` com `metodos_aceitos`, que a tela precisa ler intacto) + `Retry-After` do 429.
Onde é usada
grupo SessionAuth+Impersonacao do BFF (server/server_me.go). Erros próprios: 404 rota fora do contrato; 400 corpo inválido; 401 sem token; 502 auth fora.
curl -X GET 'https://api.asender.net/' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/version
Versão e commit do binário em execução.
Onde é usada
rota pública, usada por ops e por quem investiga incidente. Pública porque a primeira pergunta de todo incidente é "que versão está no ar?", e ela tem de ser respondível sem credencial — quem investiga muitas vezes ainda não tem uma. O que sai é só isso: nunca configuração, nunca endereço interno, que é topologia e pertence ao console de plataforma.
Respostas
código
significado
200
OK
Exemplo
curl -X GET 'https://api.asender.net/version' \
-H 'Authorization: Bearer SEU_TOKEN'
public-account
GET/v1/account
Conta da API key usada (ou do app OAuth, com `ClientId`), mais o contexto da credencial.
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/v1/account' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/account/usage
Contadores de consumo do tenant no período corrente.
Onde é usada
superfície pública /v1, consumida por integrações de cliente.
Saídas
200 com {Usage:{Period,EmailsSent,SmsSent,PushSent,ApiCalls}}; 404 se o tenant da API key não existe mais no core; 502 se o core está fora. DEFEITO QUE ISTO TAMBÉM FECHA (item 7 do briefing Y2, metade da borda): o core respondia 404 quando não havia linha em core.usage_counters — e o seed não cria nenhuma —, e este handler traduzia QUALQUER erro do upstream em 502. Ou seja: a rota respondia "serviço indisponível" para todo tenant que ainda não enviou nada, mandando o alerta para o time errado. O 404 do core agora só significa "tenant inexistente" (a correção principal está em asender_plataforma_api/internal/service/usage.Get) e é traduzido como 404, não 502. DEFEITO QUE ISTO FECHA (T3 §3.6): o payload do core saía CRU sob Usage ({"Usage":{"sent":42}}), fora do contrato PascalCase da API pública (API_DESIGN.md §51). O cliente que segue o contrato lê Usage.Sent e recebe undefined — mesma classe de divergência de fronteira do GET /api/tenants.
Respostas
código
significado
200
Consumo do período. Tenant sem nenhum envio devolve os contadores em
zero, com `UpdatedAt: null` — ausência de linha é consumo zero, não
ausência de recurso.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
Core fora **ou** tenant sem contador de uso (ver `x-asender-divergence`).
Exemplo
curl -X GET 'https://api.asender.net/v1/account/usage' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/me
Quem autorizou o app OAuth, em que conta e com que escopos (só token OAuth, escopo openid).
Onde é usada
GET /v1/me, depois do PublicAuth + dados.ExigirProduto("").
Erros
401 sem principal OAuth; 502 se o core falha.
Respostas
código
significado
200
OK (JSON em snake_case, sem RequestId)
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Sem o escopo openid, chave sk_* (rota só de OAuth) ou usuário sem acesso à conta.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/v1/me' \
-H 'Authorization: Bearer SEU_TOKEN'
public-analytics
GET/v1/analytics/breakdown
Janela agrupada por uma dimensão.
Escopo analytics:read. dimension: page (padrão), source, medium, campaign, country, device ou form. Valor não preenchido sai como (none). Janela de até 92 dias.
Saídas
200 {from,to,dimension,data,next_cursor:null}.
Parâmetros
nome
em
obrigatório
descrição
dimension
query
não
Dimensão de agrupamento.
limit
query
não
Itens por página.
from
query
não
Início INCLUSIVO da janela: RFC 3339 ou `YYYY-MM-DD` (= 00:00:00Z). Padrão: `to` menos 30 dias.
to
query
não
Fim EXCLUSIVO da janela (mesmo formato). Padrão: agora.
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
`503 ServiceUnavailable` com `Retry-After` — a fonte dos dados está fora; o resto da API não é afetado.
Exemplo
curl -X GET 'https://api.asender.net/v1/analytics/breakdown' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/analytics/forms
Resumo de formulários ou funil de um formulário.
Escopo analytics:read. Sem form_id: um item por formulário (visualizações, inícios, envios e taxa), janela de até 92 dias. Com form_id: o funil completo do formulário (série diária, por origem/dispositivo/página, abandono por campo), janela de até 400 dias. Formulário de outra conta ou inexistente: relatório zerado (a existência não é confirmada).
Saídas
200; 400; 401; 503 com Retry-After.
Parâmetros
nome
em
obrigatório
descrição
limit
query
não
Itens por página.
from
query
não
Início INCLUSIVO da janela: RFC 3339 ou `YYYY-MM-DD` (= 00:00:00Z). Padrão: `to` menos 30 dias.
to
query
não
Fim EXCLUSIVO da janela (mesmo formato). Padrão: agora.
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
`503 ServiceUnavailable` com `Retry-After` — a fonte dos dados está fora; o resto da API não é afetado.
Exemplo
curl -X GET 'https://api.asender.net/v1/analytics/forms' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/analytics/overview
Totais de tráfego e conversão da janela.
Escopo analytics:read. Visitas (sessões), visualizações de página, conversões (sessões com envio de formulário), taxa e o funil de formulários. Janela de até 92 dias. JSON snake_case; o tenant é sempre o da credencial.
Onde é usada
apps e chaves com analytics:read.
Saídas
200 {from,to,totals}; 400 janela/filtro inválido; 401; 503 com Retry-After se o runtime estiver fora.
Efeitos
uma leitura no runtime.
Parâmetros
nome
em
obrigatório
descrição
from
query
não
Início INCLUSIVO da janela: RFC 3339 ou `YYYY-MM-DD` (= 00:00:00Z). Padrão: `to` menos 30 dias.
to
query
não
Fim EXCLUSIVO da janela (mesmo formato). Padrão: agora.
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
`503 ServiceUnavailable` com `Retry-After` — a fonte dos dados está fora; o resto da API não é afetado.
Exemplo
curl -X GET 'https://api.asender.net/v1/analytics/overview' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/analytics/timeseries
Série diária (America/Sao_Paulo).
Escopo analytics:read. Um ponto por dia com as mesmas contagens do overview; a soma da série fecha com o total. Janela de até 92 dias.
Saídas
200 {from,to,interval,timezone,data,next_cursor:null}; erros como o overview.
Parâmetros
nome
em
obrigatório
descrição
from
query
não
Início INCLUSIVO da janela: RFC 3339 ou `YYYY-MM-DD` (= 00:00:00Z). Padrão: `to` menos 30 dias.
to
query
não
Fim EXCLUSIVO da janela (mesmo formato). Padrão: agora.
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
`503 ServiceUnavailable` com `Retry-After` — a fonte dos dados está fora; o resto da API não é afetado.
Exemplo
curl -X GET 'https://api.asender.net/v1/analytics/timeseries' \
-H 'Authorization: Bearer SEU_TOKEN'
public-campaigns
GET/v1/campaigns
Campanhas de e-mail da conta (incremental).
Escopo campaigns:read, produto E-mail Marketing. Ordem (updated_at, id) crescente; updated_after (exclusivo) ou next_cursor para sincronizar. snake_case; envelope {data, next_cursor}. Conta cujo canal ainda não migrou para a ferramenta dedicada → 503 (ServiceUnavailable, Retry-After). Parâmetro de query fora da lista → 400 (nada é ignorado em silêncio).
Saídas
200 {data, next_cursor}; 400; 503 (canal não cortado); 502.
Parâmetros
nome
em
obrigatório
descrição
limit
query
não
Itens por página.
cursor
query
não
Cursor opaco (`next_cursor` da página anterior). Estável: não pula nem repete. Inválido → 400.
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 UpstreamError` — a ferramenta de origem respondeu com erro; o corpo dela nunca é repassado.
503
`503 ServiceUnavailable` com `Retry-After` — a fonte dos dados está fora; o resto da API não é afetado.
Exemplo
curl -X GET 'https://api.asender.net/v1/campaigns' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/campaigns/{id}
Uma campanha.
Escopo campaigns:read. Objeto da campanha, sem envelope. Inexistente ou de outra conta → 404 idêntico. Conta cujo canal ainda não migrou para a ferramenta dedicada → 503 (ServiceUnavailable, Retry-After). Parâmetro de query fora da lista → 400 (nada é ignorado em silêncio).
Saídas
200 com o objeto da campanha (recurso único, sem envelope); 404 (inclusive de outra conta).
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
404
`404 NotFound` — o recurso não existe **ou é de outra conta** (mesma resposta).
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 UpstreamError` — a ferramenta de origem respondeu com erro; o corpo dela nunca é repassado.
503
`503 ServiceUnavailable` com `Retry-After` — a fonte dos dados está fora; o resto da API não é afetado.
Exemplo
curl -X GET 'https://api.asender.net/v1/campaigns/id_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/campaigns/{id}/report
Contadores de entrega e engajamento de uma campanha.
Escopo campaigns:read. Soma dos contadores dos e-mails da campanha: {data: {...}}. Inexistente ou de outra conta → 404 idêntico. Conta cujo canal ainda não migrou para a ferramenta dedicada → 503 (ServiceUnavailable, Retry-After). Parâmetro de query fora da lista → 400 (nada é ignorado em silêncio).
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
404
`404 NotFound` — o recurso não existe **ou é de outra conta** (mesma resposta).
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 UpstreamError` — a ferramenta de origem respondeu com erro; o corpo dela nunca é repassado.
503
`503 ServiceUnavailable` com `Retry-After` — a fonte dos dados está fora; o resto da API não é afetado.
Exemplo
curl -X GET 'https://api.asender.net/v1/campaigns/id_AQUI/report' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/reports/email/domains
Desempenho por domínio/provedor de destino.
Escopo campaigns:read. Entregas, aberturas e bounces por provedor: {data: {...}}. Conta cujo canal ainda não migrou para a ferramenta dedicada → 503 (ServiceUnavailable, Retry-After). Parâmetro de query fora da lista → 400 (nada é ignorado em silêncio).
Onde é usada
overview, breakdown, timeseries e forms (lista).
Parâmetros
nome
em
obrigatório
descrição
from
query
não
Início: RFC 3339 ou `YYYY-MM-DD`. Use `period` OU `from`+`to`.
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 UpstreamError` — a ferramenta de origem respondeu com erro; o corpo dela nunca é repassado.
503
`503 ServiceUnavailable` com `Retry-After` — a fonte dos dados está fora; o resto da API não é afetado.
Exemplo
curl -X GET 'https://api.asender.net/v1/reports/email/domains' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/reports/email/overview
Panorama de e-mail da conta.
Escopo campaigns:read. Totais da janela em days (não aceita from/to): {data: {...}}. Conta cujo canal ainda não migrou para a ferramenta dedicada → 503 (ServiceUnavailable, Retry-After). Parâmetro de query fora da lista → 400 (nada é ignorado em silêncio).
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 UpstreamError` — a ferramenta de origem respondeu com erro; o corpo dela nunca é repassado.
503
`503 ServiceUnavailable` com `Retry-After` — a fonte dos dados está fora; o resto da API não é afetado.
Exemplo
curl -X GET 'https://api.asender.net/v1/reports/email/overview' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/reports/email/timeseries
Série temporal de e-mail.
Escopo campaigns:read. Pontos por granularity (hour aceita janela curta), com os totais e o período anterior: {data: {...}}. Conta cujo canal ainda não migrou para a ferramenta dedicada → 503 (ServiceUnavailable, Retry-After). Parâmetro de query fora da lista → 400 (nada é ignorado em silêncio).
Onde é usada
overview, breakdown, timeseries e forms (lista).
Parâmetros
nome
em
obrigatório
descrição
from
query
não
Início: RFC 3339 ou `YYYY-MM-DD`. Use `period` OU `from`+`to`.
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 UpstreamError` — a ferramenta de origem respondeu com erro; o corpo dela nunca é repassado.
503
`503 ServiceUnavailable` com `Retry-After` — a fonte dos dados está fora; o resto da API não é afetado.
Exemplo
curl -X GET 'https://api.asender.net/v1/reports/email/timeseries' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/reports/email/top-emails
E-mails com melhor (ou maior) desempenho.
Escopo campaigns:read. Ranking por order (volume, open_rate, click_rate); min_delivered descarta amostras pequenas: {data: {...}}. Conta cujo canal ainda não migrou para a ferramenta dedicada → 503 (ServiceUnavailable, Retry-After). Parâmetro de query fora da lista → 400 (nada é ignorado em silêncio).
Onde é usada
overview, breakdown, timeseries e forms (lista).
Parâmetros
nome
em
obrigatório
descrição
from
query
não
Início: RFC 3339 ou `YYYY-MM-DD`. Use `period` OU `from`+`to`.
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 UpstreamError` — a ferramenta de origem respondeu com erro; o corpo dela nunca é repassado.
503
`503 ServiceUnavailable` com `Retry-After` — a fonte dos dados está fora; o resto da API não é afetado.
Exemplo
curl -X GET 'https://api.asender.net/v1/reports/email/top-emails' \
-H 'Authorization: Bearer SEU_TOKEN'
public-conversations
GET/v1/conversations
Conversas da conta (incremental).
Escopo conversations:read. Ordenadas por atividade ascendente; updated_after traz o que mudou — inclusive conversa que só recebeu mensagem. Releia com ~5 minutos de sobreposição e deduplique por id (uma transação concorrente pode confirmar depois do carimbo já lido). Não saem chamados de suporte da plataforma, notas internas nem anexos (só attachment_count). lead_id é o id público do lead: o MESMO de GET /v1/leads (id) e de /v1/conversions (lead_id).
Onde é usada
a busca do cabeçalho do hub (asender_plataforma_front, components/busca-do-app.tsx).
Erros
400 termo; 401 sem sessão; 404 conta alheia (resolveSessionTenant — nunca 403, que seria oráculo); 502 se o PDP não responde (sem ele não há como saber o que a pessoa pode ver: nega).
Parâmetros
nome
em
obrigatório
descrição
updated_after
query
não
RFC 3339; estritamente depois.
limit
query
não
Itens por página.
cursor
query
não
Cursor opaco (`next_cursor` da página anterior). Estável: não pula nem repete.
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
`503 ServiceUnavailable` com `Retry-After` — a fonte dos dados está fora; o resto da API não é afetado.
Exemplo
curl -X GET 'https://api.asender.net/v1/conversations' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/conversations/{id}/messages
Mensagens de uma conversa.
Escopo conversations:read. Em ordem de chegada, com cursor. Só mensagens públicas de contato, atendente, bot, aparelho e API (sem notas internas nem mensagens de sistema). Conversa inexistente ou de outra conta: 404 idêntico.
Saídas
200 {data,next_cursor}; 400; 401; 404; 503 com Retry-After.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Id da conversa (`cnv_…`).
created_after
query
não
RFC 3339; estritamente depois.
limit
query
não
Itens por página.
cursor
query
não
Cursor opaco (`next_cursor` da página anterior). Estável: não pula nem repete.
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
404
`404 NotFound` — o recurso não existe **ou é de outra conta** (mesma resposta).
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
`503 ServiceUnavailable` com `Retry-After` — a fonte dos dados está fora; o resto da API não é afetado.
Exemplo
curl -X GET 'https://api.asender.net/v1/conversations/id_AQUI/messages' \
-H 'Authorization: Bearer SEU_TOKEN'
public-data
GET/v1/conversions
Conversões enviadas (ou não) às plataformas de anúncios, e o desfecho.
Escopo conversions:read. Fatos imutáveis do CRM — conversão sent à plataforma (Meta CAPI, Google Ads, TikTok), ignored (com o motivo, ex.: sem consentimento) e result da entrega — em ordem de registro. Use created_after (exclusivo) ou next_cursor. Sem payload cru, token ou hash; lead_id é o id público do lead.
Saídas
200 envelope; 400 parâmetro/cursor inválido; 503 se o CRM não responde.
Parâmetros
nome
em
obrigatório
descrição
limit
query
não
cursor
query
não
created_after
query
não
Respostas
código
significado
200
OK
400
`InvalidParameter`.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
CRM indisponível.
Exemplo
curl -X GET 'https://api.asender.net/v1/conversions' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/forms
Formulários da conta e o schema dos seus campos.
Escopo forms:read. Ordem por id; cursor opaco. Não saem origens CORS, destinos (URLs/segredos de webhook) nem o snippet.
Saídas
200 envelope; 400 parâmetro inválido; 503 se o pages_api não responde.
Parâmetros
nome
em
obrigatório
descrição
limit
query
não
cursor
query
não
Respostas
código
significado
200
OK
400
`InvalidParameter`.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
Pages indisponível.
Exemplo
curl -X GET 'https://api.asender.net/v1/forms' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/forms/{id}/submissions
Envios de um formulário, do mais antigo ao mais novo.
Escopo forms:read. Ordem (created_at, id) crescente; use created_after (exclusivo) ou next_cursor para sincronizar. O corpo do formulário vem em claro em fields (é o lead); IP, user agent, score e roteamento não saem. Cada página grava a trilha de acesso a dado pessoal da conta. Formulário inexistente ou de outra conta → 404 idêntico.
Saídas
200 envelope; 400 parâmetro/cursor inválido; 404 formulário inexistente ou de outra conta; 503 se pages_api ou runtime não respondem.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
limit
query
não
cursor
query
não
created_after
query
não
RFC 3339, exclusivo. Ignorado se `cursor` vier.
Respostas
código
significado
200
OK
400
`InvalidParameter`.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
404
Formulário não encontrado.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
Pages ou runtime indisponível.
Exemplo
curl -X GET 'https://api.asender.net/v1/forms/id_AQUI/submissions' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/leads
Leads da conta, em ordem de atualização (sincronização incremental).
Contatos do CRM da conta do token, do mais antigo ao mais novo por (updated_at, id). Para sincronizar, guarde o maior updated_at recebido e envie-o em updated_after (exclusivo), ou siga next_cursor até null. Paginação por keyset estável: linhas com o mesmo instante não se repetem nem se perdem. Contatos apagados ou anonimizados deixam de aparecer.
Escopo leads:read. Formato:snake_case (diferente do PascalCase das rotas legadas de envio). Campos expostos: ver Lead; não saem CPF, endereço, IP, dono interno nem índices. Origem do CRM fora do ar: 503 com Retry-After.
Saídas
200 envelope; 400 parâmetro/cursor inválido; 503 se o CRM não responde.
Parâmetros
nome
em
obrigatório
descrição
limit
query
não
cursor
query
não
Cursor opaco (`next_cursor`). Inválido → 400.
updated_after
query
não
RFC 3339, exclusivo. Ignorado se `cursor` vier.
Respostas
código
significado
200
OK
400
`InvalidParameter`.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
curl -X GET 'https://api.asender.net/v1/leads' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/leads
Cria um lead na conta.
Cria um contato no CRM da conta da chave (origem api). Escopo leads:write, só chave sk_* (app OAuth nunca escreve leads → 403). A conta vem da chave; o corpo não aceita id, tenant_id, dono, empresa, consentimento, origem/UTM nem dados pessoais fortes — campo desconhecido é 400 (inclusive os nomes em português).
Identidade. Informe email e/ou phone (ao menos um). E-mail e telefone são guardados cifrados e deduplicados por índice cego: outro lead da conta com o mesmo e-mail ou telefone → 409 Conflict.
Idempotência. Envie Idempotency-Key (24 h, por conta e rota): repetir a mesma chave com o mesmo corpo devolve o mesmo resultado (mesmo status e corpo, com o cabeçalho Idempotent-Replayed: true) sem criar outro lead; a mesma chave com outro corpo é 422 IdempotencyKeyReused; enquanto a primeira chamada ainda roda, 409 IdempotencyInProgress (com Retry-After). Falha nossa (5xx) não consome a chave. Chave malformada é recusada com 422, nunca ignorada.
Saídas
201 com o lead; 400 corpo inválido; 409 Conflict (já existe lead com esse e-mail/telefone) ou IdempotencyInProgress; 422 IdempotencyKeyReused; 403 token OAuth; 413; 503 se o CRM não responde.
Parâmetros
nome
em
obrigatório
descrição
Idempotency-Key
header
não
Chave de idempotência da escrita de lead (1 a 128 caracteres de `[A-Za-z0-9._:-]`; malformada
é `422`). Vale 24 h por conta e rota: a repetição com o mesmo corpo devolve o resultado
original (`Idempotent-Replayed: true`); com outro corpo, `422 IdempotencyKeyReused`.
Respostas
código
significado
201
Lead criado (ou resultado guardado, se for repetição da mesma `Idempotency-Key`).
400
`ValidationError`: campo desconhecido, tipo errado, e-mail/telefone/etapa/país inválido ou nenhum identificador.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
409
`Conflict` (já existe lead com esse e-mail/telefone) ou `IdempotencyInProgress` (`Retry-After`).
413
`PayloadTooLarge` (corpo acima de 256 KiB).
422
`IdempotencyKeyReused` ou `Idempotency-Key` malformada (`ValidationError`).
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
Escopo leads:read. Id inexistente ou de outra conta → 404 idêntico (nunca 403, para não confirmar a existência).
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
404
Lead não encontrado.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
CRM indisponível.
Exemplo
curl -X GET 'https://api.asender.net/v1/leads/id_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
PATCH/v1/leads/{id}
Atualiza parcialmente um lead.
Altera só os campos enviados (envie ao menos um). Escopo leads:write, só chave sk_*. Texto opcional (last_name, job_title…) com ""limpa o valor; tags presente substitui a lista (vazia limpa); custom_fieldsmescla (valor "" remove a chave). Id inexistente ou de outra conta → 404 idêntico (nunca 403). Mesmas regras de corpo fechado, validação e Idempotency-Key do POST /v1/leads.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Idempotency-Key
header
não
Chave de idempotência da escrita de lead (1 a 128 caracteres de `[A-Za-z0-9._:-]`; malformada
é `422`). Vale 24 h por conta e rota: a repetição com o mesmo corpo devolve o resultado
original (`Idempotent-Replayed: true`); com outro corpo, `422 IdempotencyKeyReused`.
Respostas
código
significado
200
Lead atualizado (ou resultado guardado, se for repetição da mesma `Idempotency-Key`).
400
`ValidationError` (corpo vazio, campo desconhecido ou valor inválido).
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
404
Lead não encontrado (inexistente ou de outra conta).
409
`IdempotencyInProgress` (`Retry-After`).
413
`PayloadTooLarge`.
422
`IdempotencyKeyReused` ou `Idempotency-Key` malformada.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
Escopo dns:read (só chave). Histórico append-only de toda tentativa de escrita (ok, failed ou dry_run), do mais novo ao mais antigo, com before/after, o ator (actor: id da chave ou do usuário) e a origem (source: tela, api ou mcp). Parâmetro de consulta desconhecido é 400.
Parâmetros
nome
em
obrigatório
descrição
zone
query
não
Id da zona (filtra o histórico).
page
query
não
per_page
query
não
Respostas
código
significado
200
Alterações e total.
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Sem o escopo (`InsufficientScope`), chave de app OAuth, conta suspensa, escrita fora de produção (`NonProductionBlocked`) ou o token Cloudflare sem permissão na zona (`ProviderPermissionDenied`). `Error.Details.Code` traz o código estável do serviço de DNS.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
503
Serviço de DNS fora do ar ou a Cloudflare indisponível (`ServiceUnavailable`/`ProviderUnavailable`); tente de novo depois de `Retry-After`.
Exemplo
curl -X GET 'https://api.asender.net/v1/dns/changes' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/dns/zones
Lista as zonas de DNS da conta.
Escopo dns:read (só chave; token OAuth recebe 403). Devolve as zonas de todas as conexões Cloudflare da conta (a conexão — o token da Cloudflare — é colada no painel; não há rota de conexão nesta API). Se a Cloudflare falhar para uma conexão, o cache aparece e a conexão vem em warnings. Sem parâmetros de consulta (parâmetro desconhecido é 400). Resposta Cache-Control: no-store.
Respostas
código
significado
200
Zonas e avisos, em inglês e snake_case.
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Sem o escopo (`InsufficientScope`), chave de app OAuth, conta suspensa, escrita fora de produção (`NonProductionBlocked`) ou o token Cloudflare sem permissão na zona (`ProviderPermissionDenied`). `Error.Details.Code` traz o código estável do serviço de DNS.
404
Zona, registro ou conexão inexistente **ou de outra conta** (`NotFound`, idêntico nos dois casos).
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
503
Serviço de DNS fora do ar ou a Cloudflare indisponível (`ServiceUnavailable`/`ProviderUnavailable`); tente de novo depois de `Retry-After`.
Exemplo
curl -X GET 'https://api.asender.net/v1/dns/zones' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/dns/zones/{zona}/records
Lista os registros de uma zona.
Escopo dns:read (só chave). A Cloudflare é lida por inteiro; page/per_page recortam o resultado. Zona inexistente ou de outra conta: 404 idêntico. Parâmetro de consulta desconhecido (inclusive tenant_id) é 400: filtro digitado errado nunca vira "sem filtro".
Parâmetros
nome
em
obrigatório
descrição
type
query
não
Tipo do registro: A, AAAA, CNAME, TXT, MX, NS…
name
query
não
Nome completo (FQDN) ou relativo à zona.
page
query
não
per_page
query
não
Respostas
código
significado
200
Registros e total.
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Sem o escopo (`InsufficientScope`), chave de app OAuth, conta suspensa, escrita fora de produção (`NonProductionBlocked`) ou o token Cloudflare sem permissão na zona (`ProviderPermissionDenied`). `Error.Details.Code` traz o código estável do serviço de DNS.
404
Zona, registro ou conexão inexistente **ou de outra conta** (`NotFound`, idêntico nos dois casos).
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
503
Serviço de DNS fora do ar ou a Cloudflare indisponível (`ServiceUnavailable`/`ProviderUnavailable`); tente de novo depois de `Retry-After`.
Exemplo
curl -X GET 'https://api.asender.net/v1/dns/zones/zona_AQUI/records' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/dns/zones/{zona}/records
Cria um registro de DNS na zona.
Escopo dns:write (só chave). Tipos aceitos: A, AAAA, CNAME, TXT, MX, NS. ttl é 1 (automático) ou de 60 a 86400 (padrão 1); priority é obrigatório só no MX. NS no apex é recusado (422 ProtectedRecord).
Controles de escrita.
dry_run=1 (ou true) devolve o plano (dry_run: true, action, before, after,
requires_confirmation, confirm) sem tocar a Cloudflare; fica no histórico como dry_run. Qualquer outro valor que não seja 1/true/0/false é 400, para que dry_run=yes nunca execute a escrita de verdade.
X-Asender-Confirmar: <nome do registro> é obrigatório para MX, SPF e DMARC do apex
(428 ConfirmationRequired sem ele). O valor é o nome completo do registro.
Idempotency-Key (1 a 128 caracteres de [A-Za-z0-9._:-], 24 h por conta): repetir a
mesma chave com o mesmo corpo devolve o resultado original; a mesma chave com outro corpo é 422 IdempotencyKeyReused.
Ambiente e ritmo. Fora de produção toda escrita responde 403 NonProductionBlocked (salvo zonas liberadas por configuração). Leitura 60/min e escrita 10/min por conta (429 RateLimited com Retry-After). Todo registro criado leva o comentário asender:<id da chave> e a tentativa entra no histórico (GET /v1/dns/changes).
Parâmetros
nome
em
obrigatório
descrição
dry_run
query
não
`1`/`true`: devolve o plano sem tocar a Cloudflare. Outro valor é `400`.
X-Asender-Confirmar
header
não
Nome completo do registro. **Obrigatório** no `DELETE` e em qualquer escrita em `MX`, SPF ou
DMARC do apex (`428 ConfirmationRequired` sem ele). Um valor diferente do nome também é recusado.
Idempotency-Key
header
não
Chave de idempotência da escrita de DNS (1 a 128 caracteres de `[A-Za-z0-9._:-]`; malformada é
`400`). Vale 24 h por conta: a mesma chave com o mesmo pedido devolve o resultado original sem
tocar a Cloudflare de novo; a mesma chave com outro pedido é `422 IdempotencyKeyReused`.
Respostas
código
significado
200
Com `dry_run`, o plano (nada foi alterado).
201
Registro criado.
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Sem o escopo (`InsufficientScope`), chave de app OAuth, conta suspensa, escrita fora de produção (`NonProductionBlocked`) ou o token Cloudflare sem permissão na zona (`ProviderPermissionDenied`). `Error.Details.Code` traz o código estável do serviço de DNS.
404
Zona, registro ou conexão inexistente **ou de outra conta** (`NotFound`, idêntico nos dois casos).
413
`413 PayloadTooLarge` — corpo acima de 2 MiB.
422
Recusado pelas regras do DNS: `ProtectedRecord` (NS no apex), `ProviderRejected` (a Cloudflare não aceitou), `IdempotencyKeyReused`. `Error.Details` traz `Code` e `Detail`.
428
`ConfirmationRequired`: envie `X-Asender-Confirmar` com o nome do registro (ou use `dry_run=1` para pré-visualizar).
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
503
Serviço de DNS fora do ar ou a Cloudflare indisponível (`ServiceUnavailable`/`ProviderUnavailable`); tente de novo depois de `Retry-After`.
Escopo dns:write (só chave). ExigeX-Asender-Confirmar com o nome completo do registro (428 ConfirmationRequired sem ele); use dry_run=1 antes para ver o que seria apagado. A resposta traz o registro como era (deleted) e o histórico guarda o before, para recriá-lo. NS no apex não pode ser apagado (422 ProtectedRecord).
Parâmetros
nome
em
obrigatório
descrição
dry_run
query
não
`1`/`true`: devolve o plano sem tocar a Cloudflare. Outro valor é `400`.
X-Asender-Confirmar
header
não
Nome completo do registro. **Obrigatório** no `DELETE` e em qualquer escrita em `MX`, SPF ou
DMARC do apex (`428 ConfirmationRequired` sem ele). Um valor diferente do nome também é recusado.
Idempotency-Key
header
não
Chave de idempotência da escrita de DNS (1 a 128 caracteres de `[A-Za-z0-9._:-]`; malformada é
`400`). Vale 24 h por conta: a mesma chave com o mesmo pedido devolve o resultado original sem
tocar a Cloudflare de novo; a mesma chave com outro pedido é `422 IdempotencyKeyReused`.
Respostas
código
significado
200
Apagado (`deleted`), ou o plano quando há `dry_run`.
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Sem o escopo (`InsufficientScope`), chave de app OAuth, conta suspensa, escrita fora de produção (`NonProductionBlocked`) ou o token Cloudflare sem permissão na zona (`ProviderPermissionDenied`). `Error.Details.Code` traz o código estável do serviço de DNS.
404
Zona, registro ou conexão inexistente **ou de outra conta** (`NotFound`, idêntico nos dois casos).
422
Recusado pelas regras do DNS: `ProtectedRecord` (NS no apex), `ProviderRejected` (a Cloudflare não aceitou), `IdempotencyKeyReused`. `Error.Details` traz `Code` e `Detail`.
428
`ConfirmationRequired`: envie `X-Asender-Confirmar` com o nome do registro (ou use `dry_run=1` para pré-visualizar).
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
503
Serviço de DNS fora do ar ou a Cloudflare indisponível (`ServiceUnavailable`/`ProviderUnavailable`); tente de novo depois de `Retry-After`.
Escopo dns:write (só chave). Envie ao menos um de content, ttl, priority, proxied, comment; type e name são imutáveis (apague e recrie). Os controles de escrita (dry_run, X-Asender-Confirmar, Idempotency-Key), o ritmo e o bloqueio fora de produção são os de POST /v1/dns/zones/{zona}/records. Mudar MX, SPF ou DMARC do apex exige X-Asender-Confirmar com o nome do registro (428 sem ele).
Parâmetros
nome
em
obrigatório
descrição
dry_run
query
não
`1`/`true`: devolve o plano sem tocar a Cloudflare. Outro valor é `400`.
X-Asender-Confirmar
header
não
Nome completo do registro. **Obrigatório** no `DELETE` e em qualquer escrita em `MX`, SPF ou
DMARC do apex (`428 ConfirmationRequired` sem ele). Um valor diferente do nome também é recusado.
Idempotency-Key
header
não
Chave de idempotência da escrita de DNS (1 a 128 caracteres de `[A-Za-z0-9._:-]`; malformada é
`400`). Vale 24 h por conta: a mesma chave com o mesmo pedido devolve o resultado original sem
tocar a Cloudflare de novo; a mesma chave com outro pedido é `422 IdempotencyKeyReused`.
Respostas
código
significado
200
Registro alterado (`record`), ou o plano quando há `dry_run`.
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
Sem o escopo (`InsufficientScope`), chave de app OAuth, conta suspensa, escrita fora de produção (`NonProductionBlocked`) ou o token Cloudflare sem permissão na zona (`ProviderPermissionDenied`). `Error.Details.Code` traz o código estável do serviço de DNS.
404
Zona, registro ou conexão inexistente **ou de outra conta** (`NotFound`, idêntico nos dois casos).
413
`413 PayloadTooLarge` — corpo acima de 2 MiB.
422
Recusado pelas regras do DNS: `ProtectedRecord` (NS no apex), `ProviderRejected` (a Cloudflare não aceitou), `IdempotencyKeyReused`. `Error.Details` traz `Code` e `Detail`.
428
`ConfirmationRequired`: envie `X-Asender-Confirmar` com o nome do registro (ou use `dry_run=1` para pré-visualizar).
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
503
Serviço de DNS fora do ar ou a Cloudflare indisponível (`ServiceUnavailable`/`ProviderUnavailable`); tente de novo depois de `Retry-After`.
channel é FIXO em email nesta rota — mandar ?channel=sms é 422 (parâmetro fora da allowlist), não filtro silencioso.
Onde é usada
API pública do cliente.
Entradas
status, q, cursor, limit (allowlist). channel do cliente é IGNORADO: nesta rota o canal é do contrato, não do parâmetro.
Saídas
200 com {Messages, NextCursor, HasMore}; 422 em query malformada.
Parâmetros
nome
em
obrigatório
descrição
status
query
não
q
query
não
Busca. `%` e `_` são escapados no repositório.
cursor
query
não
limit
query
não
created_after
query
não
RFC3339, estrito (>). Sincronização incremental: a lista passa a sair em ordem **ascendente** e `cursor` anda para frente. Inválido → 422; conta cujo canal ainda não migrou → 501.
Respostas
código
significado
200
Página de mensagens.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/v1/emails' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/emails/{id}
Um email com corpo, metadata e timeline de eventos.
Mensagem de outro tenant → 404 (não confirma existência). Mensagem deste tenant em outro canal também → 404: nesta rota só email existe.
Onde é usada
API pública do cliente.
Saídas
200 com {Message, Events}; 404 quando o id não é do tenant (o asender_mkt_api já responde 404 nesse caso, sem confirmar existência) ou quando o id aponta para outro canal — nesta rota só email existe. DEFEITO QUE ESTE HANDLER FECHA (T4 LOW-10 / item 6 do briefing Y2 — oráculo de enumeração): os dois 404 tinham mensagens DIFERENTES. Id inexistente devolvia "Resource not found." (vindo do writeUpstreamError) e id existente no próprio tenant, mas de outro canal, devolvia "Email not found.". Status igual, corpo distinto: bastava comparar a string para descobrir, sem autorização para aquele recurso, QUAIS ids existem — e ainda de brinde o canal deles. O escopo do vazamento era o próprio tenant (id de outro tenant já respondia "Resource not found."), o que mantém a severidade baixa, mas o §22.8 exige que existente-mas-negado e inexistente sejam INDISTINGUÍVEIS. Agora as duas saídas usam a mesma constante.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id da mensagem (`msg_<hex>`).
Respostas
código
significado
200
Detalhe.
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
Exemplo
curl -X GET 'https://api.asender.net/v1/emails/id_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/emails/batch
Até 500 emails independentes numa chamada.
Resultado parcial é legítimo e explícito. Falha de um item não derruba os outros: cada entrada de Results traz MessageId ou Error, sempre com o Index de origem.
Idempotency-Key vale por item (<chave>:<índice>); cada item traz ReusedIdempotency, e um item que reusa a chave com outro corpo falha com Error.CodeIdempotencyConflict.
Item com Source de domínio não verificado (2026-10-02): Status: failed com Error.CodeRemetenteNaoVerificado e a Message que diz o domínio e o link de Domínios (ou VerificacaoIndisponivel com a verificação fora). Outras recusas do upstream continuam genéricas (ValidationError/InternalError, sem o motivo interno).
Onde é usada
API pública do cliente.
Saídas
202 com {Results, Count}; cada item traz MessageId ou Error.
Efeitos
escrita no serviço de mensageria por item aceito.
Parâmetros
nome
em
obrigatório
descrição
Idempotency-Key
header
não
Chave de idempotência do envio (1 a 128 caracteres de `[A-Za-z0-9._:-]`;
uma chave malformada é recusada com `422`, nunca ignorada). **Tem
precedência** sobre o campo `IdempotencyKey` do corpo.
- **Mesma chave + mesmo corpo** = replay: devolve `202` com os ids da
mensagem ORIGINAL e `ReusedIdempotency: true`; nada é enviado nem
cobrado de novo.
- **Mesma chave + corpo DIFERENTE** = **`409 IdempotencyConflict`**: o
texto novo NÃO é enviado e a mensagem original não muda. Use uma chave
nova para uma mensagem nova.
O "mesmo corpo" ignora a ordem dos campos e o próprio campo
`IdempotencyKey`. Em `/v1/emails/batch` a chave vale por item
(`<chave>:<índice>`). A checagem de corpo vale hoje para SMS, push e
WhatsApp; no e-mail, até o canal migrar para a ferramenta dedicada, o
replay devolve o `202` original sem comparar o corpo.
**Aceite assíncrono e replay (resiliência).** Se o serviço de mensageria
estiver indisponível (timeout, conexão recusada ou 5xx do gateway), o
envio NÃO falha: a API persiste o pedido em disco e responde **`202` com
`Status: accepted`**, `MessageIds: []` e `IdempotencyKey` — a chave
EFETIVA, também devolvida no header de resposta `Idempotency-Key`. Se o
cliente não mandou chave, a API gera uma (`bffauto:…`) ANTES da primeira
tentativa. O pedido é reenviado ao destino, em ordem, com **essa mesma
chave** quando ele volta; por isso o replay nunca duplica o envio, nem
quando a primeira tentativa chegou ao destino e só a resposta se perdeu.
Para obter os ids depois, reenvie o MESMO `POST` com a mesma
`Idempotency-Key`: a resposta traz os ids originais e
`ReusedIdempotency: true`. Erros de validação (`4xx`) continuam
síncronos e nunca entram na fila; um pedido aceito que o destino recusar
de vez no replay (4xx) fica retido em arquivo de mortos para ação
manual, com alerta.
Respostas
código
significado
202
Lote processado (com ou sem itens rejeitados).
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
Enfileira um email (payload compatível com SES SendEmail).
Persiste no asender_mkt_api (mensagem + outbox no MESMO commit) e o publisher de outbox entrega ao NATS. Não publica direto no broker — seria dual-write.
Serviço de mensageria indisponível → 202 accepted (não 502). O pedido é persistido em disco pela API e reenviado em ordem com a mesma Idempotency-Key (a sua, ou uma gerada antes da 1ª tentativa); veja Idempotency-Key em Parâmetros. 4xx de validação seguem síncronos.
Cc e Bcc entram como destinatários independentes: o serviço materializa uma mensagem por destinatário, que é o comportamento correto de cópia oculta.
Source só de domínio verificado (quebra de contrato, 2026-10-02). O Source tem de ser de domínio VERIFICADO nesta conta (SPF e DKIM, em Admin → Domínios). Senão: 422 RemetenteNaoVerificado (inclusive domínio verificado em OUTRA conta), com Message que nomeia o domínio e traz o link de Domínios; verificação fora → 503 VerificacaoIndisponivel com Retry-After. Nada é enfileirado nem cobrado. Antes desta data o pedido era aceito (202) e o e-mail saía com o remetente da plataforma e o Source em Reply-To. Se o domínio perde a verificação DEPOIS do 202, a mensagem fica failed (RemetenteNaoVerificado) — nunca sai com outro remetente.
Destinatário suprimido é explícito (ADR-0134, 2026-10-21). O Asender não envia a destinatário na supressão global (TLD fora da lista da IANA, domínio de teste, caixa inexistente, duas recusas em 30 dias, entrada manual…). Lote com destinatários enviáveis E suprimidos: 202 com Suppressed[] (Email, Reason, Message) — só os enviáveis ganham mensagem em MessageIds. Todos suprimidos: 422 SuppressedRecipient com a Message humana. Depois, GET /v1/emails/{id} de uma mensagem suprimida traz Status: suppressed, SuppressionReason e ErrorMessage humano; suprimido não é falha. Motivos em SuppressionReason.
Onde é usada
API pública do cliente (autenticada por API key).
Saídas
202 com {MessageId, MessageIds, Status, ReusedIdempotency, Suppressed?}; 400 em validação local; 422 quando o asender_mkt_api recusa — com o código RemetenteNaoVerificado quando o Source não é de domínio verificado da conta (2026-10-02: antes o e-mail saía com o remetente da plataforma); 503 VerificacaoIndisponivel quando a verificação está fora; 502 se ele está fora.
Efeitos
escrita no serviço de mensageria. Idempotente por IdempotencyKey.
Parâmetros
nome
em
obrigatório
descrição
Idempotency-Key
header
não
Chave de idempotência do envio (1 a 128 caracteres de `[A-Za-z0-9._:-]`;
uma chave malformada é recusada com `422`, nunca ignorada). **Tem
precedência** sobre o campo `IdempotencyKey` do corpo.
- **Mesma chave + mesmo corpo** = replay: devolve `202` com os ids da
mensagem ORIGINAL e `ReusedIdempotency: true`; nada é enviado nem
cobrado de novo.
- **Mesma chave + corpo DIFERENTE** = **`409 IdempotencyConflict`**: o
texto novo NÃO é enviado e a mensagem original não muda. Use uma chave
nova para uma mensagem nova.
O "mesmo corpo" ignora a ordem dos campos e o próprio campo
`IdempotencyKey`. Em `/v1/emails/batch` a chave vale por item
(`<chave>:<índice>`). A checagem de corpo vale hoje para SMS, push e
WhatsApp; no e-mail, até o canal migrar para a ferramenta dedicada, o
replay devolve o `202` original sem comparar o corpo.
**Aceite assíncrono e replay (resiliência).** Se o serviço de mensageria
estiver indisponível (timeout, conexão recusada ou 5xx do gateway), o
envio NÃO falha: a API persiste o pedido em disco e responde **`202` com
`Status: accepted`**, `MessageIds: []` e `IdempotencyKey` — a chave
EFETIVA, também devolvida no header de resposta `Idempotency-Key`. Se o
cliente não mandou chave, a API gera uma (`bffauto:…`) ANTES da primeira
tentativa. O pedido é reenviado ao destino, em ordem, com **essa mesma
chave** quando ele volta; por isso o replay nunca duplica o envio, nem
quando a primeira tentativa chegou ao destino e só a resposta se perdeu.
Para obter os ids depois, reenvie o MESMO `POST` com a mesma
`Idempotency-Key`: a resposta traz os ids originais e
`ReusedIdempotency: true`. Erros de validação (`4xx`) continuam
síncronos e nunca entram na fila; um pedido aceito que o destino recusar
de vez no replay (4xx) fica retido em arquivo de mortos para ação
manual, com alerta.
Respostas
código
significado
202
Aceito e persistido; a entrega é assíncrona. Destinatários suprimidos vêm em `Suppressed[]`. Com o serviço de mensageria indisponível, `Status: accepted` (spool durável; replay com a mesma `Idempotency-Key`).
400
`400 ValidationError` com `Error.Details` mapeando campo → motivo.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
409
`409 IdempotencyConflict` — a `Idempotency-Key` já foi usada com um corpo
DIFERENTE. Nada foi enviado. Use uma chave nova, ou reenvie o corpo
original para obter o replay (`ReusedIdempotency: true`).
422
`422 SuppressedRecipient` — TODOS os destinatários estão suprimidos; nada
foi enfileirado. `Message` traz o(s) texto(s) humano(s) e
`Details.Suppressed[]` a lista `{Email, Reason, Message}`.
`422 RemetenteNaoVerificado` — o remetente não é de domínio verificado
nesta conta (ver `RemetenteOuInvalido`). `422 ValidationError` — demais
recusas do upstream.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
503
`503 VerificacaoIndisponivel` — a verificação do domínio do remetente
não respondeu. Fail-closed: nada foi enfileirado. Repita depois do
`Retry-After` (com a mesma `Idempotency-Key`).
Remetentes que a chave pode usar como `from` (domínios e caixas da conta).
Descubra os remetentes válidos antes de enviar. O tenant é o da API key; não há parâmetro de conta (query ou header do cliente é ignorado). Fonte: o asender_mail_api, que é quem verifica domínios e guarda as caixas.
Domains — todos os domínios da conta. Verified = ativo com SPF
e DKIM ok; só o verificado traz UsableFor: ["emails/send"]. Domínio não verificado é listado com Verified: false e UsableFor: [], para você ver o que falta. DnsStatus vem da verificação automática de DNS: ok, pending (ainda esperando o DNS propagar), outdated (um registro mudou depois de verificado) ou unknown (sem leitura).
Mailboxes — caixas ativas. Em domínio verificado: UsableFor
["emails/send", "mail:/v1/emails"]; em domínio não verificado só ["mail:/v1/emails"]. Em POST /v1/emails/send basta o domínio estar verificado; a caixa só é exigida na Mail API (POST /v1/emails).
Escopo: email:send. Mail API fora do ar: 503 SendersUnavailable com Retry-After (nunca uma lista parcial).
200; 401 sem tenant no contexto; 503 com Retry-After se o mail falhar.
Efeitos
leituras no mail_api; falha vai ao log com o request id.
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
`503 SendersUnavailable` — o diretório de remetentes (Mail API) não
respondeu. Repita depois do `Retry-After`.
Exemplo
curl -X GET 'https://api.asender.net/v1/senders' \
-H 'Authorization: Bearer SEU_TOKEN'
public-journeys
GET/v1/journeys
Jornadas da conta (incremental).
Escopo journeys:read. Estrutura (gatilho, tipo/ação/espera de cada passo) e estatísticas de fluxo (entries, in_progress, exits). Os parâmetros dos passos (corpo de e-mail, URL de webhook) não saem. updated_after filtra pela última alteração da jornada.
Onde é usada
a busca do cabeçalho do hub (asender_plataforma_front, components/busca-do-app.tsx).
Erros
400 termo; 401 sem sessão; 404 conta alheia (resolveSessionTenant — nunca 403, que seria oráculo); 502 se o PDP não responde (sem ele não há como saber o que a pessoa pode ver: nega).
Parâmetros
nome
em
obrigatório
descrição
updated_after
query
não
RFC 3339; estritamente depois.
limit
query
não
Itens por página.
cursor
query
não
Cursor opaco (`next_cursor` da página anterior). Estável: não pula nem repete.
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
`503 ServiceUnavailable` com `Retry-After` — a fonte dos dados está fora; o resto da API não é afetado.
Exemplo
curl -X GET 'https://api.asender.net/v1/journeys' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/journeys/{id}
Uma jornada.
Escopo journeys:read. Inexistente ou de outra conta: 404 idêntico.
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
404
`404 NotFound` — o recurso não existe **ou é de outra conta** (mesma resposta).
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
`503 ServiceUnavailable` com `Retry-After` — a fonte dos dados está fora; o resto da API não é afetado.
Exemplo
curl -X GET 'https://api.asender.net/v1/journeys/id_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
public-logs
GET/v1/logs
Log das requisições feitas à API com as chaves da conta (sem corpos).
Lista, do mais novo para o mais antigo, as requisições que a sua conta fez à API pública — inclusive as recusadas (400, 401 com chave da conta, 403, 429). A listagem não traz os corpos; use GET /v1/logs/{id}.
O tenant é o da API key; não há parâmetro de conta (tenant_id na query é descartado). Paginação por cursor: repita a chamada com after=<next> enquanto has_more for true.
Escopo: logs:read. Teto próprio de 5 req/s por chave, além do geral. Serviço de logs fora do ar: 503 ServiceUnavailable com Retry-After; o resto da API não é afetado.
Formato: o corpo é repassado do serviço de logs como veio, em snake_case (diferente do PascalCase das demais rotas /v1), e os erros 400/404 vêm no envelope {"error":{"type","code","message"}} (ver LogsErrorEnvelope).
Onde é usada
API pública do cliente.
Entradas
status, q, cursor, limit (allowlist). channel do cliente é IGNORADO: nesta rota o canal é do contrato, não do parâmetro.
Saídas
200 com {Messages, NextCursor, HasMore}; 422 em query malformada.
Parâmetros
nome
em
obrigatório
descrição
limit
query
não
Itens por página.
after
query
não
Cursor opaco (`next` da página anterior): devolve os registros mais antigos. Adulterado ou de outra conta → 400. Não combine com `before`.
before
query
não
Cursor opaco: devolve os mais novos. `after` e `before` juntos → 400.
status_class
query
não
Lista separada por vírgula de `2xx`, `3xx`, `4xx`, `5xx`.
status
query
não
Status HTTP exato.
method
query
não
Verbo HTTP.
path
query
não
Padrão de rota exato (não o caminho com ids).
api_key_id
query
não
Id de uma ou mais chaves da conta, separados por vírgula.
error_code
query
não
Código do envelope de erro devolvido.
idempotency_key
query
não
Chave de idempotência efetiva da requisição.
message_id
query
não
Id de mensagem devolvido pelo envio (liga o log ao envio).
mode
query
não
Modo da chave usada.
since
query
não
RFC 3339. Padrão: agora menos 24 h.
until
query
não
RFC 3339. Padrão: agora. Janela maior que 31 dias é reduzida a 31 dias antes de `until`.
Respostas
código
significado
200
OK
400
`400 ValidationError` — parâmetro inválido, cursor adulterado ou de outra
conta, `after` com `before`. Corpo repassado do serviço de logs
(`LogsErrorEnvelope`).
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 InsufficientScope` (chave sem `logs:read`) ou `403 TenantSuspended`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
`503 ServiceUnavailable` — o serviço de logs não respondeu. Repita depois
do `Retry-After`; o resto da API segue funcionando.
Exemplo
curl -X GET 'https://api.asender.net/v1/logs' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/logs/{id}
Detalhe de uma requisição, com os corpos já redigidos.
Traz request_body e response_bodydepois da redação: tokens de API, JWT, senhas, cabeçalhos de autorização, códigos de verificação (OTP), cartões e CPF não são gravados; destinatários aparecem mascarados. Corpos acima de 16 KiB são cortados (*_truncated: true). redaction_version diz qual conjunto de regras valeu quando o log foi gravado.
Id de outra conta e id inexistente respondem o mesmo 404, com o mesmo corpo. Escopo: logs:read; mesmo teto de 5 req/s.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 InsufficientScope` (chave sem `logs:read`) ou `403 TenantSuspended`.
404
`404 NotFound` — id inexistente **ou de outra conta** (corpo idêntico, sem
oráculo de existência). Corpo repassado do serviço de logs.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
`503 ServiceUnavailable` — o serviço de logs não respondeu. Repita depois
do `Retry-After`; o resto da API segue funcionando.
Exemplo
curl -X GET 'https://api.asender.net/v1/logs/id_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
public-messaging
GET/v1/messaging/push/messages
Mensagens de push enviadas pela conta (incremental).
Escopo messaging:read, produto push. Em ordem de criação crescente: use created_after (exclusivo) ou next_cursor para sincronizar. Campos: id, to, status, queued_at, created_at, title. Conta cujo canal ainda não migrou para a ferramenta dedicada → 503 (ServiceUnavailable, Retry-After). Parâmetro de query fora da lista → 400 (nada é ignorado em silêncio).
Parâmetros
nome
em
obrigatório
descrição
limit
query
não
Itens por página.
cursor
query
não
Cursor opaco (`next_cursor` da página anterior). Estável: não pula nem repete. Inválido → 400.
created_after
query
não
RFC 3339, exclusivo. Sem ele a lista começa na mensagem mais antiga.
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 UpstreamError` — a ferramenta de origem respondeu com erro; o corpo dela nunca é repassado.
503
`503 ServiceUnavailable` com `Retry-After` — a fonte dos dados está fora; o resto da API não é afetado.
Exemplo
curl -X GET 'https://api.asender.net/v1/messaging/push/messages' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/messaging/push/reports/overview
Panorama de push da conta.
Escopo messaging:read, produto push. Totais por status na janela days: {data: {...}}. Conta cujo canal ainda não migrou para a ferramenta dedicada → 503 (ServiceUnavailable, Retry-After). Parâmetro de query fora da lista → 400 (nada é ignorado em silêncio).
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 UpstreamError` — a ferramenta de origem respondeu com erro; o corpo dela nunca é repassado.
503
`503 ServiceUnavailable` com `Retry-After` — a fonte dos dados está fora; o resto da API não é afetado.
Exemplo
curl -X GET 'https://api.asender.net/v1/messaging/push/reports/overview' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/messaging/sms/messages
Mensagens de SMS enviadas pela conta (incremental).
Escopo messaging:read, produto sns. Em ordem de criação crescente: use created_after (exclusivo) ou next_cursor para sincronizar. Campos: id, to, status, queued_at, created_at, segments. Conta cujo canal ainda não migrou para a ferramenta dedicada → 503 (ServiceUnavailable, Retry-After). Parâmetro de query fora da lista → 400 (nada é ignorado em silêncio).
Parâmetros
nome
em
obrigatório
descrição
limit
query
não
Itens por página.
cursor
query
não
Cursor opaco (`next_cursor` da página anterior). Estável: não pula nem repete. Inválido → 400.
created_after
query
não
RFC 3339, exclusivo. Sem ele a lista começa na mensagem mais antiga.
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 UpstreamError` — a ferramenta de origem respondeu com erro; o corpo dela nunca é repassado.
503
`503 ServiceUnavailable` com `Retry-After` — a fonte dos dados está fora; o resto da API não é afetado.
Exemplo
curl -X GET 'https://api.asender.net/v1/messaging/sms/messages' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/messaging/sms/reports/overview
Panorama de SMS da conta.
Escopo messaging:read, produto sns. Totais por status na janela days: {data: {...}}. Conta cujo canal ainda não migrou para a ferramenta dedicada → 503 (ServiceUnavailable, Retry-After). Parâmetro de query fora da lista → 400 (nada é ignorado em silêncio).
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 UpstreamError` — a ferramenta de origem respondeu com erro; o corpo dela nunca é repassado.
503
`503 ServiceUnavailable` com `Retry-After` — a fonte dos dados está fora; o resto da API não é afetado.
Exemplo
curl -X GET 'https://api.asender.net/v1/messaging/sms/reports/overview' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/messaging/whatsapp/messages
Mensagens de WhatsApp enviadas pela conta (incremental).
Escopo messaging:read, produto zapify. Em ordem de criação crescente: use created_after (exclusivo) ou next_cursor para sincronizar. Campos: id, to, status, queued_at, created_at. Conta cujo canal ainda não migrou para a ferramenta dedicada → 503 (ServiceUnavailable, Retry-After). Parâmetro de query fora da lista → 400 (nada é ignorado em silêncio).
Parâmetros
nome
em
obrigatório
descrição
limit
query
não
Itens por página.
cursor
query
não
Cursor opaco (`next_cursor` da página anterior). Estável: não pula nem repete. Inválido → 400.
created_after
query
não
RFC 3339, exclusivo. Sem ele a lista começa na mensagem mais antiga.
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 UpstreamError` — a ferramenta de origem respondeu com erro; o corpo dela nunca é repassado.
503
`503 ServiceUnavailable` com `Retry-After` — a fonte dos dados está fora; o resto da API não é afetado.
Exemplo
curl -X GET 'https://api.asender.net/v1/messaging/whatsapp/messages' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/messaging/whatsapp/reports/overview
Panorama de WhatsApp da conta.
Escopo messaging:read, produto zapify. Totais por status na janela days: {data: {...}}. Conta cujo canal ainda não migrou para a ferramenta dedicada → 503 (ServiceUnavailable, Retry-After). Parâmetro de query fora da lista → 400 (nada é ignorado em silêncio).
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403` — credencial sem o escopo da rota (`InsufficientScope`), conta suspensa (`TenantSuspended`)
ou, para app OAuth, o usuário que autorizou o app não tem mais acesso à conta ou ao produto
(`Forbidden`). Chave `sk_*` recebe o envelope legado (`ErrorEnvelope`); app OAuth recebe `DataError`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 UpstreamError` — a ferramenta de origem respondeu com erro; o corpo dela nunca é repassado.
503
`503 ServiceUnavailable` com `Retry-After` — a fonte dos dados está fora; o resto da API não é afetado.
Exemplo
curl -X GET 'https://api.asender.net/v1/messaging/whatsapp/reports/overview' \
-H 'Authorization: Bearer SEU_TOKEN'
public-push
GET/v1/push/devices
Lista os devices de push da conta da API key.
Contraparte de leitura do registro: quem envia token precisa poder conferir o que está registrado. O tenant vem da API key, NUNCA da query — aceitar tenant_id do cliente aqui seria vazamento cross-tenant.
Onde é usada
SDK/integração do cliente e diagnóstico de entrega.
Entradas
cursor, limit.
Saídas
200 com {Devices, NextCursor, HasMore}; 422 em query malformada. O tenant vem do Principal, NUNCA da query: aceitar tenant_id do cliente aqui seria vazamento cross-tenant direto.
Parâmetros
nome
em
obrigatório
descrição
cursor
query
não
limit
query
não
Respostas
código
significado
200
Página de devices, em PascalCase.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Exemplo
curl -X GET 'https://api.asender.net/v1/push/devices' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/push/devices
Registra (upsert por token) um device de push.
Idempotente pelo par (tenant, token). Responde 200 sempre, mesmo quando o asender_mkt_api respondeu 201 no primeiro registro.
Só servidor-a-servidor. Pedido com Origin ou Sec-Fetch-* (sinal de navegador) é recusado com 403 ServerKeyInBrowser e logado com o id da chave: a API key de servidor não pode estar num site. Do navegador, use POST /v1/push/site/devices com a chave de site (M15, ADR-0057).
Onde é usada
SDK do cliente, na primeira abertura do app / renovação do token.
Saídas
200 com {Device} (upsert: o mesmo token duas vezes não cria dois registros); 400 em validação local; 422 quando o upstream recusa a plataforma.
Efeitos
escrita no serviço de mensageria. # Chave de servidor vinda do navegador é recusada (M15) O snippet antigo mandava colar a API key de SERVIDOR no site. Pedido com Origin/Sec-Fetch-* aqui significa exatamente isso: a chave está num JavaScript público. Recusa com 403 ServerKeyInBrowser e loga em Warn com o id da chave, para o dono ser avisado e rotacioná-la. Sem janela de compatibilidade: esta rota nunca respondeu CORS, então nenhum site conseguiu usá-la do navegador com sucesso — não há base instalada a preservar (ADR 0057). O caminho do navegador é POST /v1/push/site/devices, com a chave de site.
Respostas
código
significado
200
Device registrado ou reativado.
400
`400 ValidationError` com `Error.Details` mapeando campo → motivo.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`TenantSuspended` (tenant não-`active`) ou `ServerKeyInBrowser`
(API key de servidor usada a partir de navegador).
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
Enfileira um push para aparelhos registrados (DeviceTokens).
O destino é sempre uma lista de DeviceTokens de aparelhos registrados em POST /v1/push/devices (pelo menos um). Envio por tópico não é suportado: o campo Topic é recusado com 400 ValidationError (Error.Details.Topic), junto ou sem DeviceTokens. Sem tokens, 400 com Details.DeviceTokens.
202 quer dizer enfileirado, não entregue. Se nenhum dos tokens é um aparelho ativo da conta (desregistrado ou descadastrado), a resposta é 422 ValidationError e nada é cobrado.
Onde é usada
API pública do cliente (autenticada por API key).
Saídas
202 com {MessageId, MessageIds, Status, ReusedIdempotency}; 400 em validação local; 422 quando o asender_mkt_api recusa; 502 se ele está fora.
Efeitos
escrita no serviço de mensageria. Idempotente por IdempotencyKey.
Parâmetros
nome
em
obrigatório
descrição
Idempotency-Key
header
não
Chave de idempotência do envio (1 a 128 caracteres de `[A-Za-z0-9._:-]`;
uma chave malformada é recusada com `422`, nunca ignorada). **Tem
precedência** sobre o campo `IdempotencyKey` do corpo.
- **Mesma chave + mesmo corpo** = replay: devolve `202` com os ids da
mensagem ORIGINAL e `ReusedIdempotency: true`; nada é enviado nem
cobrado de novo.
- **Mesma chave + corpo DIFERENTE** = **`409 IdempotencyConflict`**: o
texto novo NÃO é enviado e a mensagem original não muda. Use uma chave
nova para uma mensagem nova.
O "mesmo corpo" ignora a ordem dos campos e o próprio campo
`IdempotencyKey`. Em `/v1/emails/batch` a chave vale por item
(`<chave>:<índice>`). A checagem de corpo vale hoje para SMS, push e
WhatsApp; no e-mail, até o canal migrar para a ferramenta dedicada, o
replay devolve o `202` original sem comparar o corpo.
**Aceite assíncrono e replay (resiliência).** Se o serviço de mensageria
estiver indisponível (timeout, conexão recusada ou 5xx do gateway), o
envio NÃO falha: a API persiste o pedido em disco e responde **`202` com
`Status: accepted`**, `MessageIds: []` e `IdempotencyKey` — a chave
EFETIVA, também devolvida no header de resposta `Idempotency-Key`. Se o
cliente não mandou chave, a API gera uma (`bffauto:…`) ANTES da primeira
tentativa. O pedido é reenviado ao destino, em ordem, com **essa mesma
chave** quando ele volta; por isso o replay nunca duplica o envio, nem
quando a primeira tentativa chegou ao destino e só a resposta se perdeu.
Para obter os ids depois, reenvie o MESMO `POST` com a mesma
`Idempotency-Key`: a resposta traz os ids originais e
`ReusedIdempotency: true`. Erros de validação (`4xx`) continuam
síncronos e nunca entram na fila; um pedido aceito que o destino recusar
de vez no replay (4xx) fica retido em arquivo de mortos para ação
manual, com alerta.
Respostas
código
significado
202
Aceito e persistido; a entrega é assíncrona. Com o serviço dono indisponível, `Status: accepted` (spool durável, replay com a mesma `Idempotency-Key`).
400
`400 ValidationError` com `Error.Details` mapeando campo → motivo.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
409
`409 IdempotencyConflict` — a `Idempotency-Key` já foi usada com um corpo
DIFERENTE. Nada foi enviado. Use uma chave nova, ou reenvie o corpo
original para obter o replay (`ReusedIdempotency: true`).
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Registra a inscrição de Web Push do visitante, a partir do navegador.
Rota do snippet de push colado no site do cliente (M15, ADR-0057). Autenticada pela chave PÚBLICA de site (?site_key=pk_push_…, emitida por GET /api/push/site-config), nunca por API key de servidor — que é recusada aqui (401), assim como a chave de site é recusada em toda outra rota. Exige Origin declarado na chave (403 OriginNotAllowed, sem CORS). O OPTIONS (preflight) faz a mesma checagem de chave e origem e responde 204 com CORS da origem aprovada. Teto por (chave, IP) e por chave (PUSH_SITE_RATE_LIMIT_*). O tenant vem da chave assinada e é conferido active no core a cada registro. Upsert por endpoint no asender_mkt_api; a resposta não devolve o registro interno.
Onde é usada
snippet de push colado no site do cliente (asenderSubscribePush).
Saídas
200 {Status: "registered"}; 400 validação; 401 principal ausente ou tenant inexistente; 403 tenant suspenso; 5xx de upstream mapeado.
Efeitos
escrita em messages.push_devices. A resposta NÃO devolve o device: quem chama é um visitante anônimo, e o registro interno (ids, contato, tenant) não é assunto dele.
Parâmetros
nome
em
obrigatório
descrição
site_key
query
sim
Origin
header
sim
Respostas
código
significado
200
Inscrição registrada ou reativada.
400
`400 ValidationError` com `Error.Details` mapeando campo → motivo.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`OriginNotAllowed` (origem ausente/não declarada) ou `TenantSuspended`.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
202 com {MessageId, MessageIds, Status, ReusedIdempotency, Suppressed?}; 400 em validação local; 422 quando o asender_mkt_api recusa — com o código RemetenteNaoVerificado quando o Source não é de domínio verificado da conta (2026-10-02: antes o e-mail saía com o remetente da plataforma); 503 VerificacaoIndisponivel quando a verificação está fora; 502 se ele está fora.
Efeitos
escrita no serviço de mensageria. Idempotente por IdempotencyKey.
Parâmetros
nome
em
obrigatório
descrição
Idempotency-Key
header
não
Chave de idempotência do envio (1 a 128 caracteres de `[A-Za-z0-9._:-]`;
uma chave malformada é recusada com `422`, nunca ignorada). **Tem
precedência** sobre o campo `IdempotencyKey` do corpo.
- **Mesma chave + mesmo corpo** = replay: devolve `202` com os ids da
mensagem ORIGINAL e `ReusedIdempotency: true`; nada é enviado nem
cobrado de novo.
- **Mesma chave + corpo DIFERENTE** = **`409 IdempotencyConflict`**: o
texto novo NÃO é enviado e a mensagem original não muda. Use uma chave
nova para uma mensagem nova.
O "mesmo corpo" ignora a ordem dos campos e o próprio campo
`IdempotencyKey`. Em `/v1/emails/batch` a chave vale por item
(`<chave>:<índice>`). A checagem de corpo vale hoje para SMS, push e
WhatsApp; no e-mail, até o canal migrar para a ferramenta dedicada, o
replay devolve o `202` original sem comparar o corpo.
**Aceite assíncrono e replay (resiliência).** Se o serviço de mensageria
estiver indisponível (timeout, conexão recusada ou 5xx do gateway), o
envio NÃO falha: a API persiste o pedido em disco e responde **`202` com
`Status: accepted`**, `MessageIds: []` e `IdempotencyKey` — a chave
EFETIVA, também devolvida no header de resposta `Idempotency-Key`. Se o
cliente não mandou chave, a API gera uma (`bffauto:…`) ANTES da primeira
tentativa. O pedido é reenviado ao destino, em ordem, com **essa mesma
chave** quando ele volta; por isso o replay nunca duplica o envio, nem
quando a primeira tentativa chegou ao destino e só a resposta se perdeu.
Para obter os ids depois, reenvie o MESMO `POST` com a mesma
`Idempotency-Key`: a resposta traz os ids originais e
`ReusedIdempotency: true`. Erros de validação (`4xx`) continuam
síncronos e nunca entram na fila; um pedido aceito que o destino recusar
de vez no replay (4xx) fica retido em arquivo de mortos para ação
manual, com alerta.
Respostas
código
significado
202
Aceito e persistido; a entrega é assíncrona. Com o serviço dono indisponível, `Status: accepted` (spool durável, replay com a mesma `Idempotency-Key`).
400
`400 ValidationError` com `Error.Details` mapeando campo → motivo.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
409
`409 IdempotencyConflict` — a `Idempotency-Key` já foi usada com um corpo
DIFERENTE. Nada foi enviado. Use uma chave nova, ou reenvie o corpo
original para obter o replay (`ReusedIdempotency: true`).
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Envia uma mensagem de texto por Telegram, pelo bot da conta.
202 quer dizer enfileirado, não entregue. A mensagem sai pelo bot que a conta conectou no Telegram; BotId (o id público tgb_..., nunca o token do bot) só é necessário quando a conta tem mais de um bot.
O destinatário precisa ter iniciado o bot. O Telegram só deixa o bot escrever para quem o abriu e enviou /start. Para um ChatId que nunca fez isso a resposta é 422 ChatNaoIniciado e nada é enviado: peça ao destinatário que abra o bot e envie /start. Quem bloqueou o bot depois também deixa de receber.
ChatId é o id numérico do chat (positivo para pessoas, negativo para grupos), como texto ou como inteiro. Use Idempotency-Key para repetir o pedido com segurança: a mesma chave com o mesmo corpo não duplica a mensagem; com outro corpo devolve 409.
Chave de servidor nunca vai para o navegador: pedido com Origin / Sec-Fetch-* leva 403 ServerKeyInBrowser. Com o canal fora do ar a resposta é 503 ServiceUnavailable com Retry-After; repita depois.
Onde é usada
API pública do cliente (autenticada por API key).
Saídas
202 com {MessageId, MessageIds, Status, ReusedIdempotency, Suppressed?}; 400 em validação local; 422 quando o asender_mkt_api recusa — com o código RemetenteNaoVerificado quando o Source não é de domínio verificado da conta (2026-10-02: antes o e-mail saía com o remetente da plataforma); 503 VerificacaoIndisponivel quando a verificação está fora; 502 se ele está fora.
Efeitos
escrita no serviço de mensageria. Idempotente por IdempotencyKey.
Parâmetros
nome
em
obrigatório
descrição
Idempotency-Key
header
não
Chave de idempotência do envio (1 a 128 caracteres de `[A-Za-z0-9._:-]`;
uma chave malformada é recusada com `422`, nunca ignorada). **Tem
precedência** sobre o campo `IdempotencyKey` do corpo.
- **Mesma chave + mesmo corpo** = replay: devolve `202` com os ids da
mensagem ORIGINAL e `ReusedIdempotency: true`; nada é enviado nem
cobrado de novo.
- **Mesma chave + corpo DIFERENTE** = **`409 IdempotencyConflict`**: o
texto novo NÃO é enviado e a mensagem original não muda. Use uma chave
nova para uma mensagem nova.
O "mesmo corpo" ignora a ordem dos campos e o próprio campo
`IdempotencyKey`. Em `/v1/emails/batch` a chave vale por item
(`<chave>:<índice>`). A checagem de corpo vale hoje para SMS, push e
WhatsApp; no e-mail, até o canal migrar para a ferramenta dedicada, o
replay devolve o `202` original sem comparar o corpo.
**Aceite assíncrono e replay (resiliência).** Se o serviço de mensageria
estiver indisponível (timeout, conexão recusada ou 5xx do gateway), o
envio NÃO falha: a API persiste o pedido em disco e responde **`202` com
`Status: accepted`**, `MessageIds: []` e `IdempotencyKey` — a chave
EFETIVA, também devolvida no header de resposta `Idempotency-Key`. Se o
cliente não mandou chave, a API gera uma (`bffauto:…`) ANTES da primeira
tentativa. O pedido é reenviado ao destino, em ordem, com **essa mesma
chave** quando ele volta; por isso o replay nunca duplica o envio, nem
quando a primeira tentativa chegou ao destino e só a resposta se perdeu.
Para obter os ids depois, reenvie o MESMO `POST` com a mesma
`Idempotency-Key`: a resposta traz os ids originais e
`ReusedIdempotency: true`. Erros de validação (`4xx`) continuam
síncronos e nunca entram na fila; um pedido aceito que o destino recusar
de vez no replay (4xx) fica retido em arquivo de mortos para ação
manual, com alerta.
Respostas
código
significado
202
Aceito e enfileirado; a entrega é assíncrona.
400
`400 ValidationError` com `Error.Details` mapeando campo → motivo.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`InsufficientScope` (a chave não tem `telegram:send`), `TenantSuspended` ou `ServerKeyInBrowser`.
404
`NotFound`: o `BotId` não existe nesta conta.
409
`409 IdempotencyConflict` — a `Idempotency-Key` já foi usada com um corpo
DIFERENTE. Nada foi enviado. Use uma chave nova, ou reenvie o corpo
original para obter o replay (`ReusedIdempotency: true`).
422
`ChatNaoIniciado`: esse chat nunca enviou `/start` ao bot (ou `ValidationError` na chave `Idempotency-Key`). Nada é enviado.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
`ServiceUnavailable`: o canal de Telegram está fora do ar ou não está configurado. Traz `Retry-After`; repita depois.
Envia uma mensagem de texto pela conta do Telegram (não por bot).
202 quer dizer enfileirado, não entregue. A mensagem sai pela conta de atendimento que a conta Asender conectou no Telegram; SessionId (o id público tgc_...) só é necessário quando há mais de uma conta conectada.
To aceita @username, telefone ou id de peer. A conta só escreve para diálogos que já existem: para um destino sem conversa a resposta é 422 DialogoInexistente e nada é enviado. Envio em massa pode levar o Telegram a restringir a conta; use com moderação.
Use Idempotency-Key para repetir o pedido com segurança: a mesma chave com o mesmo corpo não duplica a mensagem; com outro corpo devolve 409. Chave de servidor nunca vai para o navegador: pedido com Origin / Sec-Fetch-* leva 403 ServerKeyInBrowser.
Onde é usada
API pública do cliente (autenticada por API key).
Saídas
202 com {MessageId, MessageIds, Status, ReusedIdempotency, Suppressed?}; 400 em validação local; 422 quando o asender_mkt_api recusa — com o código RemetenteNaoVerificado quando o Source não é de domínio verificado da conta (2026-10-02: antes o e-mail saía com o remetente da plataforma); 503 VerificacaoIndisponivel quando a verificação está fora; 502 se ele está fora.
Efeitos
escrita no serviço de mensageria. Idempotente por IdempotencyKey.
Parâmetros
nome
em
obrigatório
descrição
Idempotency-Key
header
não
Chave de idempotência do envio (1 a 128 caracteres de `[A-Za-z0-9._:-]`;
uma chave malformada é recusada com `422`, nunca ignorada). **Tem
precedência** sobre o campo `IdempotencyKey` do corpo.
- **Mesma chave + mesmo corpo** = replay: devolve `202` com os ids da
mensagem ORIGINAL e `ReusedIdempotency: true`; nada é enviado nem
cobrado de novo.
- **Mesma chave + corpo DIFERENTE** = **`409 IdempotencyConflict`**: o
texto novo NÃO é enviado e a mensagem original não muda. Use uma chave
nova para uma mensagem nova.
O "mesmo corpo" ignora a ordem dos campos e o próprio campo
`IdempotencyKey`. Em `/v1/emails/batch` a chave vale por item
(`<chave>:<índice>`). A checagem de corpo vale hoje para SMS, push e
WhatsApp; no e-mail, até o canal migrar para a ferramenta dedicada, o
replay devolve o `202` original sem comparar o corpo.
**Aceite assíncrono e replay (resiliência).** Se o serviço de mensageria
estiver indisponível (timeout, conexão recusada ou 5xx do gateway), o
envio NÃO falha: a API persiste o pedido em disco e responde **`202` com
`Status: accepted`**, `MessageIds: []` e `IdempotencyKey` — a chave
EFETIVA, também devolvida no header de resposta `Idempotency-Key`. Se o
cliente não mandou chave, a API gera uma (`bffauto:…`) ANTES da primeira
tentativa. O pedido é reenviado ao destino, em ordem, com **essa mesma
chave** quando ele volta; por isso o replay nunca duplica o envio, nem
quando a primeira tentativa chegou ao destino e só a resposta se perdeu.
Para obter os ids depois, reenvie o MESMO `POST` com a mesma
`Idempotency-Key`: a resposta traz os ids originais e
`ReusedIdempotency: true`. Erros de validação (`4xx`) continuam
síncronos e nunca entram na fila; um pedido aceito que o destino recusar
de vez no replay (4xx) fica retido em arquivo de mortos para ação
manual, com alerta.
Respostas
código
significado
202
Aceito e enfileirado; a entrega é assíncrona.
400
`400 ValidationError` com `Error.Details` mapeando campo → motivo.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`InsufficientScope` (a chave não tem `telegramconta:send`), `TenantSuspended` ou `ServerKeyInBrowser`.
404
`NotFound`: o `SessionId` não existe nesta conta.
409
`409 IdempotencyConflict` — a `Idempotency-Key` já foi usada com um corpo
DIFERENTE. Nada foi enviado. Use uma chave nova, ou reenvie o corpo
original para obter o replay (`ReusedIdempotency: true`).
422
`DialogoInexistente`: a conta não tem conversa com esse destino. Nada é enviado. Também `ValidationError` na chave `Idempotency-Key`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
`ServiceUnavailable`: o canal está fora do ar ou não está configurado. Traz `Retry-After`; repita depois.
Lista os endpoints de webhook da conta da API key.
Escopo webhooks:manage (só chave; token OAuth recebe 403). O tenant vem da chave, nunca da query. O segredo de assinatura não é devolvido aqui.
Onde é usada
API pública do cliente.
Entradas
status, q, cursor, limit (allowlist). channel do cliente é IGNORADO: nesta rota o canal é do contrato, não do parâmetro.
Saídas
200 com {Messages, NextCursor, HasMore}; 422 em query malformada.
Respostas
código
significado
200
Endpoints da conta, em PascalCase.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/v1/webhooks' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/webhooks
Cadastra um endpoint de webhook.
Escopo webhooks:manage (só chave). A resposta traz o Secret (whsec_…) uma única vez (Cache-Control: no-store): guarde-o agora, ele nunca mais é exibido — perdeu, apague o endpoint e crie outro. O segredo é sempre gerado pelo Asender (não aceita o seu). Com ele você confere a assinatura das entregas (formato Asender Webhook v1, ADR-0133: Asender-Signature: t=<unix>,v1=<hex>, HMAC-SHA256 do segredo sobre "<t>.<corpo cru>").
Validação.Url absoluta, https em produção (http só fora de produção), até 2048 caracteres, sem credencial nem fragmento e sem destino interno (loopback, faixa privada, link-local/metadados de nuvem, localhost, .local, .internal) — o destino é conferido de novo, no endereço resolvido, a cada entrega (anti-SSRF). Events é uma lista de 1 a 32 itens sem repetição, cada um * (tudo), <família>.* (email, sms, whatsapp, push, mail) ou um tipo exato do catálogo entregável: email.queued, email.failed, sms.sent, sms.failed, whatsapp.sent, whatsapp.failed, push.delivered, push.failed, mail.sent, mail.received. Eventos internos (interno.*) nunca são entregues.
Sem Idempotency-Key. Esta rota não guarda estado de idempotência: repetir o POST cria outro endpoint. Em caso de dúvida, liste antes (GET /v1/webhooks).
Onde é usada
fluxo de cadastro (AuthHandler.Register faz o equivalente internamente) e recuperação manual quando o provisionamento automático falhou.
Entradas
{Name, Slug} — allowlist explícita, ver tenantCreateInput.
Saídas
201 com {Tenant:{Id,...}} em PascalCase; 400 JSON inválido OU campo fora da allowlist; 422 nome vazio; o status 4xx do core preservado; 502 quando o core está fora.
Efeitos
escreve no asender-core. O owner_user_id vem SEMPRE do principal verificado, nunca do corpo.
Respostas
código
significado
201
Endpoint criado; o `Secret` só aparece nesta resposta.
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
413
`413 PayloadTooLarge` — corpo acima de 2 MiB.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Escopo webhooks:manage (só chave). Inexistente ou de outra conta: 404 idêntico. Sem o segredo.
Respostas
código
significado
200
O endpoint.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/v1/webhooks/id_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
PATCH/v1/webhooks/{id}
Altera URL, eventos e/ou estado de um endpoint.
Escopo webhooks:manage (só chave). Envie ao menos um campo; as mesmas regras do POST valem para o que for enviado. Não rotaciona o segredo. Active: false pausa as entregas.
Onde é usada
API pública, escopo webhooks:manage.
Respostas
código
significado
200
O endpoint atualizado.
400
`400 ValidationError` — JSON inválido, campo desconhecido (a mensagem nomeia o campo: `unknown field "X"`) ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Envia um evento `webhook.test` assinado a UM endpoint.
Escopo webhooks:manage (só chave). Faz umPOST síncrono, sem retentativa, só para o endpoint pedido (o evento webhook.test não é assinável e não vai para os outros endpoints da conta). Leva os cabeçalhos atuais (X-Asender-*) e os do formato v1 (Asender-Webhook-Id, Asender-Event, Asender-Signature). O corpo da resposta do seu servidor não é devolvido.
Responde 200 sempre que o endpoint existe; o resultado da entrega está no corpo: Delivered, StatusCode (0 se não houve resposta) e Reason — "" (entregue), non_2xx, transport_error, destination_blocked (destino interno) ou non_production_blocked (fora de produção só domínios de teste, p.ex. *.test.asender.com.br/.test/.example; nada chega a terceiros).
Onde é usada
API pública, escopo webhooks:manage.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Id público do endpoint (`whk_…`).
Respostas
código
significado
200
Resultado do envio de teste.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Enfileira uma mensagem de WhatsApp (texto livre ou template).
Mesmo padrão do SMS: 202 quer dizer enfileirado, não entregue. O opt-out do canal (destinatário que saiu do WhatsApp), a quota do plano e o limite de vazão são aplicados no enfileiramento; quem saiu do canal fica de fora e entra na contagem Suppressed (se ninguém sobra, 422).
Janela de 24h da Meta.Body (texto livre) só é entregue se o destinatário escreveu para o seu número nas últimas 24h. Fora da janela a Meta recusa e a mensagem termina failed (OutsideWindow) — isso é assíncrono, o 202 já foi dado. Para abrir conversa use Template: o slug de um template aprovado na Meta e registrado na conta (Admin → WhatsApp → Templates), com Variables posicionais "1".."N" — exatamente o número que o template exige, senão 422. Mande BodyouTemplate, nunca os dois.
From é o phone_number_id numérico do número da conta; omitido, vale o número padrão. Sem credencial do WhatsApp configurada na conta, o envio não sai (a mensagem fica failed).
Chave de servidor nunca vai para o navegador: pedido com Origin / Sec-Fetch-* leva 403 ServerKeyInBrowser.
Onde é usada
API pública do cliente (autenticada por API key).
Saídas
202 com {MessageId, MessageIds, Status, ReusedIdempotency, Suppressed?}; 400 em validação local; 422 quando o asender_mkt_api recusa — com o código RemetenteNaoVerificado quando o Source não é de domínio verificado da conta (2026-10-02: antes o e-mail saía com o remetente da plataforma); 503 VerificacaoIndisponivel quando a verificação está fora; 502 se ele está fora.
Efeitos
escrita no serviço de mensageria. Idempotente por IdempotencyKey.
Parâmetros
nome
em
obrigatório
descrição
Idempotency-Key
header
não
Chave de idempotência do envio (1 a 128 caracteres de `[A-Za-z0-9._:-]`;
uma chave malformada é recusada com `422`, nunca ignorada). **Tem
precedência** sobre o campo `IdempotencyKey` do corpo.
- **Mesma chave + mesmo corpo** = replay: devolve `202` com os ids da
mensagem ORIGINAL e `ReusedIdempotency: true`; nada é enviado nem
cobrado de novo.
- **Mesma chave + corpo DIFERENTE** = **`409 IdempotencyConflict`**: o
texto novo NÃO é enviado e a mensagem original não muda. Use uma chave
nova para uma mensagem nova.
O "mesmo corpo" ignora a ordem dos campos e o próprio campo
`IdempotencyKey`. Em `/v1/emails/batch` a chave vale por item
(`<chave>:<índice>`). A checagem de corpo vale hoje para SMS, push e
WhatsApp; no e-mail, até o canal migrar para a ferramenta dedicada, o
replay devolve o `202` original sem comparar o corpo.
**Aceite assíncrono e replay (resiliência).** Se o serviço de mensageria
estiver indisponível (timeout, conexão recusada ou 5xx do gateway), o
envio NÃO falha: a API persiste o pedido em disco e responde **`202` com
`Status: accepted`**, `MessageIds: []` e `IdempotencyKey` — a chave
EFETIVA, também devolvida no header de resposta `Idempotency-Key`. Se o
cliente não mandou chave, a API gera uma (`bffauto:…`) ANTES da primeira
tentativa. O pedido é reenviado ao destino, em ordem, com **essa mesma
chave** quando ele volta; por isso o replay nunca duplica o envio, nem
quando a primeira tentativa chegou ao destino e só a resposta se perdeu.
Para obter os ids depois, reenvie o MESMO `POST` com a mesma
`Idempotency-Key`: a resposta traz os ids originais e
`ReusedIdempotency: true`. Erros de validação (`4xx`) continuam
síncronos e nunca entram na fila; um pedido aceito que o destino recusar
de vez no replay (4xx) fica retido em arquivo de mortos para ação
manual, com alerta.
Respostas
código
significado
202
Aceito e enfileirado; a entrega é assíncrona.
400
`400 ValidationError` com `Error.Details` mapeando campo → motivo.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`TenantSuspended` (conta não ativa) ou `ServerKeyInBrowser` (chave de servidor usada de um navegador).
409
`409 IdempotencyConflict` — a `Idempotency-Key` já foi usada com um corpo
DIFERENTE. Nada foi enviado. Use uma chave nova, ou reenvie o corpo
original para obter o replay (`ReusedIdempotency: true`).
422
`ValidationError`: template inexistente na conta, variáveis em número errado, ou todos os destinatários fora do canal (opt-out). Nada é enfileirado nem cobrado.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Envia uma mensagem de texto pelo WhatsApp da conta, via sessão do WhatsApp Web (canal não oficial).
Risco do canal não oficial: this channel uses an unofficial WhatsApp Web session; the number may be banned by WhatsApp. Use apenas com um número que a conta aceita arriscar, e prefira POST /v1/whatsapp/send (canal oficial) sempre que possível.
202 quer dizer enfileirado, não entregue. A mensagem sai pelo número pareado pela conta; SessionId (o id público wws_...) só é necessário quando a conta tem mais de uma sessão.
Só responde a conversas que já existem. O canal escreve para quem já falou com este número. Para um número que nunca escreveu a resposta é 422 ChatInexistente e nada é enviado, a menos que o gestor da conta habilite novos contatos por dia. To é o telefone em E.164 (+5511987654321).
Use Idempotency-Key para repetir o pedido com segurança: a mesma chave com o mesmo corpo não duplica a mensagem; com outro corpo devolve 409. Chave de servidor nunca vai para o navegador: pedido com Origin / Sec-Fetch-* leva 403 ServerKeyInBrowser.
Onde é usada
API pública do cliente (autenticada por API key).
Saídas
202 com {MessageId, MessageIds, Status, ReusedIdempotency, Suppressed?}; 400 em validação local; 422 quando o asender_mkt_api recusa — com o código RemetenteNaoVerificado quando o Source não é de domínio verificado da conta (2026-10-02: antes o e-mail saía com o remetente da plataforma); 503 VerificacaoIndisponivel quando a verificação está fora; 502 se ele está fora.
Efeitos
escrita no serviço de mensageria. Idempotente por IdempotencyKey.
Parâmetros
nome
em
obrigatório
descrição
Idempotency-Key
header
não
Chave de idempotência do envio (1 a 128 caracteres de `[A-Za-z0-9._:-]`;
uma chave malformada é recusada com `422`, nunca ignorada). **Tem
precedência** sobre o campo `IdempotencyKey` do corpo.
- **Mesma chave + mesmo corpo** = replay: devolve `202` com os ids da
mensagem ORIGINAL e `ReusedIdempotency: true`; nada é enviado nem
cobrado de novo.
- **Mesma chave + corpo DIFERENTE** = **`409 IdempotencyConflict`**: o
texto novo NÃO é enviado e a mensagem original não muda. Use uma chave
nova para uma mensagem nova.
O "mesmo corpo" ignora a ordem dos campos e o próprio campo
`IdempotencyKey`. Em `/v1/emails/batch` a chave vale por item
(`<chave>:<índice>`). A checagem de corpo vale hoje para SMS, push e
WhatsApp; no e-mail, até o canal migrar para a ferramenta dedicada, o
replay devolve o `202` original sem comparar o corpo.
**Aceite assíncrono e replay (resiliência).** Se o serviço de mensageria
estiver indisponível (timeout, conexão recusada ou 5xx do gateway), o
envio NÃO falha: a API persiste o pedido em disco e responde **`202` com
`Status: accepted`**, `MessageIds: []` e `IdempotencyKey` — a chave
EFETIVA, também devolvida no header de resposta `Idempotency-Key`. Se o
cliente não mandou chave, a API gera uma (`bffauto:…`) ANTES da primeira
tentativa. O pedido é reenviado ao destino, em ordem, com **essa mesma
chave** quando ele volta; por isso o replay nunca duplica o envio, nem
quando a primeira tentativa chegou ao destino e só a resposta se perdeu.
Para obter os ids depois, reenvie o MESMO `POST` com a mesma
`Idempotency-Key`: a resposta traz os ids originais e
`ReusedIdempotency: true`. Erros de validação (`4xx`) continuam
síncronos e nunca entram na fila; um pedido aceito que o destino recusar
de vez no replay (4xx) fica retido em arquivo de mortos para ação
manual, com alerta.
Respostas
código
significado
202
Aceito e enfileirado; a entrega é assíncrona.
400
`400 ValidationError` com `Error.Details` mapeando campo → motivo.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada.
403
`InsufficientScope` (a chave não tem `whatsweb:send`), `TenantSuspended` ou `ServerKeyInBrowser`.
404
`NotFound`: o `SessionId` não existe nesta conta.
409
`409 IdempotencyConflict` — a `Idempotency-Key` já foi usada com um corpo
DIFERENTE. Nada foi enviado. Use uma chave nova, ou reenvie o corpo
original para obter o replay (`ReusedIdempotency: true`).
422
`ChatInexistente`: esse número nunca escreveu para este WhatsApp. Nada é enviado. Também `ValidationError` na chave `Idempotency-Key`.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
503
`ServiceUnavailable`: o canal está fora do ar ou não está configurado. Traz `Retry-After`; repita depois.