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
A superfície pública de máquina. Envie a API key da conta em Authorization:
O tenant_id vem da chave. 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.
/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
As chaves são gerenciadas pela própria Platform API, na superfície de sessã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"] }'
# `scopes` é obrigatório (veja a tabela de escopos); sem ele a emissão responde 400.
# 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 de API key é limitada por principal (a chave), por janela fixa. O teto vem do plano da conta (ou do default do serviço). Toda resposta traz o estado do balde:
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
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
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)
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.
Superfície da API
/v1/* — pública (API key)
Mensageria — POST /v1/emails/send e /v1/emails/batch (payload estilo SES: Source, Destination.ToAddresses, Message.Subject/Body), POST /v1/sms/send, POST /v1/whatsapp/send (texto livre ou template), POST /v1/push/send, registro/lista de dispositivos push.
Remetentes — GET /v1/senders (domínios e caixas que a chave pode usar como from).
Conta — GET /v1/account, GET /v1/account/usage (consumo do mês, enviados, quota).
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.
escopo
libera
email:send
POST /v1/emails/send, POST /v1/emails/batch, GET /v1/senders
email:read
GET /v1/emails, GET /v1/emails/{id}
sms:send
POST /v1/sms/send
push:send
POST /v1/push/send
push:devices
GET/POST /v1/push/devices
whatsapp:send
POST /v1/whatsapp/send
account:read
GET /v1/account/usage
logs:read
GET /v1/logs, GET /v1/logs/{id} (log das requisições da própria conta)
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)
forms:read
ler formulários, em api.pages.asender.net (não são rotas de api.asender.net)
forms:write
criar, editar e apagar formulários e enviar leads (submissões), em api.pages.asender.net
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)
pages:write
criar e editar páginas (versões, rascunho, SEO, importar, duplicar, mudar de domínio, mídia), em api.pages.asender.net
pages:publish
publicar e despublicar páginas (ação visível ao público; escopo separado do pages:write), em api.pages.asender.net
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.
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.
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 e push
POST /v1/sms/send (To em E.164, Body) e POST /v1/push/send respondem 202 com enfileirado, não entregue.
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: sms:send, push:send (e push:devices para o registro).
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.
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 e WhatsApp; 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.
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.
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-me
GET/api/me
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.
Onde é usada
tela de audiência do dashboard.
Entradas
q, list_id, 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
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.
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'
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}; 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.
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: POST /api/lists, POST /api/lists/{id}/members e GET /api/lists/{id}/contacts seguem exigindo marketing.
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 o produto marketing (a LEITURA em GET /api/lists aceita também as ferramentas de despacho; 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.
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'
POST/api/lists/{id}/members
Anexa contatos a uma lista.
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/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 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.
Onde é usada
o formulário "acrescentar relay" no backoffice.
Efeitos
uma escrita no core, registrada no log com o ator (nunca o segredo).
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.
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'
PATCH/api/tenants/{id}/parent
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.
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).
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, mais o contexto da própria chave.
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'
public-email
GET/v1/emails
Histórico de emails do tenant da API key.
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
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.
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.
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.
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 — 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.
Respostas
código
significado
202
Aceito e persistido; a entrega é assíncrona.
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 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…).
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-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.
Onde é usada
superfície pública /v1, escopo logs:read.
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-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.
Respostas
código
significado
202
Aceito e persistido; a entrega é assíncrona.
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`.
corpo PascalCase {From, To[], Body, IdempotencyKey, Tags}.
Saídas
202 com {MessageId, MessageIds, Status, ReusedIdempotency}; 400 em validação local; 401 sem tenant; 422 quando o asender_mkt_api recusa; 502 se ele está fora.
Efeitos
escrita no serviço de mensageria. Idempotente por IdempotencyKey — que agora é de fato honrado, porque quem grava é o asender_messages.
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.
Respostas
código
significado
202
Aceito e persistido; a entrega é assíncrona.
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.
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}; 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.
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.