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 (user_id, código): o user_id sozinho não abre nada, 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.
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.
Corpo aceito em snake_case ({"email","password"}) porque é o que o asender-auth define e o proxy repassa; a RESPOSTA é PascalCase.
Com 2FA habilitado a resposta é 200 com Token:"" e TwoFactorRequired:true — explícito, para o chamador não confundir com upstream quebrado.
Respostas
código
significado
200
Sessão criada, ou 2FA pendente.
400
`400 ValidationError` — JSON inválido, campo desconhecido ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
Conta bloqueada ou desabilitada.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Revoga a sessão do Bearer apresentado. IDEMPOTENTE.
Revoga no asender-auth; 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), 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 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, é 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. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
TenantId vem do asender-auth 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. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
Exemplo
curl -X GET 'https://api.asender.net/api/auth/me' \
-H 'Authorization: Bearer SEU_TOKEN'
PATCH/api/auth/me
Altera o perfil do usuário da sessão.
Onde é usada
tela de perfil do painel.
Efeitos
escreve no asender-auth; e-mail novo desverifica a conta e dispara a verificação do endereço novo (lá).
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á.
A criação do tenant é best-effort: se falhar, a resposta ainda é 201 com Tenant: null e um campo Note dizendo para repetir via POST /api/tenants. Falha silenciosa não é opção.
Respostas
código
significado
201
Usuário criado.
400
`400 ValidationError` — JSON inválido, campo desconhecido ou tipo errado.
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 impersonação NÃO passa por aqui A sessão de suporte é somente leitura (ADR-0017), e o middleware de impersonação 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.
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. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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 ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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.
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 ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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.
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. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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.
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 ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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.
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 ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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. Fluxo do dado: cookie de sessão → SessionAuth → Principal → tenant resolvido no core → POST /v1/messages no asender_messages → outbox → NATS → worker.
Saídas
202 com {Messages, ReusedIdempotency}; 422 quando o asender_messages 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 ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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 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.
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. Fluxo do dado: messages.push_devices → asender_messages → aqui → seletor.
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. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
422
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>.
Onde é usada
tela de push do dashboard (botão de copiar/baixar cada arquivo). Fluxo do dado: core.vapid_keys → aqui → tela → arquivos no site do cliente → browser → POST /v1/push/devices → messages.push_devices.
Saídas
200 com {PublicKey, ApiBase, Manifest, ServiceWorker, Snippet}.
Efeitos
pode criar o par VAPID do tenant na primeira chamada (no core). ## 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).
Respostas
código
significado
200
Artefatos prontos para o site.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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 ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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.
o console, ao abrir — é o que decide entre mostrar o console e mostrar "não disponível".
Efeitos
uma leitura na allowlist. Existe em vez de o console deduzir de outra rota: sem ela, o front descobriria que não é plataforma pelo 404 da PRIMEIRA rota que chamasse — e mostraria um erro de carregamento onde a resposta certa é "esta área não é sua".
Respostas
código
significado
200
OK
401
credencial ausente ou inválida
Exemplo
curl -X GET 'https://api.asender.net/api/admin/me' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/admin/metrics
Métricas agregadas da plataforma.
Respostas
código
significado
200
OK
401
credencial ausente ou inválida
Exemplo
curl -X GET 'https://api.asender.net/api/admin/metrics' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/admin/platform-alerts
Regras de alerta da plataforma.
Respostas
código
significado
200
OK
401
credencial ausente ou inválida
Exemplo
curl -X GET 'https://api.asender.net/api/admin/platform-alerts' \
-H 'Authorization: Bearer SEU_TOKEN'
curl -X GET 'https://api.asender.net/api/admin/platform-alerts/events' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/admin/tenants
Lista as contas, na visão da plataforma.
Onde é usada
tela /impersonate.
Efeitos
uma leitura no core.
Respostas
código
significado
200
OK
401
credencial ausente ou inválida
Exemplo
curl -X GET 'https://api.asender.net/api/admin/tenants' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/admin/tenants/{id}
Detalhe de uma conta, na visão da plataforma.
Onde é usada
console, ao abrir um cliente.
Efeitos
uma leitura no core. # Por que não reusar GET /api/tenants/{id} Aquela rota exige PERTINÊNCIA: o usuário tem de ser membro da conta. Quem opera a plataforma não é membro de nenhuma conta de cliente — e não deve virar, porque virar membro para poder ver é exatamente o atalho que o ADR-0017 existe para impedir. A autorização aqui é a allowlist de plataforma, e a rota é outra.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Respostas
código
significado
200
OK
401
credencial ausente ou inválida
Exemplo
curl -X GET 'https://api.asender.net/api/admin/tenants/id_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
PATCH/api/admin/tenants/{id}
Altera uma conta, na visão da plataforma.
Onde é usada
console.
Efeitos
uma escrita no core. # Isto NÃO é a sessão de suporte A impersonação é somente leitura (ADR-0017) porque entrar na conta é ver o que o cliente vê. Isto é outra coisa: é a plataforma agindo COMO plataforma — suspender uma conta abusiva, por exemplo — e a ação fica no log com o ator. A distinção importa: se a sessão de suporte pudesse escrever, "entrar para ajudar" viraria "entrar para consertar", e o limite que o ADR desenhou desapareceria na prática.
Abre sessão de suporte na conta (somente leitura, ADR-0017).
Onde é usada
tela /impersonate.
Efeitos
uma escrita no core — e, a partir dela, LEITURA da conta alheia. O motivo é obrigatório: uma trilha sem motivo responde "alguém entrou", que é a metade inútil da pergunta que ela existe para responder.
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). Fluxo do dado: sessão → tenant resolvido → asender_messages GET /v1/reports/overview → PascalCase.
Entradas
from/to em ISO-8601 UTC; ausentes = últimos 30 dias (default resolvido pelo asender_messages, 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. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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_messages 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. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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_messages é 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. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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-tenants
POST/api/invitations/accept
Aceita um convite e vincula o usuário à conta.
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.
API pública do cliente. Fluxo do dado: API key → Principal.TenantID → GET /v1/messages?channel=email.
Entradas
status, q, cursor, limit (allowlist). channel do cliente é IGNORADO: nesta rota o canal é do contrato, não do parâmetro.
Saídas
200 com {Messages, NextCursor, HasMore}; 422 em query malformada.
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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-core` 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 ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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.
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.
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.
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 ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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.
uma leitura no core. # Esta rota NÃO exige plataforma Ela exige PERTINÊNCIA, como qualquer leitura de conta. Uma trilha que só a plataforma consegue ler serve para a plataforma se defender, não para o cliente se proteger (ADR-0017).
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/impersonations' \
-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.
Autoriza os DOIS lados: {id} e o ParentId de destino. Autorizar só a origem deixaria pendurar um tenant na árvore de outro cliente, que passaria a vê-lo em rollup de relatório.
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.
Onde é usada
tela de hierarquia de tenants. Segurança: autoriza os DOIS lados. Autorizar só {id} deixaria um usuário pendurar o tenant dele sob a árvore de outro cliente, o que é escalada de escopo — o novo pai passaria a "ver" a subárvore em rollup.
Saídas
200 com {Tenant}; 404 se qualquer um dos lados não é acessível; 422 quando o movimento cria ciclo (o trigger do core rejeita com check_violation).
Efeitos
reescreve a closure de tenants 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
Movido; a closure foi reescrita.
400
`400 ValidationError` — JSON inválido, campo desconhecido ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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.
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 ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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.
dashboard (settings/tenants) e backoffice. Fluxo do dado: sessão → tenant resolvido → asender-core GET /v1/tenants/{id}/tree → PascalCase.
Entradas
depth — inteiro positivo; valor não numérico é 422, não "sem limite" silencioso.
Saídas
200 com {TenantId, Tree:[...]}; 404 se o tenant pedido não é acessível.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
depth
query
não
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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'
meta
GET/
Identidade do serviço.
Respostas
código
significado
200
OK
Exemplo
curl -X GET 'https://api.asender.net/' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/healthz
Liveness. Não toca dependência.
Respostas
código
significado
200
Processo vivo.
Exemplo
curl -X GET 'https://api.asender.net/healthz' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/metrics
Métricas Prometheus.
Respostas
código
significado
200
Texto no formato de exposição do Prometheus.
Exemplo
curl -X GET 'https://api.asender.net/metrics' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/readyz
Readiness.
Reporta o estado do NATS em checks.nats mas responde 200 mesmo degradado (decisão explícita: o orquestrador não deve matar o processo por NATS momentaneamente fora). Redis não é dependência dura — o limiter cai para no-op.
Respostas
código
significado
200
Pronto (possivelmente degradado).
Exemplo
curl -X GET 'https://api.asender.net/readyz' \
-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. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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. Fluxo do dado: API key verificada → Principal.TenantID → asender-core GET /v1/tenants/{id}/usage (snake_case) → PascalMap → resposta pública.
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-core/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. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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. Fluxo do dado: API key → Principal.TenantID → GET /v1/messages?channel=email.
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. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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.
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 ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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.
Onde é usada
API pública do cliente. Fluxo do dado: igual ao Send, uma chamada ao asender_messages por item.
Saídas
202 com {Results, Count}; cada item traz MessageId ou Error.
Efeitos
escrita no serviço de mensageria por item aceito.
Respostas
código
significado
202
Lote processado (com ou sem itens rejeitados).
400
`400 ValidationError` — JSON inválido, campo desconhecido ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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_messages (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.
Onde é usada
API pública do cliente (autenticada por API key). Fluxo do dado: API key → APIKeyAuth → Principal.TenantID → POST /v1/messages (header X-Asender-Tenant) → messages + outbox no mesmo commit → NATS → worker.
Saídas
202 com {MessageId, MessageIds, Status, ReusedIdempotency}; 400 em validação local; 422 quando o asender_messages 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
**DIVERGÊNCIA CONHECIDA (RELATORIO-FINAL §3 item 10).** Nesta superfície
pública o header é ACEITO E IGNORADO — o handler só lê o campo
`IdempotencyKey` do CORPO. Replay com o mesmo header e corpo sem o campo
**duplica o envio**.
O `asender_messages` implementa idempotência de verdade e dá precedência
ao header sobre o campo do corpo; o buraco está apenas em não propagar o
header daqui para lá. Enquanto não for corrigido, use o campo
`IdempotencyKey` no 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. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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.
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
dashboard, tela /send/push. Fluxo do dado: messages.push_devices → asender_messages → aqui → seletor.
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
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. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
422
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_messages respondeu 201 no primeiro registro.
Onde é usada
SDK do cliente, na primeira abertura do app / renovação do token. Fluxo do dado: API key → Principal.TenantID → POST /v1/push/devices no asender_messages → messages.push_devices.
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.
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. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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`.
Envio só por tópico entra como destinatário sintético topic:<nome> — o asender_messages exige ao menos um destinatário e não modela tópico.
Onde é usada
API pública do cliente (autenticada por API key). Fluxo do dado: API key → APIKeyAuth → Principal.TenantID → POST /v1/messages (header X-Asender-Tenant) → messages + outbox no mesmo commit → NATS → worker.
Saídas
202 com {MessageId, MessageIds, Status, ReusedIdempotency}; 400 em validação local; 422 quando o asender_messages 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
**DIVERGÊNCIA CONHECIDA (RELATORIO-FINAL §3 item 10).** Nesta superfície
pública o header é ACEITO E IGNORADO — o handler só lê o campo
`IdempotencyKey` do CORPO. Replay com o mesmo header e corpo sem o campo
**duplica o envio**.
O `asender_messages` implementa idempotência de verdade e dá precedência
ao header sobre o campo do corpo; o buraco está apenas em não propagar o
header daqui para lá. Enquanto não for corrigido, use o campo
`IdempotencyKey` no 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. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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.
API pública do cliente (autenticada por API key). Fluxo do dado: API key → APIKeyAuth → Principal.TenantID → POST /v1/messages (header X-Asender-Tenant) → messages + outbox no mesmo commit → NATS → worker.
Saídas
202 com {MessageId, MessageIds, Status, ReusedIdempotency}; 400 em validação local; 422 quando o asender_messages 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
**DIVERGÊNCIA CONHECIDA (RELATORIO-FINAL §3 item 10).** Nesta superfície
pública o header é ACEITO E IGNORADO — o handler só lê o campo
`IdempotencyKey` do CORPO. Replay com o mesmo header e corpo sem o campo
**duplica o envio**.
O `asender_messages` implementa idempotência de verdade e dá precedência
ao header sobre o campo do corpo; o buraco está apenas em não propagar o
header daqui para lá. Enquanto não for corrigido, use o campo
`IdempotencyKey` no 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. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
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.
Authorization Server. É daqui que sai o token que a API pública exige.
https://auth.asender.net · 42 operações
.well-known
GET/.well-known/jwks.jsonsem schema na spec
JWKS serve /.well-known/jwks.json — a(s) chave(s) pública(s) RS256. Os clients
Onde é usada
verificação no client.
Exemplo
curl -X GET 'https://auth.asender.net/.well-known/jwks.json' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/.well-known/openid-configurationsem schema na spec
anuncia os endpoints e as capacidades (só `code`, só S256, só RS256 — o perfil OAuth2.1 da casa).
Onde é usada
os clients leem isto no bootstrap para se autoconfigurar. Cacheável (muda só em deploy).
Exemplo
curl -X GET 'https://auth.asender.net/.well-known/openid-configuration' \
-H 'Authorization: Bearer SEU_TOKEN'
auth
POST/v1/auth/login
Autentica e emite sessão.
Com 2FA ativo, a resposta traz two_factor_required:true e sem token; a sessão só é emitida por POST /v1/2fa/check.
User-Agent e IP do chamador entram no registro da sessão (o IP vem do RealIP do chi, portanto de X-Forwarded-For quando houver proxy).
Respostas
código
significado
200
Sessão emitida, ou 2FA pendente.
400
`400` — `InvalidRequest` (JSON inválido, **campo desconhecido**, corpo
vazio, corpo acima de 1 MiB), `InvalidInput` (validação de domínio) ou
`TwoFactorMissing` (2FA não configurado).
401
`401 InvalidCredentials` — credenciais inválidas.
500
`500 InternalError`.
**DIVERGÊNCIA:** o `Message` recebe `err.Error()` CRU, como no
`asender-core`. Pode vazar estrutura interna.
Sempre `ok:true`; `reset_token` presente quando um token foi gerado.
400
`400` — `InvalidRequest` (JSON inválido, **campo desconhecido**, corpo
vazio, corpo acima de 1 MiB), `InvalidInput` (validação de domínio) ou
`TwoFactorMissing` (2FA não configurado).
401
`401` — token de serviço ausente ou inválido. **Não ocorre com
`ENV=development`.**
500
`500 InternalError`.
**DIVERGÊNCIA:** o `Message` recebe `err.Error()` CRU, como no
`asender-core`. Pode vazar estrutura interna.
`400` — `InvalidRequest` (JSON inválido, **campo desconhecido**, corpo
vazio, corpo acima de 1 MiB), `InvalidInput` (validação de domínio) ou
`TwoFactorMissing` (2FA não configurado).
uma leitura. Não recebe usuário por parâmetro: a lista é sempre a do dono do token. 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://auth.asender.net/v1/auth/sessions' \
-H 'Authorization: Bearer SEU_TOKEN'
DELETE/v1/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. 404 tanto para sessão inexistente quanto para sessão de OUTRA pessoa: a distinção transformaria a rota num oráculo de id de sessão alheia.
É o hop que o middleware SessionAuth do asender-api faz em toda requisição /api/* autenticada.
A resposta DEVE trazer user.id. Um 200 sem ele é violação de contrato: o cliente do asender-api recusa com ErrContractViolation e o middleware barra o principal anônimo (defesa em profundidade). Esta rota não devolve tenant — quem sabe de tenant é o asender-core.
Respostas
código
significado
200
Sessão válida.
401
`401` — `MissingToken` (sem `Authorization: Bearer`), `InvalidToken`
(assinatura/formato) ou `ExpiredToken` (expirada **ou revogada por
logout**).
Exemplo
curl -X GET 'https://auth.asender.net/v1/auth/validate' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/auth/verify
Confirma o e-mail a partir do token do link.
Onde é usada
tela /verify do front, com o token da URL.
Efeitos
duas escritas. Token inválido, expirado e já usado respondem IGUAL: distingui-los diria a quem tem um link velho se ele já foi usado por outra pessoa.
Authorize implementa GET /authorize (OAuth2.1 authorization code + PKCE).
Onde é usada
o browser é redirecionado para cá pelo client (app) que quer logar.
Exemplo
curl -X GET 'https://auth.asender.net/authorize' \
-H 'Authorization: Bearer SEU_TOKEN'
authz
POST/v1/authz/decidesem schema na spec
`{permitido, motivo, papel, nivel}`.
Onde é usada
POST /v1/authz/decide.
Efeitos
grava a trilha (dentro do PDP). Erro de banco vira 503, e não {permitido:false}: o PEP precisa distinguir "a política diz não" de "não consegui perguntar" — o primeiro ele mostra ao usuário, o segundo ele resolve com o cache que já tem (piso 10).
GET /v1/authz/eu?sub=&tenant= — alimenta o switcher e a home do hub.
Exemplo
curl -X GET 'https://auth.asender.net/v1/authz/eu' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/authz/produtossem schema na spec
Catalogo devolve as ferramentas que existem.
Onde é usada
GET /v1/authz/produtos.
Exemplo
curl -X GET 'https://auth.asender.net/v1/authz/produtos' \
-H 'Authorization: Bearer SEU_TOKEN'
logout
GET/logoutsem schema na spec
Expects Authorization: Bearer <token>.
Exemplo
curl -X GET 'https://auth.asender.net/logout' \
-H 'Authorization: Bearer SEU_TOKEN'
meta
GET/
Identidade do serviço.
Onde é usada
rota raiz.
Efeitos
escreve a resposta. Serve para saber QUAL binário está rodando quando o comportamento diverge do esperado — a primeira pergunta de todo diagnóstico de deploy.
Respostas
código
significado
200
OK
Exemplo
curl -X GET 'https://auth.asender.net/' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/healthz
Liveness.
Onde é usada
liveness probe.
Efeitos
escreve a resposta. Não consulta o banco de propósito: liveness que falha por causa do Postgres faz o orquestrador REINICIAR um serviço saudável durante uma instabilidade do banco — e reiniciar o serviço de login em massa transforma degradação em queda de autenticação para todo mundo.
Respostas
código
significado
200
OK
Exemplo
curl -X GET 'https://auth.asender.net/healthz' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/metrics
Métricas Prometheus.
Respostas
código
significado
200
Texto.
Exemplo
curl -X GET 'https://auth.asender.net/metrics' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/readyz
Readiness — ping no Postgres.
Onde é usada
readiness probe.
Efeitos
uma consulta trivial ao Postgres. É aqui que a dependência entra — o oposto do /healthz. O prazo curto é deliberado: uma sonda que espera indefinidamente nunca reporta "não pronto", e a instância continua recebendo login que vai falhar.
Respostas
código
significado
200
Banco respondeu.
503
`NotReady` — o erro do ping vai no `Message`.
Exemplo
curl -X GET 'https://auth.asender.net/readyz' \
-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.
Respostas
código
significado
200
OK
Exemplo
curl -X GET 'https://auth.asender.net/version' \
-H 'Authorization: Bearer SEU_TOKEN'
tenants
GET/v1/tenants/{id}/acessossem schema na spec
ListarAcessos devolve quem acessa o quê na conta.
Onde é usada
GET /v1/tenants/{id}/acessos.
Exemplo
curl -X GET 'https://auth.asender.net/v1/tenants/id_AQUI/acessos' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/tenants/{id}/acessossem schema na spec
Conceder dá acesso de uma pessoa a uma ferramenta da conta.
Onde é usada
POST /v1/tenants/{id}/acessos {user_id, produto, papel}. Papel que o produto não declara é recusado pela FK — 422 com a mensagem, e não 500: "papel inventado" é erro de quem chamou, não do servidor.
DELETE/v1/tenants/{id}/acessos/{userId}/{produto}sem schema na spec
Revogar tira o acesso.
Onde é usada
DELETE /v1/tenants/{id}/acessos/{userId}/{produto}. Idempotente: revogar duas vezes responde ok nas duas. O efeito chega às ferramentas em até 60s (TTL do cache do PEP) — contrato escrito no ADR-0025.
GET/v1/tenants/{id}/authz/decisoessem schema na spec
Trilha devolve as decisões recentes da conta.
Onde é usada
GET /v1/tenants/{id}/authz/decisoes?negadas=1&limite=50.
Exemplo
curl -X GET 'https://auth.asender.net/v1/tenants/id_AQUI/authz/decisoes' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/tenants/{id}/produtossem schema na spec
ListarAssinatura devolve o que a conta assina.
Onde é usada
GET /v1/tenants/{id}/produtos.
Exemplo
curl -X GET 'https://auth.asender.net/v1/tenants/id_AQUI/produtos' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/tenants/{id}/produtossem schema na spec
Assinar liga ou suspende um produto na conta.
Onde é usada
POST /v1/tenants/{id}/produtos {produto, status}. status é validado aqui E no CHECK da tabela: a borda dá a mensagem, o banco dá a garantia — a borda pode ser contornada por outro caminho de escrita.
Segunda etapa do login — valida o código e EMITE a sessão.
Sucesso devolve o mesmo login de POST /v1/auth/login, agora com token.
Respostas
código
significado
200
Código válido; sessão emitida.
400
`400` — `InvalidRequest` (JSON inválido, **campo desconhecido**, corpo
vazio, corpo acima de 1 MiB), `InvalidInput` (validação de domínio) ou
`TwoFactorMissing` (2FA não configurado).
`400` — `InvalidRequest` (JSON inválido, **campo desconhecido**, corpo
vazio, corpo acima de 1 MiB), `InvalidInput` (validação de domínio) ou
`TwoFactorMissing` (2FA não configurado).
401
`401` — token de serviço ausente ou inválido. **Não ocorre com
`ENV=development`.**
Ainda não confirmado: o 2FA só passa a valer depois de POST /v1/2fa/verify.
Respostas
código
significado
200
Segredo gerado.
400
`400` — `InvalidRequest` (JSON inválido, **campo desconhecido**, corpo
vazio, corpo acima de 1 MiB), `InvalidInput` (validação de domínio) ou
`TwoFactorMissing` (2FA não configurado).
401
`401` — token de serviço ausente ou inválido. **Não ocorre com
`ENV=development`.**
`400` — `InvalidRequest` (JSON inválido, **campo desconhecido**, corpo
vazio, corpo acima de 1 MiB), `InvalidInput` (validação de domínio) ou
`TwoFactorMissing` (2FA não configurado).
valida o Bearer (RS256, iss, exp) e devolve sub/email/name.
Onde é usada
o client chama para hidratar o perfil. Sem token válido -> 401 invalid_token.
Exemplo
curl -X GET 'https://auth.asender.net/userinfo' \
-H 'Authorization: Bearer SEU_TOKEN'
users
POST/v1/users
Cria um usuário.
A rota é declarada como r.Post("/") dentro de r.Route("/users"), ou seja o padrão registrado é /v1/users/. Ambas as formas respondem 201 (verificado na stack): POST /v1/users e POST /v1/users/.
Senha é hasheada com bcrypt no custo configurado (BCRYPT_COST, ≥ 12). O hash NUNCA aparece em resposta alguma.
Respostas
código
significado
201
Criado.
400
`400` — `InvalidRequest` (JSON inválido, **campo desconhecido**, corpo
vazio, corpo acima de 1 MiB), `InvalidInput` (validação de domínio) ou
`TwoFactorMissing` (2FA não configurado).
401
`401` — token de serviço ausente ou inválido. **Não ocorre com
`ENV=development`.**
409
`409 Conflict` — email já cadastrado.
500
`500 InternalError`.
**DIVERGÊNCIA:** o `Message` recebe `err.Error()` CRU, como no
`asender-core`. Pode vazar estrutura interna.
Public id (`usr_<hex>`). O BIGSERIAL interno nunca aparece.
Respostas
código
significado
200
OK
401
`401` — token de serviço ausente ou inválido. **Não ocorre com
`ENV=development`.**
404
`404 NotFound`.
Exemplo
curl -X GET 'https://auth.asender.net/v1/users/id_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
PATCH/v1/users/{id}
Altera nome e/ou email.
Senha NÃO é alterável por aqui — o caminho é POST /v1/auth/password-reset/confirm.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id (`usr_<hex>`). O BIGSERIAL interno nunca aparece.
Respostas
código
significado
200
OK
400
`400` — `InvalidRequest` (JSON inválido, **campo desconhecido**, corpo
vazio, corpo acima de 1 MiB), `InvalidInput` (validação de domínio) ou
`TwoFactorMissing` (2FA não configurado).
401
`401` — token de serviço ausente ou inválido. **Não ocorre com
`ENV=development`.**
Alcançável apenas pela malha privada — não é superfície pública.
http://asender-core:8002 · 63 operações
api-keys
POST/v1/api-keys/validate
Resolve uma API key em tenant + escopos.
É o hop que o middleware APIKeyAuth do asender-api faz em toda requisição /v1/* pública.
O campo do corpo é key (minúsculo) e o de status é tenant_status, não status. Errar qualquer um dos dois derruba a API pública inteira para 503 — foi exatamente o defeito corrigido antes desta rodada.
Chave inválida é 401, e o handler NÃO distingue causa (não existe / revogada / expirada).
Onde é usada
POST /internal/v1/api-keys/validate, chamada pelo asender-api a cada request autenticada por chave.
Efeitos
escreve a resposta; pode consultar banco e cache. Toda recusa responde o MESMO 401 "Invalid API key.", sem distinguir chave inexistente de revogada, expirada ou de outra conta: a diferença entre essas mensagens é um oráculo que diz ao atacante quando ele acertou o formato.
Respostas
código
significado
200
Chave válida.
400
`400 ValidationError` — JSON inválido, **campo desconhecido**
(`DisallowUnknownFields`), corpo acima de 1 MiB, campo obrigatório
ausente, ou `parent_id`/`monthly_quota` omitido no PATCH.
401
`401 AuthorizationError` — chave inválida, revogada ou expirada, sem distinguir.
Public id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
Exemplo
curl -X GET 'http://asender-core:8002/v1/tenants/id_AQUI/api-keys' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/tenants/{id}/api-keys
Emite uma API key.
plaintext é devolvido UMA VEZ e nunca mais. Só o hash é persistido. A resposta traz plaintext_warning dizendo isso.
Onde é usada
POST /v1/tenants/{id}/api-keys.
Efeitos
escreve a resposta HTTP. A chave em claro sai UMA vez, nesta resposta: o banco guarda só o hash, e por isso chave perdida se substitui — não se recupera.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
201
Emitida.
400
`400 ValidationError` — JSON inválido, **campo desconhecido**
(`DisallowUnknownFields`), corpo acima de 1 MiB, campo obrigatório
ausente, ou `parent_id`/`monthly_quota` omitido no PATCH.
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
escreve a resposta; cria a associação de membro. É a única rota de convite SEM a conta no caminho: quem aceita ainda não pertence a conta nenhuma, e o tenant é a RESPOSTA do token, não a entrada. Por isso a validade é conferida aqui, e não só na emissão.
Respostas
código
significado
200
Aceito; devolve o membro criado.
400
`400 ValidationError` — JSON inválido, **campo desconhecido**
(`DisallowUnknownFields`), corpo acima de 1 MiB, campo obrigatório
ausente, ou `parent_id`/`monthly_quota` omitido no PATCH.
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
O token de cada convite vem no corpo. Quem lê esta rota pode aceitar qualquer convite pendente.
Onde é usada
GET /v1/tenants/{id}/invitations.
Efeitos
escreve a resposta HTTP.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
Exemplo
curl -X GET 'http://asender-core:8002/v1/tenants/id_AQUI/invitations' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/tenants/{id}/invitations
Cria um convite.
Onde é usada
POST /v1/tenants/{id}/invitations.
Efeitos
escreve a resposta HTTP.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
201
Criado.
400
`400 ValidationError` — JSON inválido, **campo desconhecido**
(`DisallowUnknownFields`), corpo acima de 1 MiB, campo obrigatório
ausente, ou `parent_id`/`monthly_quota` omitido no PATCH.
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
Public id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
Exemplo
curl -X GET 'http://asender-core:8002/v1/tenants/id_AQUI/members' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/tenants/{id}/members
Vincula um usuário ao tenant.
Onde é usada
POST /v1/tenants/{id}/members.
Efeitos
escreve a resposta HTTP.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
201
Vinculado.
400
`400 ValidationError` — JSON inválido, **campo desconhecido**
(`DisallowUnknownFields`), corpo acima de 1 MiB, campo obrigatório
ausente, ou `parent_id`/`monthly_quota` omitido no PATCH.
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
409
`409 Conflict` — slug já em uso, vínculo duplicado.
escreve a resposta. Serve para saber QUAL binário está rodando quando o comportamento diverge do esperado — a primeira pergunta de todo diagnóstico de deploy.
Respostas
código
significado
200
OK
Exemplo
curl -X GET 'http://asender-core:8002/' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/healthz
Liveness.
Onde é usada
liveness probe.
Efeitos
escreve a resposta. Não consulta o banco de propósito: liveness que falha por causa do Postgres faz o orquestrador REINICIAR um serviço saudável durante uma instabilidade do banco — trocando degradação por queda.
Respostas
código
significado
200
OK
Exemplo
curl -X GET 'http://asender-core:8002/healthz' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/metrics
Métricas Prometheus.
Respostas
código
significado
200
Texto.
Exemplo
curl -X GET 'http://asender-core:8002/metrics' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/readyz
Readiness — ping em Postgres e Redis.
Resposta é o MAPA DE CHECKS cru, sem chave status.
Onde é usada
readiness probe.
Efeitos
uma consulta trivial a cada dependência. É aqui que a dependência entra — o oposto do /healthz. O prazo curto é deliberado: uma sonda que espera indefinidamente nunca reporta "não pronto", e a instância segue recebendo tráfego que vai falhar.
Respostas
código
significado
200
Todas as dependências responderam.
503
Alguma dependência fora; o valor traz `down: <motivo>`.
Exemplo
curl -X GET 'http://asender-core:8002/readyz' \
-H 'Authorization: Bearer SEU_TOKEN'
platform
GET/platform/admins/{userId}sem schema na spec
GET /v1/platform/admins/{userId}.
Onde é usada
gateway, antes de mostrar ou aceitar qualquer rota de plataforma.
Efeitos
uma leitura. 200 com {"platform": false} em vez de 404: quem pergunta é serviço, não usuário, e um 404 aqui obrigaria o chamador a distinguir "não é plataforma" de "a rota sumiu" — que é como um deny-default vira um allow por engano.
Exemplo
curl -X GET 'http://asender-core:8002/platform/admins/userId_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/platform/alertssem schema na spec
GET /v1/platform/alerts.
Onde é usada
console de plataforma.
Efeitos
uma leitura.
Exemplo
curl -X GET 'http://asender-core:8002/platform/alerts' \
-H 'Authorization: Bearer SEU_TOKEN'
uma escrita. ativo ausente PRESERVA o valor: quem renomeia não quer ligar nem desligar, e um default implícito faria a renomeação mudar o comportamento em silêncio.
uma leitura. Sem ator, ao contrário de todas as outras deste arquivo: quem chama é serviço, não operador. O token de serviço já foi conferido no grupo /v1.
Exemplo
curl -X GET 'http://asender-core:8002/platform/listas' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/platform/metricssem schema na spec
GET /v1/platform/metrics.
Onde é usada
home do console de plataforma.
Efeitos
uma leitura.
Exemplo
curl -X GET 'http://asender-core:8002/platform/metrics' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/platform/tenantssem schema na spec
GET /v1/platform/tenants?q=&limite=N.
Onde é usada
a tela de suporte, para achar a conta do chamado.
Efeitos
uma leitura. # Ela vive aqui, e não junto de /v1/tenantsGET /v1/tenants é escopado por usuário — é a lista de quem pertence. Esta é a lista de TODAS as contas, e é o tipo de leitura que só quem pode entrar em conta alheia deveria fazer. Deixá-la ao lado da outra faria as duas parecerem a mesma coisa com um parâmetro a mais, que é como se abre uma sem querer. Quem confere a allowlist é o gateway, ANTES de chamar: aqui a superfície já exige o token de serviço.
Exemplo
curl -X GET 'http://asender-core:8002/platform/tenants' \
-H 'Authorization: Bearer SEU_TOKEN'
relays
GET/v1/relays
Relays de entrega, opcionalmente filtrados por tenant.
Onde é usada
GET /v1/relays, com filtro opcional por conta.
Efeitos
escreve a resposta HTTP.
Parâmetros
nome
em
obrigatório
descrição
tenant_id
query
não
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
Exemplo
curl -X GET 'http://asender-core:8002/v1/relays' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/relays
Registra um relay.
master_key é cifrada em repouso e NUNCA volta em nenhuma leitura.
Onde é usada
POST /v1/relays.
Efeitos
escreve a resposta HTTP. A master key chega em claro e é cifrada ANTES de qualquer escrita.
Respostas
código
significado
201
Registrado.
400
`400 ValidationError` — JSON inválido, **campo desconhecido**
(`DisallowUnknownFields`), corpo acima de 1 MiB, campo obrigatório
ausente, ou `parent_id`/`monthly_quota` omitido no PATCH.
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
409
`409 Conflict` — slug já em uso, vínculo duplicado.
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
Exemplo
curl -X GET 'http://asender-core:8002/v1/relays/id_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/relays/{id}/health-check
Sonda o relay AGORA e grava o resultado.
Efeito colateral de saída de rede (chama a URL do relay). Relay inalcançável não é erro desta rota: o estado vai para status e health_data do payload devolvido.
Onde é usada
POST /v1/relays/{id}/health.
Efeitos
escreve a resposta HTTP. Fala com um servidor de FORA por HTTP, com prazo curto: sem o prazo, um relay que aceita a conexão e nunca responde prenderia a requisição do painel.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Respostas
código
significado
200
Sondado; veja `status`.
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
502
Erro repassado do relay (`relayclient.APIError` preserva o status upstream).
Provisiona um tenant no relay e devolve a credencial dele.
relay_api_key sai UMA VEZ. O relay não a mostra de novo.
Onde é usada
POST /v1/relays/{id}/provision.
Efeitos
escreve a resposta HTTP. Decifra a master key só no momento da chamada e não a devolve na resposta.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Respostas
código
significado
201
Provisionado.
400
`400 ValidationError` — JSON inválido, **campo desconhecido**
(`DisallowUnknownFields`), corpo acima de 1 MiB, campo obrigatório
ausente, ou `parent_id`/`monthly_quota` omitido no PATCH.
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
Breadcrumb raiz→nó, com o próprio nó como ÚLTIMO item.
Tenant raiz devolve lista de 1 item. É a rota que o asender-api usa para provar acesso herdado.
Onde é usada
GET /v1/tenants/{id}/ancestors. Fluxo do dado: path {id} → TreeService.Ancestors → closure → JSON.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
Exemplo
curl -X GET 'http://asender-core:8002/v1/tenants/id_AQUI/ancestors' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/tenants/{id}/children
Cria uma sub-conta (`kind='leaf'`) sob o tenant do path.
Não existe campo parent_id no corpo: o pai vem do PATH e é resolvido no servidor.
Onde é usada
POST /v1/tenants/{id}/children. Fluxo do dado: body {name,slug,monthly_quota} + path {id} → TreeService.CreateChild → core.tenants + closure → 201.
Efeitos
cria tenant kind='leaf' com parent_id = tenant do path.
Erros
400 body inválido/nome vazio; 404 pai inexistente; 409 slug duplicado.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
201
Criada.
400
`400 ValidationError` — JSON inválido, **campo desconhecido**
(`DisallowUnknownFields`), corpo acima de 1 MiB, campo obrigatório
ausente, ou `parent_id`/`monthly_quota` omitido no PATCH.
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
409
`409 Conflict` — slug já em uso, vínculo duplicado.
Para rollup, o escopo é o id do path MAIS estes ids. A exclusão é deliberada.
Onde é usada
GET /v1/tenants/{id}/descendants. Fluxo do dado: path {id} + query depth → TreeService.Descendants → closure → JSON.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
depth
query
não
Limite de níveis. Ausente = sem limite. Não numérico, negativo ou acima
de `MaxTreeDepth` → **422**, sem coerção e sem tocar o banco.
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
422
`422 ValidationError` — `depth` fora da faixa, pai inválido, ou **ciclo**
(`repo.ErrCycle`, vindo do `check_violation` do trigger de closure).
Nunca 500.
Exemplo
curl -X GET 'http://asender-core:8002/v1/tenants/id_AQUI/descendants' \
-H 'Authorization: Bearer SEU_TOKEN'
PATCH/v1/tenants/{id}/parent
Move o tenant, ou o promove a raiz.
{"parent_id":"acc_..."} move; {"parent_id":null} desanexa e o nó vira kind='root'. Campo ausente é 400 — um {} acidental não pode promover uma sub-conta.
Mover para dentro da própria subárvore (ciclo) é 422, nunca 500. A closure e o depth de toda a subárvore são reescritos pelo trigger.
Onde é usada
PATCH /v1/tenants/{id}/parent. Fluxo do dado: body {parent_id} (public id) + path {id} → TreeService.SetParent → UPDATE parent_id → trigger recalcula a closure → 200.
Efeitos
reescreve closure e depth de toda a subárvore movida.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
Movido.
400
`400 ValidationError` — JSON inválido, **campo desconhecido**
(`DisallowUnknownFields`), corpo acima de 1 MiB, campo obrigatório
ausente, ou `parent_id`/`monthly_quota` omitido no PATCH.
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
422
`422 ValidationError` — `depth` fora da faixa, pai inválido, ou **ciclo**
(`repo.ErrCycle`, vindo do `check_violation` do trigger de closure).
Nunca 500.
null limpa e o nó volta a herdar do ancestral mais próximo com valor. Campo ausente, tipo errado (sem coerção) ou negativo → 400. A resposta já traz effective_quota recalculada.
Onde é usada
PATCH /v1/tenants/{id}/quota. Fluxo do dado: body {monthly_quota} + path {id} → TreeService.SetQuota → core.tenants → 200.
Erros
400 campo ausente, tipo errado (sem coerção) ou valor negativo; 404 tenant inexistente.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
Gravada.
400
`400 ValidationError` — JSON inválido, **campo desconhecido**
(`DisallowUnknownFields`), corpo acima de 1 MiB, campo obrigatório
ausente, ou `parent_id`/`monthly_quota` omitido no PATCH.
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
Cada filho vem logo abaixo do pai; indente por relative_depth.
Onde é usada
GET /v1/tenants/{id}/tree. Fluxo do dado: path {id} + query depth → TreeService.Tree → closure → JSON.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
depth
query
não
Limite de níveis. Ausente = sem limite. Não numérico, negativo ou acima
de `MaxTreeDepth` → **422**, sem coerção e sem tocar o banco.
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
422
`422 ValidationError` — `depth` fora da faixa, pai inválido, ou **ciclo**
(`repo.ErrCycle`, vindo do `check_violation` do trigger de closure).
Nunca 500.
Exemplo
curl -X GET 'http://asender-core:8002/v1/tenants/id_AQUI/tree' \
-H 'Authorization: Bearer SEU_TOKEN'
tenants
GET/tenants/{id}/impersonationssem schema na spec
GET /v1/tenants/{id}/impersonations?limite=N.
Onde é usada
consulta do CLIENTE sobre a própria conta, e da plataforma.
Efeitos
uma leitura.
Exemplo
curl -X GET 'http://asender-core:8002/tenants/id_AQUI/impersonations' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/tenants
Tenants de um usuário.
user_id é OBRIGATÓRIO: sem ele é 400, não listagem global. A listagem global existiria só para ferramenta administrativa e está marcada como TODO — não é rota.
Parâmetros
nome
em
obrigatório
descrição
user_id
query
sim
Respostas
código
significado
200
OK
400
`400 ValidationError` — JSON inválido, **campo desconhecido**
(`DisallowUnknownFields`), corpo acima de 1 MiB, campo obrigatório
ausente, ou `parent_id`/`monthly_quota` omitido no PATCH.
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
500
`500 InternalError`.
**DIVERGÊNCIA:** ao contrário do `asender_messages`, este serviço passa
`err.Error()` CRU para o campo `Message`. Erro de driver pode vazar
estrutura interna no corpo. Aceitável só enquanto o serviço não é
exposto; é dívida registrada.
Exemplo
curl -X GET 'http://asender-core:8002/v1/tenants' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/tenants
Cria um tenant raiz.
DisallowUnknownFields recusa campo fora deste conjunto. Esta guarda era, até a onda 7, a ÚNICA defesa contra o mass assignment do POST /api/tenants — defesa morando no serviço errado. Hoje o asender-api tem allowlist tipada própria; esta continua como defesa em profundidade.
Onde é usada
POST /v1/tenants.
Efeitos
escreve a resposta HTTP.
Respostas
código
significado
201
Criado.
400
`400 ValidationError` — JSON inválido, **campo desconhecido**
(`DisallowUnknownFields`), corpo acima de 1 MiB, campo obrigatório
ausente, ou `parent_id`/`monthly_quota` omitido no PATCH.
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
409
`409 Conflict` — slug já em uso, vínculo duplicado.
escreve a resposta HTTP. Apagamento LÓGICO: mensagens, chaves e membros referenciam a conta, e um DELETE físico deixaria o histórico apontando para o nada.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
204
Removido.
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
A resposta é o objeto do tenant no TOPO, sem chave de recurso — exceção ao padrão aninhado deste serviço.
Onde é usada
GET /v1/tenants/{id}.
Efeitos
escreve a resposta HTTP.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
Exemplo
curl -X GET 'http://asender-core:8002/v1/tenants/id_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
PATCH/v1/tenants/{id}
Altera nome, status ou metadata.
Onde é usada
PATCH /v1/tenants/{id}.
Efeitos
escreve a resposta HTTP. Campo ausente PRESERVA o valor — não apaga. É o contrato de PATCH, e é o que permite ao painel mandar só o que mudou.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
OK
400
`400 ValidationError` — JSON inválido, **campo desconhecido**
(`DisallowUnknownFields`), corpo acima de 1 MiB, campo obrigatório
ausente, ou `parent_id`/`monthly_quota` omitido no PATCH.
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
Tenant sem linha em core.usage_counters recebe os contadores ZERADOS, com updated_at: null — ausência de contador é consumo zero, não ausência de recurso. O 404 fica reservado ao tenant que de fato não existe.
Histórico: até esta rodada a ausência de linha devolvia 404, e o asender-api o traduzia em 502 no GET /v1/account/usage. Como o seed não cria contador nenhum, isso atingia todos os tenants.
Onde é usada
GET /v1/tenants/{id}/usage.
Efeitos
escreve a resposta HTTP.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
period
query
não
Vazio = período corrente.
Respostas
código
significado
200
Contadores do período (zerados quando não houve consumo).
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
O TENANT não existe. Nunca "não há contador".
Exemplo
curl -X GET 'http://asender-core:8002/v1/tenants/id_AQUI/usage' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/tenants/{id}/usage/increment
Incrementa um contador de consumo.
Onde é usada
POST /internal/v1/tenants/{id}/usage, chamada pelos serviços de envio.
Efeitos
escreve a resposta HTTP.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
Contador após o incremento.
400
`400 ValidationError` — JSON inválido, **campo desconhecido**
(`DisallowUnknownFields`), corpo acima de 1 MiB, campo obrigatório
ausente, ou `parent_id`/`monthly_quota` omitido no PATCH.
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
Chave pública VAPID do tenant (gera o par na primeira chamada).
Valor que o site do cliente coloca em applicationServerKey. A chave PRIVADA não é exposta por rota nenhuma: quem precisa assinar chama /vapid/sign.
Onde é usada
asender-api, ao montar o artefato de integração do site (/api/push/site-config) e a tela de push do dashboard. Fluxo do dado: core.vapid_keys → asender-api → site do cliente → browser.
Saídas
200 com {public_key, subject}.
Efeitos
pode criar a linha do par na primeira chamada do tenant. A chave PRIVADA nunca aparece nesta resposta nem em nenhuma outra: quem precisa assinar chama /vapid/sign.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Respostas
código
significado
200
Chave pública em base64url.
Exemplo
curl -X GET 'http://asender-core:8002/v1/tenants/id_AQUI/vapid' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/tenants/{id}/vapid/sign
Assina o JWT VAPID (RFC 8292) para um endpoint de push service.
Chamado pelo asender-push-worker antes de cada entrega. A audiência do JWT é derivada da ORIGEM do endpoint informado, nunca escolhida pelo chamador.
Onde é usada
chamado pelo asender-push-worker antes de entregar cada notificação. Fluxo do dado: worker → aqui → JWT ES256 → worker → push service.
Saídas
200 com {authorization, public_key}; 400 em endpoint inválido. ## Por que assinar aqui, em vez de mandar a chave para o worker A alternativa óbvia — o worker pedir a chave privada e assinar sozinho — coloca material secreto na rede e na memória de outro processo, e transforma cada worker num lugar de onde a chave pode vazar. Assinando aqui, a chave privada não sai deste serviço: o worker recebe um token de 12h atado a UMA audiência, inútil para qualquer outro push service.
Public id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
Exemplo
curl -X GET 'http://asender-core:8002/v1/tenants/id_AQUI/webhooks' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/tenants/{id}/webhooks
Cria um endpoint de webhook.
secret é devolvido UMA VEZ (gerado se não informado) e cifrado em repouso.
Onde é usada
POST /v1/tenants/{id}/webhooks.
Efeitos
escreve a resposta HTTP. O segredo de assinatura sai em claro UMA vez, nesta resposta.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
201
Criado.
400
`400 ValidationError` — JSON inválido, **campo desconhecido**
(`DisallowUnknownFields`), corpo acima de 1 MiB, campo obrigatório
ausente, ou `parent_id`/`monthly_quota` omitido no PATCH.
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
Public id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
whkId
path
sim
Respostas
código
significado
200
OK
400
`400 ValidationError` — JSON inválido, **campo desconhecido**
(`DisallowUnknownFields`), corpo acima de 1 MiB, campo obrigatório
ausente, ou `parent_id`/`monthly_quota` omitido no PATCH.
401
`401 AuthorizationError` — pretendido para token de serviço ausente ou
inválido. **Não ocorre hoje:** o middleware é um placeholder.
404
`404 NotFound` — o recurso não existe. **Não significa "não é seu"**:
este serviço não faz checagem de dono; quem autoriza é o `asender-api`.
Alcançável apenas pela malha privada — não é superfície pública.
http://asender-messages:8004 · 24 operações
contacts
GET/v1/contacts
Audiência do tenant, com busca e filtro por lista.
Onde é usada
tela de audiência do dashboard, via asender-api. Fluxo do dado: header de tenant + query → repo → messages.contacts → JSON.
Entradas
q, list_id, cursor, limit. Query malformada é 422 (parseQuery), não listagem sem filtro.
Saídas
200 com contacts, next_cursor e has_more; 404 se list_id não existir no tenant; 422 em parâmetro inválido.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
sim
**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a
partir do token verificado. Ausente → 400 `MissingTenant`.
q
query
não
list_id
query
não
Public id (`lst_<hex>`). Lista de outro tenant → 404.
cursor
query
não
limit
query
não
Respostas
código
significado
200
Página de contatos.
400
`400` — `MissingTenant` (header de tenant ausente) ou `InvalidRequest`
(JSON inválido, **campo desconhecido**, corpo acima de 1 MiB, mais de um
documento JSON no corpo).
401
`401 Unauthorized` — `X-Asender-Svc-Token` ausente, vazio ou incorreto.
Também é a resposta quando a auth está ligada e o `SERVICE_SECRET` está
vazio (fail closed).
404
`404 NotFound` — inexistente **ou de outro tenant**. A mensagem é sempre
`resource not found`, sem distinguir os dois casos.
422
`422 ValidationError` — query malformada, `cursor`/`limit` não numérico
ou não positivo, canal/status fora da allowlist, contato sem email nem
telefone, plataforma inválida, variável de template faltando (a mensagem
NOMEIA as faltantes), lote vazio ou acima do teto.
Exemplo
curl -X GET 'http://asender-messages:8004/v1/contacts' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/contacts
Cria um contato.
Onde é usada
formulário "novo contato" da tela de audiência. Fluxo do dado: asender-api → aqui → messages.contacts → resposta 201.
Saídas
201 com contact; 400 em JSON inválido ou campo desconhecido; 422 em validação (sem email nem telefone, email malformado, email já usado).
Efeitos
escreve em messages.contacts.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
sim
**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a
partir do token verificado. Ausente → 400 `MissingTenant`.
Respostas
código
significado
201
Criado.
400
`400` — `MissingTenant` (header de tenant ausente) ou `InvalidRequest`
(JSON inválido, **campo desconhecido**, corpo acima de 1 MiB, mais de um
documento JSON no corpo).
401
`401 Unauthorized` — `X-Asender-Svc-Token` ausente, vazio ou incorreto.
Também é a resposta quando a auth está ligada e o `SERVICE_SECRET` está
vazio (fail closed).
422
`422 ValidationError` — query malformada, `cursor`/`limit` não numérico
ou não positivo, canal/status fora da allowlist, contato sem email nem
telefone, plataforma inválida, variável de template faltando (a mensagem
NOMEIA as faltantes), lote vazio ou acima do teto.
500
`500 InternalError` — falha genuína de infra. O detalhe fica no log; o
corpo é sempre `unexpected error`, sem SQL, driver ou stack trace.
**Input do cliente nunca deve chegar aqui** (a exceção conhecida é
`POST /v1/messages/{id}/status` com `sending`).
O histórico de mensagens do contato continua íntegro e o email volta a poder ser cadastrado. Já removido → 404.
Onde é usada
tela de audiência.
Saídas
204 sem corpo; 404 se não existir no tenant ou já estar removido.
Efeitos
escreve em messages.contacts (soft delete).
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
sim
**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a
partir do token verificado. Ausente → 400 `MissingTenant`.
id
path
sim
Respostas
código
significado
204
Removido.
400
`400` — `MissingTenant` (header de tenant ausente) ou `InvalidRequest`
(JSON inválido, **campo desconhecido**, corpo acima de 1 MiB, mais de um
documento JSON no corpo).
401
`401 Unauthorized` — `X-Asender-Svc-Token` ausente, vazio ou incorreto.
Também é a resposta quando a auth está ligada e o `SERVICE_SECRET` está
vazio (fail closed).
404
`404 NotFound` — inexistente **ou de outro tenant**. A mensagem é sempre
`resource not found`, sem distinguir os dois casos.
email e phone NÃO são patcháveis: mandá-los é campo desconhecido → 400, não silêncio.
Onde é usada
edição inline da tela de audiência e fluxo de descadastro.
Saídas
200 com contact; 404 se não existir no tenant; 422 se nada foi enviado ou algum campo for inválido; 400 em JSON inválido (email/telefone não são patcháveis, então vêm como campo desconhecido).
Efeitos
escreve em messages.contacts.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
sim
**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a
partir do token verificado. Ausente → 400 `MissingTenant`.
id
path
sim
Respostas
código
significado
200
Atualizado.
400
`400` — `MissingTenant` (header de tenant ausente) ou `InvalidRequest`
(JSON inválido, **campo desconhecido**, corpo acima de 1 MiB, mais de um
documento JSON no corpo).
401
`401 Unauthorized` — `X-Asender-Svc-Token` ausente, vazio ou incorreto.
Também é a resposta quando a auth está ligada e o `SERVICE_SECRET` está
vazio (fail closed).
404
`404 NotFound` — inexistente **ou de outro tenant**. A mensagem é sempre
`resource not found`, sem distinguir os dois casos.
422
`422 ValidationError` — query malformada, `cursor`/`limit` não numérico
ou não positivo, canal/status fora da allowlist, contato sem email nem
telefone, plataforma inválida, variável de template faltando (a mensagem
NOMEIA as faltantes), lote vazio ou acima do teto.
Upsert em lote pela identidade natural (email; telefone se não houver email).
Import parcial é resultado legítimo: item ruim aparece em errors com seu índice e não invalida o lote. 422 só quando o lote INTEIRO é inválido (vazio ou acima do teto).
Onde é usada
import da tela de audiência e seed de dados reais, que precisa poder re-rodar sem duplicar (§22.5). Fluxo do dado: asender-api → aqui → messages.contacts.
Saídas
200 com created, updated, errors[] e contacts[]. Um item ruim não invalida o lote — ele aparece em errors com seu índice. 422 só quando o lote inteiro é inválido (vazio ou acima do teto).
Efeitos
escreve em messages.contacts.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
sim
**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a
partir do token verificado. Ausente → 400 `MissingTenant`.
Respostas
código
significado
200
Lote processado.
400
`400` — `MissingTenant` (header de tenant ausente) ou `InvalidRequest`
(JSON inválido, **campo desconhecido**, corpo acima de 1 MiB, mais de um
documento JSON no corpo).
401
`401 Unauthorized` — `X-Asender-Svc-Token` ausente, vazio ou incorreto.
Também é a resposta quando a auth está ligada e o `SERVICE_SECRET` está
vazio (fail closed).
422
`422 ValidationError` — query malformada, `cursor`/`limit` não numérico
ou não positivo, canal/status fora da allowlist, contato sem email nem
telefone, plataforma inválida, variável de template faltando (a mensagem
NOMEIA as faltantes), lote vazio ou acima do teto.
tela de audiência do dashboard, via asender-api. Fluxo do dado: header de tenant + query → repo → messages.contacts → JSON.
Entradas
q, list_id, cursor, limit. Query malformada é 422 (parseQuery), não listagem sem filtro.
Saídas
200 com contacts, next_cursor e has_more; 404 se list_id não existir no tenant; 422 em parâmetro inválido.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
sim
**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a
partir do token verificado. Ausente → 400 `MissingTenant`.
cursor
query
não
limit
query
não
Respostas
código
significado
200
OK
400
`400` — `MissingTenant` (header de tenant ausente) ou `InvalidRequest`
(JSON inválido, **campo desconhecido**, corpo acima de 1 MiB, mais de um
documento JSON no corpo).
401
`401 Unauthorized` — `X-Asender-Svc-Token` ausente, vazio ou incorreto.
Também é a resposta quando a auth está ligada e o `SERVICE_SECRET` está
vazio (fail closed).
422
`422 ValidationError` — query malformada, `cursor`/`limit` não numérico
ou não positivo, canal/status fora da allowlist, contato sem email nem
telefone, plataforma inválida, variável de template faltando (a mensagem
NOMEIA as faltantes), lote vazio ou acima do teto.
Exemplo
curl -X GET 'http://asender-messages:8004/v1/push/devices' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/push/devices
Registra ou reativa um token de push (upsert por tenant+token).
201 quando o token é novo, 200 quando já existia e foi atualizado (plataforma/contato + last_seen_at). É o que o app faz a cada abertura.
Onde é usada
chamado pelo asender-api ao servir /v1/push/devices (API key do app) e o cadastro de device do dashboard. Fluxo do dado: app → asender-api → aqui → messages.push_devices → o push-worker lê o token na entrega.
Saídas
200 com device quando o token já existia, 201 quando é novo; 422 se o token faltar ou a plataforma não for ios|android|web, ou se contact_id não existir no tenant; 400 em JSON inválido.
Efeitos
escreve em messages.push_devices.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
sim
**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a
partir do token verificado. Ausente → 400 `MissingTenant`.
Respostas
código
significado
200
Token já registrado; atualizado.
201
Novo device.
400
`400` — `MissingTenant` (header de tenant ausente) ou `InvalidRequest`
(JSON inválido, **campo desconhecido**, corpo acima de 1 MiB, mais de um
documento JSON no corpo).
401
`401 Unauthorized` — `X-Asender-Svc-Token` ausente, vazio ou incorreto.
Também é a resposta quando a auth está ligada e o `SERVICE_SECRET` está
vazio (fail closed).
422
`422 ValidationError` — query malformada, `cursor`/`limit` não numérico
ou não positivo, canal/status fora da allowlist, contato sem email nem
telefone, plataforma inválida, variável de template faltando (a mensagem
NOMEIA as faltantes), lote vazio ou acima do teto.
tela de audiência do dashboard, via asender-api. Fluxo do dado: header de tenant + query → repo → messages.contacts → JSON.
Entradas
q, list_id, cursor, limit. Query malformada é 422 (parseQuery), não listagem sem filtro.
Saídas
200 com contacts, next_cursor e has_more; 404 se list_id não existir no tenant; 422 em parâmetro inválido.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
sim
**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a
partir do token verificado. Ausente → 400 `MissingTenant`.
limit
query
não
Respostas
código
significado
200
OK
400
`400` — `MissingTenant` (header de tenant ausente) ou `InvalidRequest`
(JSON inválido, **campo desconhecido**, corpo acima de 1 MiB, mais de um
documento JSON no corpo).
401
`401 Unauthorized` — `X-Asender-Svc-Token` ausente, vazio ou incorreto.
Também é a resposta quando a auth está ligada e o `SERVICE_SECRET` está
vazio (fail closed).
422
`422 ValidationError` — query malformada, `cursor`/`limit` não numérico
ou não positivo, canal/status fora da allowlist, contato sem email nem
telefone, plataforma inválida, variável de template faltando (a mensagem
NOMEIA as faltantes), lote vazio ou acima do teto.
Exemplo
curl -X GET 'http://asender-messages:8004/v1/lists' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/lists
Cria (ou atualiza pelo slug) uma lista.
201 quando criou, 200 quando o slug já existia e foi atualizado. O status é a diferença observável — o seed e o retry do cliente não duplicam segmento.
Onde é usada
formulário "novo contato" da tela de audiência. Fluxo do dado: asender-api → aqui → messages.contacts → resposta 201.
Saídas
201 com contact; 400 em JSON inválido ou campo desconhecido; 422 em validação (sem email nem telefone, email malformado, email já usado).
Efeitos
escreve em messages.contacts.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
sim
**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a
partir do token verificado. Ausente → 400 `MissingTenant`.
Respostas
código
significado
200
Slug já existia; atualizada.
201
Criada.
400
`400` — `MissingTenant` (header de tenant ausente) ou `InvalidRequest`
(JSON inválido, **campo desconhecido**, corpo acima de 1 MiB, mais de um
documento JSON no corpo).
401
`401 Unauthorized` — `X-Asender-Svc-Token` ausente, vazio ou incorreto.
Também é a resposta quando a auth está ligada e o `SERVICE_SECRET` está
vazio (fail closed).
422
`422 ValidationError` — query malformada, `cursor`/`limit` não numérico
ou não positivo, canal/status fora da allowlist, contato sem email nem
telefone, plataforma inválida, variável de template faltando (a mensagem
NOMEIA as faltantes), lote vazio ou acima do teto.
detalhe da lista na tela de audiência e pré-visualização de audiência da campanha. Fluxo do dado: contact_list_members ⨝ contacts → asender-api → UI.
Entradas
cursor, limit.
Saídas
200 com contacts, next_cursor e has_more; 404 se a lista não existir no tenant; 422 em parâmetro inválido.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
sim
**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a
partir do token verificado. Ausente → 400 `MissingTenant`.
id
path
sim
cursor
query
não
limit
query
não
Respostas
código
significado
200
Página de contatos.
400
`400` — `MissingTenant` (header de tenant ausente) ou `InvalidRequest`
(JSON inválido, **campo desconhecido**, corpo acima de 1 MiB, mais de um
documento JSON no corpo).
401
`401 Unauthorized` — `X-Asender-Svc-Token` ausente, vazio ou incorreto.
Também é a resposta quando a auth está ligada e o `SERVICE_SECRET` está
vazio (fail closed).
404
`404 NotFound` — inexistente **ou de outro tenant**. A mensagem é sempre
`resource not found`, sem distinguir os dois casos.
422
`422 ValidationError` — query malformada, `cursor`/`limit` não numérico
ou não positivo, canal/status fora da allowlist, contato sem email nem
telefone, plataforma inválida, variável de template faltando (a mensagem
NOMEIA as faltantes), lote vazio ou acima do teto.
Exemplo
curl -X GET 'http://asender-messages:8004/v1/lists/id_AQUI/contacts' \
-H 'Authorization: Bearer SEU_TOKEN'
DELETE/v1/lists/{id}/members
Desassocia contatos da lista.
DELETE COM CORPO — o mesmo payload do POST. Não é REST canônico, é o contrato real: o cliente HTTP precisa enviar body num DELETE. O contato em si continua existindo.
Onde é usada
tela de audiência.
Saídas
200 com removed e not_found; 404 se a lista não existir; 422 em payload inválido.
Efeitos
apaga linhas de messages.contact_list_members. Idempotente.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
sim
**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a
partir do token verificado. Ausente → 400 `MissingTenant`.
id
path
sim
Respostas
código
significado
200
OK
400
`400` — `MissingTenant` (header de tenant ausente) ou `InvalidRequest`
(JSON inválido, **campo desconhecido**, corpo acima de 1 MiB, mais de um
documento JSON no corpo).
401
`401 Unauthorized` — `X-Asender-Svc-Token` ausente, vazio ou incorreto.
Também é a resposta quando a auth está ligada e o `SERVICE_SECRET` está
vazio (fail closed).
404
`404 NotFound` — inexistente **ou de outro tenant**. A mensagem é sempre
`resource not found`, sem distinguir os dois casos.
422
`422 ValidationError` — query malformada, `cursor`/`limit` não numérico
ou não positivo, canal/status fora da allowlist, contato sem email nem
telefone, plataforma inválida, variável de template faltando (a mensagem
NOMEIA as faltantes), lote vazio ou acima do teto.
Idempotente; duplicata é ignorada. not_found diz o que NÃO pôde ser feito.
Onde é usada
montagem da audiência de uma campanha, na tela de audiência. Fluxo do dado: asender-api → aqui → messages.contact_list_members.
Saídas
200 com added e not_found (ids que não existem no tenant — o servidor diz o que não pôde fazer em vez de descartar em silêncio); 404 se a lista não existir; 422 em lista vazia ou acima do teto.
Efeitos
escreve em messages.contact_list_members. Idempotente.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
sim
**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a
partir do token verificado. Ausente → 400 `MissingTenant`.
id
path
sim
Respostas
código
significado
200
OK
400
`400` — `MissingTenant` (header de tenant ausente) ou `InvalidRequest`
(JSON inválido, **campo desconhecido**, corpo acima de 1 MiB, mais de um
documento JSON no corpo).
401
`401 Unauthorized` — `X-Asender-Svc-Token` ausente, vazio ou incorreto.
Também é a resposta quando a auth está ligada e o `SERVICE_SECRET` está
vazio (fail closed).
404
`404 NotFound` — inexistente **ou de outro tenant**. A mensagem é sempre
`resource not found`, sem distinguir os dois casos.
422
`422 ValidationError` — query malformada, `cursor`/`limit` não numérico
ou não positivo, canal/status fora da allowlist, contato sem email nem
telefone, plataforma inválida, variável de template faltando (a mensagem
NOMEIA as faltantes), lote vazio ou acima do teto.
tela de audiência do dashboard, via asender-api. Fluxo do dado: header de tenant + query → repo → messages.contacts → JSON.
Entradas
q, list_id, cursor, limit. Query malformada é 422 (parseQuery), não listagem sem filtro.
Saídas
200 com contacts, next_cursor e has_more; 404 se list_id não existir no tenant; 422 em parâmetro inválido.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
sim
**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a
partir do token verificado. Ausente → 400 `MissingTenant`.
X-Asender-Tenant-Scope
header
não
Rollup de subárvore, separado por vírgula. Quando presente SUBSTITUI o
escopo de um tenant só. Teto de 500 ids (acima disso, 422): um bug no
chamador viraria um `= ANY(...)` de milhares de elementos.
channel
query
não
status
query
não
q
query
não
Busca. `%` e `_` são escapados (`?q=%` devolve 0, não tudo).
cursor
query
não
limit
query
não
**DIVERGÊNCIA:** pedir acima do teto do repositório cai no default em
SILÊNCIO (pede 101, recebe 25/50) em vez de 422
(RELATORIO-FINAL §3 item 11).
Respostas
código
significado
200
OK
400
`400` — `MissingTenant` (header de tenant ausente) ou `InvalidRequest`
(JSON inválido, **campo desconhecido**, corpo acima de 1 MiB, mais de um
documento JSON no corpo).
401
`401 Unauthorized` — `X-Asender-Svc-Token` ausente, vazio ou incorreto.
Também é a resposta quando a auth está ligada e o `SERVICE_SECRET` está
vazio (fail closed).
422
`422 ValidationError` — query malformada, `cursor`/`limit` não numérico
ou não positivo, canal/status fora da allowlist, contato sem email nem
telefone, plataforma inválida, variável de template faltando (a mensagem
NOMEIA as faltantes), lote vazio ou acima do teto.
500
`500 InternalError` — falha genuína de infra. O detalhe fica no log; o
corpo é sempre `unexpected error`, sem SQL, driver ou stack trace.
**Input do cliente nunca deve chegar aqui** (a exceção conhecida é
`POST /v1/messages/{id}/status` com `sending`).
Exemplo
curl -X GET 'http://asender-messages:8004/v1/messages' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/messages
Enfileira uma mensagem (grava mensagem + outbox no mesmo commit).
Uma mensagem POR destinatário.to com N endereços cria N linhas e devolve N objetos em messages.
Idempotência de verdade (ao contrário da borda pública): o header Idempotency-Key tem PRECEDÊNCIA sobre o campo idempotency_key do corpo. Replay não cria nada e responde 200 com reused_idempotency:true; criação responde 201. O status é a diferença observável entre "criei" e "já existia".
Com template informado, variável declarada no template e não passada em variables é 422 nomeando as faltantes — o placeholder não vaza para o destinatário. A renderização é varredura única (sem expansão de segundo nível) e determinística.
Onde é usada
chamado pelo asender-api ao servir /v1/emails/send, /v1/push/send e o composer do dashboard. Fluxo do dado: asender-api → aqui → application.SendMessage → banco (+outbox).
Saídas
201 com a lista de mensagens; 200 quando foi replay de idempotency key (nada novo criado); 422 em validação; 400 em JSON inválido.
Efeitos
escrita transacional no banco.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
sim
**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a
partir do token verificado. Ausente → 400 `MissingTenant`.
Idempotency-Key
header
não
Implementado de fato. Precede o campo do corpo.
Respostas
código
significado
200
Replay de `idempotency_key` — nada novo foi criado.
201
Criado.
400
`400` — `MissingTenant` (header de tenant ausente) ou `InvalidRequest`
(JSON inválido, **campo desconhecido**, corpo acima de 1 MiB, mais de um
documento JSON no corpo).
401
`401 Unauthorized` — `X-Asender-Svc-Token` ausente, vazio ou incorreto.
Também é a resposta quando a auth está ligada e o `SERVICE_SECRET` está
vazio (fail closed).
422
`422 ValidationError` — query malformada, `cursor`/`limit` não numérico
ou não positivo, canal/status fora da allowlist, contato sem email nem
telefone, plataforma inválida, variável de template faltando (a mensagem
NOMEIA as faltantes), lote vazio ou acima do teto.
500
`500 InternalError` — falha genuína de infra. O detalhe fica no log; o
corpo é sempre `unexpected error`, sem SQL, driver ou stack trace.
**Input do cliente nunca deve chegar aqui** (a exceção conhecida é
`POST /v1/messages/{id}/status` com `sending`).
200 com message e events; 404 se não existir 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
sim
**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a
partir do token verificado. Ausente → 400 `MissingTenant`.
id
path
sim
Respostas
código
significado
200
OK
400
`400` — `MissingTenant` (header de tenant ausente) ou `InvalidRequest`
(JSON inválido, **campo desconhecido**, corpo acima de 1 MiB, mais de um
documento JSON no corpo).
401
`401 Unauthorized` — `X-Asender-Svc-Token` ausente, vazio ou incorreto.
Também é a resposta quando a auth está ligada e o `SERVICE_SECRET` está
vazio (fail closed).
404
`404 NotFound` — inexistente **ou de outro tenant**. A mensagem é sempre
`resource not found`, sem distinguir os dois casos.
422
`422 ValidationError` — query malformada, `cursor`/`limit` não numérico
ou não positivo, canal/status fora da allowlist, contato sem email nem
telefone, plataforma inválida, variável de template faltando (a mensagem
NOMEIA as faltantes), lote vazio ou acima do teto.
Exemplo
curl -X GET 'http://asender-messages:8004/v1/messages/id_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/messages/{id}/status
Transição de status vinda de um worker.
Chamado pelos workers de email/sms/push depois de falar com o provedor. Idempotente: reprocessar não duplica evento.
Onde é usada
chamado pelos workers de email/sms/push depois de falar com o provedor. Fluxo do dado: worker → aqui → messages + message_events → relatório/timeline.
Saídas
204 em sucesso (não há corpo útil a devolver); 404 se a mensagem não existir no tenant; 422 em status desconhecido.
Efeitos
escrita transacional. Idempotente: reprocessar não duplica evento.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
sim
**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a
partir do token verificado. Ausente → 400 `MissingTenant`.
id
path
sim
Respostas
código
significado
204
Aplicado. Sem corpo — não há o que devolver.
400
`400` — `MissingTenant` (header de tenant ausente) ou `InvalidRequest`
(JSON inválido, **campo desconhecido**, corpo acima de 1 MiB, mais de um
documento JSON no corpo).
401
`401 Unauthorized` — `X-Asender-Svc-Token` ausente, vazio ou incorreto.
Também é a resposta quando a auth está ligada e o `SERVICE_SECRET` está
vazio (fail closed).
404
`404 NotFound` — inexistente **ou de outro tenant**. A mensagem é sempre
`resource not found`, sem distinguir os dois casos.
422
`422 ValidationError` — query malformada, `cursor`/`limit` não numérico
ou não positivo, canal/status fora da allowlist, contato sem email nem
telefone, plataforma inválida, variável de template faltando (a mensagem
NOMEIA as faltantes), lote vazio ou acima do teto.
500
Falha de infra **ou** `status:"sending"` (ver `x-asender-divergence`).
curl -X GET 'http://asender-messages:8004/healthz' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/metrics
Métricas no formato de exposição do Prometheus (§17).
Séries RED por handler (http_server_request_duration_seconds, http_server_errors_total, http_server_active_requests), do cliente de banco (db_client_operation_duration_seconds) e do publisher de outbox (outbox_messages_published_total, outbox_messages_failed_total).
Fora do /v1 e, portanto, fora do ServiceAuth: é scrape de infraestrutura (Alloy/Prometheus dentro da rede), não superfície de cliente. O serviço publica só em 127.0.0.1:8004, então o endpoint não é alcançável de fora do host. Não expõe dado de tenant: os rótulos são nome de rota (padrão do chi, nunca o path cru), método, código de status e verbo SQL.
Disponível SEM coletor OTLP configurado — é o que mantém as métricas observáveis num ambiente que ainda não tem a stack LGTM.
Respostas
código
significado
200
Exposição em texto do Prometheus.
503
O reader de métricas não pôde ser criado no boot.
Exemplo
curl -X GET 'http://asender-messages:8004/metrics' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/readyz
Readiness com verificação REAL (ping no Postgres).
Broker fora não derruba a readiness: o serviço segue aceitando escrita e o outbox acumula (degradação graciosa). O sinal correto é nats:false com outbox_pending subindo.
Onde é usada
probe de readiness; consumido pelo smoke test, que assere o conteúdo.
Saídas
200 quando o banco responde; 503 quando não. O broker fora não derruba a readiness — o serviço segue aceitando escrita e o outbox acumula (degradação graciosa, §2); isso aparece como nats:false e outbox_pending crescendo, que é o sinal correto para alerta.
Efeitos
uma query de ping no Postgres.
Respostas
código
significado
200
Banco respondeu.
503
Banco fora.
Exemplo
curl -X GET 'http://asender-messages:8004/readyz' \
-H 'Authorization: Bearer SEU_TOKEN'
reports
GET/v1/reports/overview
Funil do período, com os três canais sempre presentes.
Canal sem tráfego vem ZERADO, não ausente — chave faltando quebra o front. Os totais gerais são a soma dos canais (soma SATURANTE: int64 estourando viraria volume negativo em silêncio).
Onde é usada
cartões e taxas da tela de relatórios do dashboard, via asender-api GET /api/reports/overview. Fluxo do dado: header de tenant + query → repo.ReportsRepo.Overview → messages.messages/messages.message_events → JSON → asender-api → dashboard.
Entradas
from, to (ISO-8601; default últimos 30 dias).
Saídas
200 com totals, rates, by_channel e period; 400 sem header de tenant; 422 em data inválida, from > to ou janela acima de 366 dias.
Efeitos
só leitura.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
sim
**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a
partir do token verificado. Ausente → 400 `MissingTenant`.
X-Asender-Tenant-Scope
header
não
Rollup de subárvore, separado por vírgula. Quando presente SUBSTITUI o
escopo de um tenant só. Teto de 500 ids (acima disso, 422): um bug no
chamador viraria um `= ANY(...)` de milhares de elementos.
from
query
não
RFC 3339 ou `YYYY-MM-DD`. Default: 30 dias antes de `to`.
to
query
não
RFC 3339 ou `YYYY-MM-DD`. Default: agora. **Fim EXCLUSIVO**; data pura
avança para a meia-noite seguinte, cobrindo o dia inteiro.
Respostas
código
significado
200
OK
400
`400` — `MissingTenant` (header de tenant ausente) ou `InvalidRequest`
(JSON inválido, **campo desconhecido**, corpo acima de 1 MiB, mais de um
documento JSON no corpo).
401
`401 Unauthorized` — `X-Asender-Svc-Token` ausente, vazio ou incorreto.
Também é a resposta quando a auth está ligada e o `SERVICE_SECRET` está
vazio (fail closed).
422
`422 ValidationError` — data ilegível, `from > to`, janela acima de 366
dias (31 dias com `interval=hour`), `interval` fora de `day|hour`,
`channel` desconhecido, `limit` não positivo, escopo de rollup vazio ou
acima de 500 tenants.
500
`500 InternalError` — falha genuína de infra. O detalhe fica no log; o
corpo é sempre `unexpected error`, sem SQL, driver ou stack trace.
**Input do cliente nunca deve chegar aqui** (a exceção conhecida é
`POST /v1/messages/{id}/status` com `sending`).
Exemplo
curl -X GET 'http://asender-messages:8004/v1/reports/overview' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/reports/timeseries
Série do período, buckets vazios inclusos.
Um ponto por (bucket, canal). Sem channel, sai a série dos três. O campo do bucket chama-se day mesmo com interval=hour — o contrato com o front é um só; a granularidade vem em period.interval.
interval aqui aceita SÓ day|hour. O BFF /api/reports/timeseries do asender-api documenta hour|day|week|month: week e month passam lá e são recusados com 422 aqui. Divergência real de allowlist entre as duas bordas.
Onde é usada
gráfico SVG da tela de relatórios, via asender-api GET /api/reports/timeseries. Fluxo do dado: query → repo.ReportsRepo.Timeseries (generate_series + LEFT JOIN) → JSON → asender-api → gráfico.
200 com points e period; 400 sem header de tenant; 422 em canal desconhecido, intervalo fora da allowlist, data inválida ou janela grande demais (366 dias no geral, 31 dias com interval=hour).
Efeitos
só leitura.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
sim
**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a
partir do token verificado. Ausente → 400 `MissingTenant`.
X-Asender-Tenant-Scope
header
não
Rollup de subárvore, separado por vírgula. Quando presente SUBSTITUI o
escopo de um tenant só. Teto de 500 ids (acima disso, 422): um bug no
chamador viraria um `= ANY(...)` de milhares de elementos.
from
query
não
RFC 3339 ou `YYYY-MM-DD`. Default: 30 dias antes de `to`.
to
query
não
RFC 3339 ou `YYYY-MM-DD`. Default: agora. **Fim EXCLUSIVO**; data pura
avança para a meia-noite seguinte, cobrindo o dia inteiro.
channel
query
não
interval
query
não
Com `hour` a janela não pode passar de 31 dias (senão 422).
Respostas
código
significado
200
OK
400
`400` — `MissingTenant` (header de tenant ausente) ou `InvalidRequest`
(JSON inválido, **campo desconhecido**, corpo acima de 1 MiB, mais de um
documento JSON no corpo).
401
`401 Unauthorized` — `X-Asender-Svc-Token` ausente, vazio ou incorreto.
Também é a resposta quando a auth está ligada e o `SERVICE_SECRET` está
vazio (fail closed).
422
`422 ValidationError` — data ilegível, `from > to`, janela acima de 366
dias (31 dias com `interval=hour`), `interval` fora de `day|hour`,
`channel` desconhecido, `limit` não positivo, escopo de rollup vazio ou
acima de 500 tenants.
500
`500 InternalError` — falha genuína de infra. O detalhe fica no log; o
corpo é sempre `unexpected error`, sem SQL, driver ou stack trace.
**Input do cliente nunca deve chegar aqui** (a exceção conhecida é
`POST /v1/messages/{id}/status` com `sending`).
Exemplo
curl -X GET 'http://asender-messages:8004/v1/reports/timeseries' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/reports/top-templates
Ranking de templates por volume, com taxas.
Mensagem sem template não entra. limit default 10, teto REAL 50 (aplicado no repositório).
Onde é usada
tabela de templates da tela de relatórios, via asender-api GET /api/reports/top-templates. Fluxo do dado: query → repo.ReportsRepo.TopTemplates (messages ⋈ templates) → JSON → asender-api → tabela.
Entradas
from, to, limit (default 10, teto 50 aplicado no repositório).
Saídas
200 com templates (array, nunca null) e period; 400 sem header de tenant; 422 em limit não numérico ou não positivo e em janela inválida.
Efeitos
só leitura.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
sim
**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a
partir do token verificado. Ausente → 400 `MissingTenant`.
X-Asender-Tenant-Scope
header
não
Rollup de subárvore, separado por vírgula. Quando presente SUBSTITUI o
escopo de um tenant só. Teto de 500 ids (acima disso, 422): um bug no
chamador viraria um `= ANY(...)` de milhares de elementos.
from
query
não
RFC 3339 ou `YYYY-MM-DD`. Default: 30 dias antes de `to`.
to
query
não
RFC 3339 ou `YYYY-MM-DD`. Default: agora. **Fim EXCLUSIVO**; data pura
avança para a meia-noite seguinte, cobrindo o dia inteiro.
limit
query
não
Respostas
código
significado
200
OK
400
`400` — `MissingTenant` (header de tenant ausente) ou `InvalidRequest`
(JSON inválido, **campo desconhecido**, corpo acima de 1 MiB, mais de um
documento JSON no corpo).
401
`401 Unauthorized` — `X-Asender-Svc-Token` ausente, vazio ou incorreto.
Também é a resposta quando a auth está ligada e o `SERVICE_SECRET` está
vazio (fail closed).
422
`422 ValidationError` — data ilegível, `from > to`, janela acima de 366
dias (31 dias com `interval=hour`), `interval` fora de `day|hour`,
`channel` desconhecido, `limit` não positivo, escopo de rollup vazio ou
acima de 500 tenants.
500
`500 InternalError` — falha genuína de infra. O detalhe fica no log; o
corpo é sempre `unexpected error`, sem SQL, driver ou stack trace.
**Input do cliente nunca deve chegar aqui** (a exceção conhecida é
`POST /v1/messages/{id}/status` com `sending`).
Exemplo
curl -X GET 'http://asender-messages:8004/v1/reports/top-templates' \
-H 'Authorization: Bearer SEU_TOKEN'
templates
GET/v1/templates
Templates do tenant.
O corpo dos templates contém {{placeholders}} LITERAIS por definição.
Onde é usada
tela de audiência do dashboard, via asender-api. Fluxo do dado: header de tenant + query → repo → messages.contacts → JSON.
Entradas
q, list_id, cursor, limit. Query malformada é 422 (parseQuery), não listagem sem filtro.
Saídas
200 com contacts, next_cursor e has_more; 404 se list_id não existir no tenant; 422 em parâmetro inválido.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
sim
**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a
partir do token verificado. Ausente → 400 `MissingTenant`.
limit
query
não
Respostas
código
significado
200
OK
400
`400` — `MissingTenant` (header de tenant ausente) ou `InvalidRequest`
(JSON inválido, **campo desconhecido**, corpo acima de 1 MiB, mais de um
documento JSON no corpo).
401
`401 Unauthorized` — `X-Asender-Svc-Token` ausente, vazio ou incorreto.
Também é a resposta quando a auth está ligada e o `SERVICE_SECRET` está
vazio (fail closed).
422
`422 ValidationError` — query malformada, `cursor`/`limit` não numérico
ou não positivo, canal/status fora da allowlist, contato sem email nem
telefone, plataforma inválida, variável de template faltando (a mensagem
NOMEIA as faltantes), lote vazio ou acima do teto.
500
`500 InternalError` — falha genuína de infra. O detalhe fica no log; o
corpo é sempre `unexpected error`, sem SQL, driver ou stack trace.
**Input do cliente nunca deve chegar aqui** (a exceção conhecida é
`POST /v1/messages/{id}/status` com `sending`).
Exemplo
curl -X GET 'http://asender-messages:8004/v1/templates' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/templates
Cria ou atualiza pelo par (tenant, slug).
Responde 200 sempre, tanto na criação quanto na atualização.
Onde é usada
chamado pelo asender-api ao servir /v1/push/devices (API key do app) e o cadastro de device do dashboard. Fluxo do dado: app → asender-api → aqui → messages.push_devices → o push-worker lê o token na entrega.
Saídas
200 com device quando o token já existia, 201 quando é novo; 422 se o token faltar ou a plataforma não for ios|android|web, ou se contact_id não existir no tenant; 400 em JSON inválido.
Efeitos
escreve em messages.push_devices.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
sim
**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a
partir do token verificado. Ausente → 400 `MissingTenant`.
Respostas
código
significado
200
Gravado.
400
`400` — `MissingTenant` (header de tenant ausente) ou `InvalidRequest`
(JSON inválido, **campo desconhecido**, corpo acima de 1 MiB, mais de um
documento JSON no corpo).
401
`401 Unauthorized` — `X-Asender-Svc-Token` ausente, vazio ou incorreto.
Também é a resposta quando a auth está ligada e o `SERVICE_SECRET` está
vazio (fail closed).
422
`422 ValidationError` — query malformada, `cursor`/`limit` não numérico
ou não positivo, canal/status fora da allowlist, contato sem email nem
telefone, plataforma inválida, variável de template faltando (a mensagem
NOMEIA as faltantes), lote vazio ou acima do teto.
500
`500 InternalError` — falha genuína de infra. O detalhe fica no log; o
corpo é sempre `unexpected error`, sem SQL, driver ou stack trace.
**Input do cliente nunca deve chegar aqui** (a exceção conhecida é
`POST /v1/messages/{id}/status` com `sending`).
uma escrita. Mudar a configuração ACALMA o alerta: ele passa a observar outra coisa, e manter o estado faria a próxima avaliação comparar maçã com laranja — ele não avisaria pela condição NOVA, porque já estaria "disparado" pela antiga.
uma leitura. Da CONTA, e não de um alerta: quem investiga "o que aconteceu ontem" quer a linha do tempo inteira, e paginar por alerta a obrigaria a abrir um por um.
Exemplo
curl -X GET 'http://asender-crm:8005/v1/alertas/eventos'
configuracoes
GET/v1/configuracoes/retencaosem schema na spec
GET /v1/configuracoes/retencao.
Onde é usada
tela de configurações da conta.
Efeitos
uma leitura. Conta sem política configurada responde 200 com dias: null — e não 404: "não configurei retenção" é o estado NORMAL, e a tela precisa dele para desenhar o formulário vazio.
Exemplo
curl -X GET 'http://asender-crm:8005/v1/configuracoes/retencao'
PUT/v1/configuracoes/retencaosem schema na spec
PUT /v1/configuracoes/retencao com `{"dias": 90}` ou `{"dias": null}`.
Onde é usada
tela de configurações da conta.
Efeitos
uma escrita; passa a anonimizar contato antigo em até uma hora. # null desliga, ausente é erro Os dois cairiam no mesmo ponteiro nil se o corpo fosse decodificado direto — e um cliente que esquecesse o campo desligaria a retenção sem querer. null é uma ORDEM ("guarde para sempre"); campo ausente é um pedido malformado.
uma leitura. Responde TODOS os canais conhecidos, inclusive os sem opt-out. Devolver só os que saíram faria a tela ter de saber a lista completa para desenhar as caixas — e a primeira que ficasse desatualizada esconderia um canal do operador.
Exemplo
curl -X GET 'http://asender-crm:8005/v1/contatos/id_AQUI'
PATCH/v1/contatos/{id}sem schema na spec
PUT /v1/alertas/{id}.
Onde é usada
tela de alertas.
Efeitos
uma escrita. Mudar a configuração ACALMA o alerta: ele passa a observar outra coisa, e manter o estado faria a próxima avaliação comparar maçã com laranja — ele não avisaria pela condição NOVA, porque já estaria "disparado" pela antiga.
GET/v1/contatos/{id}/consentimentosem schema na spec
GET /v1/contatos/{id}/consentimento.
Onde é usada
tela de contato, seção de preferências.
Efeitos
uma leitura. Responde TODOS os canais conhecidos, inclusive os sem opt-out. Devolver só os que saíram faria a tela ter de saber a lista completa para desenhar as caixas — e a primeira que ficasse desatualizada esconderia um canal do operador.
Exemplo
curl -X GET 'http://asender-crm:8005/v1/contatos/id_AQUI/consentimento'
PUT/v1/contatos/{id}/consentimentosem schema na spec
PUT /v1/contatos/{id}/consentimento.
Onde é usada
tela de contato, e o link de descadastro via BFF.
Efeitos
cria ou remove a linha de opt-out. # recebe é ponteiro, e a ausência é ERRO Com um bool comum, um corpo sem o campo viria como false — e "esqueci de mandar o campo" seria indistinguível de "descadastra esta pessoa". O ponteiro transforma a omissão em 422 explícito.
uma leitura; decifra a PII; registra a exportação na timeline. # A PII é decifrada AQUI, e só para quem é dono O tenant vem da sessão verificada e a RLS já filtra a consulta. Este handler nunca vê contato de outra conta, e a decifragem acontece com a chave do processo — não há caminho em que o CSV carregue dado alheio. # Cada exportação vira uma ATIVIDADE, e isso não é opcional A LGPD (art. 37) exige registro das operações de tratamento, e exportar é tratamento: a partir dali o dado existe num arquivo que ninguém controla. Sem esse registro, "quem baixou a base e quando" não tem resposta — e é a primeira pergunta de todo incidente de vazamento. O registro é gravado ANTES do arquivo sair. Se a gravação falhar, a exportação não acontece: um download sem trilha é exatamente o que a trilha existe para impedir, e "o arquivo já foi" não se desfaz.
Exemplo
curl -X GET 'http://asender-crm:8005/v1/contatos/exportar.csv'
uma escrita. Mudar a configuração ACALMA o alerta: ele passa a observar outra coisa, e manter o estado faria a próxima avaliação comparar maçã com laranja — ele não avisaria pela condição NOVA, porque já estaria "disparado" pela antiga.
uma leitura. O PAYLOAD não volta: ele carrega a PII do contato, e quem pergunta "por que não chegou" precisa do estado e do código — não do dado da pessoa repetido numa segunda tela.
Exemplo
curl -X GET 'http://asender-crm:8005/v1/destinos/id_AQUI/entregas'
POST/v1/destinos/{id}/testarsem schema na spec
POST /v1/destinos/{id}/testar.
Onde é usada
botão "testar" da tela de integrações.
Efeitos
cria uma entrega pendente. # ENFILEIRA, e não entrega na hora A entrega passa pelo MESMO dreno das entregas de verdade — e portanto pelo mesmo guard do piso 18, pela mesma guarda de rede e pela mesma assinatura. Um caminho de teste que chamasse o webhook direto seria um segundo caminho de saída, e o segundo caminho é sempre o que esquece uma guarda. O operador vê o resultado em GET /entregas, que é a mesma tela onde ele vê as entregas reais — então "o teste funcionou" e "a integração funciona" são a mesma leitura, e não duas afirmações que podem divergir.
PUT/v1/integracoes-de-conversao/{id}sem schema na spec
PUT /v1/alertas/{id}.
Onde é usada
tela de alertas.
Efeitos
uma escrita. Mudar a configuração ACALMA o alerta: ele passa a observar outra coisa, e manter o estado faria a próxima avaliação comparar maçã com laranja — ele não avisaria pela condição NOVA, porque já estaria "disparado" pela antiga.
GET/v1/integracoes-de-conversao/{id}/entregassem schema na spec
GET /v1/destinos/{id}/entregas?limite=N.
Onde é usada
diagnóstico da tela de integrações.
Efeitos
uma leitura. O PAYLOAD não volta: ele carrega a PII do contato, e quem pergunta "por que não chegou" precisa do estado e do código — não do dado da pessoa repetido numa segunda tela.
Exemplo
curl -X GET 'http://asender-crm:8005/v1/integracoes-de-conversao/id_AQUI/entregas'
POST/v1/integracoes-de-conversao/{id}/testarsem schema na spec
POST /v1/destinos/{id}/testar.
Onde é usada
botão "testar" da tela de integrações.
Efeitos
cria uma entrega pendente. # ENFILEIRA, e não entrega na hora A entrega passa pelo MESMO dreno das entregas de verdade — e portanto pelo mesmo guard do piso 18, pela mesma guarda de rede e pela mesma assinatura. Um caminho de teste que chamasse o webhook direto seria um segundo caminho de saída, e o segundo caminho é sempre o que esquece uma guarda. O operador vê o resultado em GET /entregas, que é a mesma tela onde ele vê as entregas reais — então "o teste funcionou" e "a integração funciona" são a mesma leitura, e não duas afirmações que podem divergir.
uma leitura. Responde TODOS os canais conhecidos, inclusive os sem opt-out. Devolver só os que saíram faria a tela ter de saber a lista completa para desenhar as caixas — e a primeira que ficasse desatualizada esconderia um canal do operador.
Exemplo
curl -X GET 'http://asender-crm:8005/v1/jornadas/id_AQUI'
PUT/v1/jornadas/{id}/ativasem schema na spec
PUT /v1/jornadas/{id}/ativa.
Onde é usada
painel, interruptor da jornada.
Efeitos
muda o estado; ligada, a jornada passa a inscrever contatos. # Revalida ANTES de ligar A jornada foi validada na criação, mas pode ter sido editada depois — e uma jornada com buraco na numeração deixaria o contato preso no meio do caminho, sem erro em lugar nenhum. Validar de novo aqui custa nada e é o último ponto antes de a coisa começar a mandar mensagem. Desligar NÃO revalida: uma jornada quebrada tem de poder ser desligada, e exigir que ela esteja válida para parar seria prender o operador justamente no caso em que ele mais precisa do botão.
503 `starting` antes do fim do boot; depois disso, CONSULTA as sondas e responde 200 `ready` ou 503 `degraded` com o nome de cada dependência.
Onde é usada
rota pública /readyz.
Efeitos
escreve JSON na resposta; pode tocar a dependência (com cache de 1s). # Por que ele não é mais um trinco de boot A readiness era MarkReady() uma vez e pronto — memória do boot, não estado atual. Num incidente real de 2026-08-27 o serviço voltou com a credencial errada do Postgres, falhou ~30 vezes por minuto ao drenar o outbox, e /healthz e /readyz responderam 200 o tempo todo: o gate de saúde do deploy passou e quem descobriu foi o pentest, depois. Readiness que só lembra do boot não enxerga a dependência que caiu DEPOIS dele. /healthz continua sem tocar em nada, e isso é de propósito: liveness que consulta o banco derruba o processo quando o banco oscila. Quem tem de dizer "não me mande tráfego" é o readiness. O que sai na resposta é o NOME da dependência e "indisponível" — nunca a mensagem do driver, que carrega usuário, host e base. Rota pública não conta topologia; o erro inteiro vai para o log.
Exemplo
curl -X GET 'http://asender-crm:8005/readyz'
segmentos
GET/v1/segmentossem schema na spec
GET /v1/alertas.
Onde é usada
tela de alertas.
Efeitos
uma leitura.
Exemplo
curl -X GET 'http://asender-crm:8005/v1/segmentos'
uma leitura. Responde TODOS os canais conhecidos, inclusive os sem opt-out. Devolver só os que saíram faria a tela ter de saber a lista completa para desenhar as caixas — e a primeira que ficasse desatualizada esconderia um canal do operador.
Exemplo
curl -X GET 'http://asender-crm:8005/v1/segmentos/id_AQUI'
PUT/v1/segmentos/{id}sem schema na spec
PUT /v1/alertas/{id}.
Onde é usada
tela de alertas.
Efeitos
uma escrita. Mudar a configuração ACALMA o alerta: ele passa a observar outra coisa, e manter o estado faria a próxima avaliação comparar maçã com laranja — ele não avisaria pela condição NOVA, porque já estaria "disparado" pela antiga.
uma consulta; NÃO grava nada. Existe para que a pergunta "isto alcança quem?" seja respondida ANTES de salvar. Sem ela, o caminho para descobrir o alcance é criar o segmento — e a tela enche de segmentos descartados chamados "teste 3".
GET /v1/uso?periodo=YYYY-MM — números locais do CRM mais a quota e os contadores do core.
Onde é usada
tela de conta/plano.
Efeitos
uma leitura local e até duas chamadas ao core. # O core fora do ar NÃO derruba esta rota Os números locais (quantos contatos, quantos entraram no mês) são a metade que o CRM sabe sozinho, e são justamente a metade que a tela usa todo dia. Com o core fora, a resposta sai com core_disponivel: false — a tela mostra o que tem e diz o que não conseguiu saber (piso 10).
Exemplo
curl -X GET 'http://asender-crm:8005/v1/uso'
Pages — páginas, formulários e mídia
interno
Alcançável apenas pela malha privada — não é superfície pública.
http://asender-pages:8006 · 49 operações
configuracoes
GET/v1/configuracoes/marcasem schema na spec
GET /v1/paginas/{id}/experimento.
Onde é usada
tela de A/B do painel.
Efeitos
uma leitura. Página SEM experimento responde 200 com o corpo vazio de variantes, e não 404: "esta página não tem teste" é uma resposta sobre o mundo, e a tela precisa dela para mostrar o botão de criar. 404 mandaria a tela tratar o caso normal como erro.
Exemplo
curl -X GET 'http://asender-pages:8006/v1/configuracoes/marca'
PUT/v1/configuracoes/marcasem schema na spec
PUT /v1/configuracoes/marca — os quatro campos de uma vez.
Onde é usada
tela de marca do painel.
Efeitos
uma escrita; muda o que o visitante vê na próxima visita. PUT e não PATCH: são quatro campos que a tela mostra e salva juntos, e campo vazio APAGA aquele elemento. Um PATCH criaria a pergunta "como apago o logo?", cuja resposta seria um campo especial que ninguém acerta na primeira leitura.
curl -X GET 'http://asender-pages:8006/v1/dominios'
POST/v1/dominiossem schema na spec
POST /v1/dominios.
Onde é usada
tela de domínios.
Efeitos
uma escrita. # O domínio nasce NÃO verificado E é isso que impede o pior desfecho do produto: sem a prova de DNS, qualquer conta declararia banco.com.br e passaria a receber certificado TLS para esse nome — servindo uma página de captura no domínio de outra pessoa, com cadeado verde.
uma escrita. Apagar TIRA a autorização de TLS, e é o único caminho para isso — de propósito: um domínio verificado que pudesse ser "desverificado" por outro caminho sairia do ar sem ninguém entender por quê.
uma consulta DNS e uma escrita. # As duas falhas têm respostas DIFERENTES "Não consegui perguntar ao DNS" é 503 com "tente de novo"; "perguntei e o registro não está lá" é 422 com "publique o registro". Colapsar as duas faria o cliente ficar publicando um registro que já está lá. # E o resultado é GRAVADO nos dois casos Sem isso, quem tenta e falha vê a tela exatamente igual à de antes de tentar — e não tem como saber se o sistema chegou a olhar.
curl -X GET 'http://asender-pages:8006/v1/formularios'
POST/v1/formulariossem schema na spec
POST /v1/dominios.
Onde é usada
tela de domínios.
Efeitos
uma escrita. # O domínio nasce NÃO verificado E é isso que impede o pior desfecho do produto: sem a prova de DNS, qualquer conta declararia banco.com.br e passaria a receber certificado TLS para esse nome — servindo uma página de captura no domínio de outra pessoa, com cadeado verde.
uma escrita. Apagar TIRA a autorização de TLS, e é o único caminho para isso — de propósito: um domínio verificado que pudesse ser "desverificado" por outro caminho sairia do ar sem ninguém entender por quê.
uma leitura. Página SEM experimento responde 200 com o corpo vazio de variantes, e não 404: "esta página não tem teste" é uma resposta sobre o mundo, e a tela precisa dela para mostrar o botão de criar. 404 mandaria a tela tratar o caso normal como erro.
Exemplo
curl -X GET 'http://asender-pages:8006/v1/formularios/id_AQUI'
responde 200 enquanto o processo está vivo. Não toca dependência: um liveness que consulta o banco derruba o pod quando o banco oscila.
Onde é usada
rota pública /healthz.
Efeitos
escreve JSON na resposta.
Exemplo
curl -X GET 'http://asender-pages:8006/healthz'
ia
POST/v1/ia/gerar-paginasem schema na spec
POST /v1/paginas/gerar — manda o pedido ao provedor, saneia a saída e devolve o template.
Onde é usada
botão "gerar com IA" do editor.
Efeitos
uma chamada externa; NÃO grava nada. # Não grava: devolve o rascunho Gravar direto faria o modelo publicar na conta do cliente. O que sai daqui é uma sugestão que a pessoa vê no editor e salva se quiser — a versão continua nascendo do gesto dela. # Sem provedor, a rota DEGRADA 503 e não 500: é configuração ausente, e a distinção separa "o produto não tem essa função ligada" de "o produto quebrou". O resto do serviço segue de pé — nenhuma página deixa de ser servida porque não há chave de IA (piso 10).
GET/internal/v1/formularios/{id}sem schema na spec
GET /internal/v1/formularios/{id}.
Onde é usada
chamado pelo asender_runtime para validar uma submissão.
Efeitos
uma leitura. # Sem o tenant na rota, de propósito No modo SNIPPET o formulário está embutido no site de um terceiro e quem posta é o navegador de um visitante anônimo. O runtime não sabe de qual conta é o formulário — a conta é a RESPOSTA, tirada dele. Exigir o tenant na rota tornaria esta chamada impossível de fazer. Isto não afrouxa nada: o public_id é opaco e já está no HTML de quem visita o site. Quem o conhece já podia postar nele. O que protege esta rota é a credencial de serviço — sem ela, ela não responde nada.
Exemplo
curl -X GET 'http://asender-pages:8006/internal/v1/formularios/id_AQUI'
GET/internal/v1/paginassem schema na spec
GET /internal/v1/paginas?dominio=&slug=.
Onde é usada
chamado pelo asender_runtime a cada requisição de página pública.
Efeitos
uma leitura. # Query string e não caminho O domínio contém pontos e o slug pode conter hífen; os dois em segmentos de rota exigiriam escape que o roteador desfaz de formas diferentes conforme a versão. Query string é literal, e o que trafega aqui é rede interna.
Exemplo
curl -X GET 'http://asender-pages:8006/internal/v1/paginas'
POST/internal/v1/paginas/{id}/cliquessem schema na spec
POST /internal/v1/paginas/{id}/cliques.
Onde é usada
descarga periódica do contador em memória do asender_runtime.
Efeitos
uma escrita por destino, somando ao que já existe. # Soma, e não atribui O corpo traz o DELTA do lote, nunca um total. Dois processos do runtime descarregando ao mesmo tempo somam certo; com atribuição, o último a escrever apagaria a contagem do outro — e o relatório mostraria menos visitas do que houve, sem nada indicar a perda. # Teto de destinos no corpo O mapa vem da rede. Sem teto, um lote com um milhão de chaves viraria um milhão de INSERT numa transação só. O teto é o mesmo do roteamento (20 destinos), porque é o máximo que uma página pode ter.
GET/internal/v1/paginas/publicadassem schema na spec
GET /internal/v1/paginas/publicadas?dominio=.
Onde é usada
chamado pelo asender_runtime para montar /sitemap.xml.
Efeitos
uma leitura. Devolve só slug e data: o sitemap não precisa de título nem template, e cada campo a mais seria dado do cliente saindo por uma rota que qualquer visitante alcança pelo runtime. Domínio sem página responde lista VAZIA, e não 404: "este host não tem página publicada" é uma resposta sobre o mundo, e o runtime precisa dela para devolver um sitemap vazio em vez de um erro.
Exemplo
curl -X GET 'http://asender-pages:8006/internal/v1/paginas/publicadas'
GET/internal/v1/redirectssem schema na spec
GET /internal/v1/redirects?dominio=&slug=.
Onde é usada
chamado pelo asender_runtime a cada clique.
Efeitos
uma leitura. Não devolve o tenant_id: quem serve o redirect não precisa saber de quem ele é, e devolvê-lo daria a um visitante um mapa de qual conta encurta o quê.
Exemplo
curl -X GET 'http://asender-pages:8006/internal/v1/redirects'
POST/internal/v1/redirects/cliquessem schema na spec
POST /internal/v1/redirects/cliques.
Onde é usada
descarga periódica do contador em memória do runtime.
Efeitos
uma escrita por link. SOMA, e não atribui: dois processos do runtime descarregando ao mesmo tempo somam certo; com atribuição, o último a escrever apagaria a contagem do outro.
uma consulta. # Fail-closed, e a resposta é um booleano Host desconhecido é false — negar é a resposta correta. Falha nossa é 503, e NÃO false: o servidor de borda tem de distinguir "não autorizado" de "não consegui decidir", senão um soluço de banco viraria recusa de certificado para domínio legítimo. A resposta não carrega tenant nem token. Quem monta o handshake não precisa disso, e devolvê-lo daria a quem sonda um mapa de quais domínios pertencem à plataforma.
Exemplo
curl -X GET 'http://asender-pages:8006/internal/v1/tls/authorize'
midias
GET/v1/midiassem schema na spec
GET /v1/dominios.
Onde é usada
tela de domínios.
Efeitos
uma leitura.
Exemplo
curl -X GET 'http://asender-pages:8006/v1/midias'
POST/v1/midiassem schema na spec
POST /v1/midias (multipart, campo `arquivo`).
Onde é usada
botão de upload do editor.
Efeitos
escreve no disco e no banco. # A ordem é DISCO e depois BANCO Se o disco falhar, nada foi gravado e o erro é honesto. A ordem inversa deixaria uma linha apontando para um arquivo inexistente, e o visitante receberia erro numa página publicada. O caso duplicado não grava nada: a mídia já está lá, com os mesmos bytes.
uma escrita. Apagar TIRA a autorização de TLS, e é o único caminho para isso — de propósito: um domínio verificado que pudesse ser "desverificado" por outro caminho sairia do ar sem ninguém entender por quê.
uma requisição a servidor de terceiro; escreve no disco e no banco. # Esta é a rota mais perigosa do serviço Ela faz o SERVIDOR buscar um endereço que o USUÁRIO escolheu — a definição de SSRF. Sem as guardas, http://169.254.169.254/latest/meta-data/ devolveria as credenciais da instância, e http://redis-interno:6379/ alcançaria um serviço que nunca deveria ver tráfego de fora. Toda a defesa está em domain/midia: allowlist de esquema e porta, recusa de IP interno, conferência REPETIDA no momento de discar (contra DNS rebinding), zero redirecionamento e teto de tamanho durante a leitura.
curl -X GET 'http://asender-pages:8006/v1/paginas'
POST/v1/paginassem schema na spec
POST /v1/dominios.
Onde é usada
tela de domínios.
Efeitos
uma escrita. # O domínio nasce NÃO verificado E é isso que impede o pior desfecho do produto: sem a prova de DNS, qualquer conta declararia banco.com.br e passaria a receber certificado TLS para esse nome — servindo uma página de captura no domínio de outra pessoa, com cadeado verde.
uma leitura. Página SEM experimento responde 200 com o corpo vazio de variantes, e não 404: "esta página não tem teste" é uma resposta sobre o mundo, e a tela precisa dela para mostrar o botão de criar. 404 mandaria a tela tratar o caso normal como erro.
Exemplo
curl -X GET 'http://asender-pages:8006/v1/paginas/id_AQUI'
PATCH/v1/paginas/{id}sem schema na spec
PATCH /v1/paginas/{id} — renomear, mudar o endereço público ou o domínio.
Onde é usada
tela de páginas do painel (renomear) e configurações da página.
Efeitos
uma escrita. # Só o que veio é tocado nil é "não mandado" e &"" é "mandado vazio". Sem essa distinção, editar o título apagaria o endereço público da página — e ela sairia do ar sem ninguém ter pedido. # Validação IGUAL à da criação O slug e o título passam pelas mesmas regras do Criar. Um PATCH mais frouxo seria a porta dos fundos: bastaria criar válido e editar para inválido para pôr no ar um endereço que a criação recusa.
POST/v1/paginas/{id}/ab/promoversem schema na spec
POST /v1/paginas/{id}/ab/promover.
Onde é usada
botão "promover vencedora" e botão "iniciar teste" da tela de A/B.
Efeitos
uma escrita. # Duas operações na mesma rota, e por quê O lynz expõe POST /v1/pages/{id}/ab/promote, sem rota de iniciar — o teste começa ao ser promovido a "rodando". Manter uma rota só preserva a superfície do lynz, e o corpo diz qual das duas transições é pedida. # Iniciar EXIGE o experimento válido É o único ponto onde o rascunho inconsistente é barrado, e é o ponto certo: pesos que não somam 100 deixariam uma faixa do tráfego sem dono, e Escolher devolveria a última variante para essa fatia — um viés silencioso que o número final não denuncia.
uma leitura. Devolve o CONTADOR, e não uma lista de eventos: a pergunta é sempre "quantos", nunca "quais". Uma linha por clique transformaria o caminho mais quente do redirect num INSERT por requisição.
Exemplo
curl -X GET 'http://asender-pages:8006/v1/paginas/id_AQUI/cliques'
GET/v1/paginas/{id}/experimentosem schema na spec
GET /v1/paginas/{id}/experimento.
Onde é usada
tela de A/B do painel.
Efeitos
uma leitura. Página SEM experimento responde 200 com o corpo vazio de variantes, e não 404: "esta página não tem teste" é uma resposta sobre o mundo, e a tela precisa dela para mostrar o botão de criar. 404 mandaria a tela tratar o caso normal como erro.
Exemplo
curl -X GET 'http://asender-pages:8006/v1/paginas/id_AQUI/experimento'
PUT/v1/paginas/{id}/experimento/auto-stopsem schema na spec
PUT /v1/paginas/{id}/experimento/auto-stop.
Onde é usada
tela de A/B.
Efeitos
uma escrita. Aceito com o teste RODANDO, ao contrário das variantes: apertar o critério não reatribui visitante nem invalida amostra — só muda quando o job pode concluir.
DELETE/v1/paginas/{id}/roteamentosem schema na spec
DELETE /v1/dominios/{id}.
Onde é usada
tela de domínios.
Efeitos
uma escrita. Apagar TIRA a autorização de TLS, e é o único caminho para isso — de propósito: um domínio verificado que pudesse ser "desverificado" por outro caminho sairia do ar sem ninguém entender por quê.
uma leitura. Página SEM experimento responde 200 com o corpo vazio de variantes, e não 404: "esta página não tem teste" é uma resposta sobre o mundo, e a tela precisa dela para mostrar o botão de criar. 404 mandaria a tela tratar o caso normal como erro.
Exemplo
curl -X GET 'http://asender-pages:8006/v1/paginas/id_AQUI/roteamento'
curl -X GET 'http://asender-pages:8006/v1/paginas/id_AQUI/variantes'
POST/v1/paginas/{id}/variantessem schema na spec
POST /v1/paginas/{id}/variantes.
Onde é usada
botão "nova variante" da tela de A/B.
Efeitos
uma ou duas escritas. # O experimento nasce aqui, e em rascunho Exigir uma chamada de "criar experimento" antes da primeira variante daria ao operador um objeto vazio que não mede nada e que ele teria de lembrar de apagar. A primeira variante cria o teste.
abre e transmite o arquivo. # Sem sessão, e por isso com cuidado extra O id é o único parâmetro, e ele é resolvido no BANCO antes de tocar o disco: o caminho do arquivo nunca é montado com o que veio da URL. Path traversal não tem por onde entrar — e o armazenamento recusa id fora do formato de qualquer jeito, como segunda linha. # X-Content-Type-Options: nosniff é obrigatório aqui O tipo é o DETECTADO no upload, e a allowlist só tem imagem. Sem nosniff, o navegador pode reinterpretar o conteúdo e executar como outra coisa — e a allowlist de tipos perderia o sentido no último metro.
Exemplo
curl -X GET 'http://asender-pages:8006/v1/public/midias/id_AQUI'
readyz
GET/readyzsem schema na spec
503 `starting` antes do fim do boot; depois disso, CONSULTA as sondas e responde 200 `ready` ou 503 `degraded` com o nome de cada dependência.
Onde é usada
rota pública /readyz.
Efeitos
escreve JSON na resposta; pode tocar a dependência (com cache de 1s). # Por que ele não é mais um trinco de boot A readiness era MarkReady() uma vez e pronto — memória do boot, não estado atual. Num incidente real de 2026-08-27 o serviço voltou com a credencial errada do Postgres, falhou ~30 vezes por minuto ao drenar o outbox, e /healthz e /readyz responderam 200 o tempo todo: o gate de saúde do deploy passou e quem descobriu foi o pentest, depois. Readiness que só lembra do boot não enxerga a dependência que caiu DEPOIS dele. /healthz continua sem tocar em nada, e isso é de propósito: liveness que consulta o banco derruba o processo quando o banco oscila. Quem tem de dizer "não me mande tráfego" é o readiness. O que sai na resposta é o NOME da dependência e "indisponível" — nunca a mensagem do driver, que carrega usuário, host e base. Rota pública não conta topologia; o erro inteiro vai para o log.
Exemplo
curl -X GET 'http://asender-pages:8006/readyz'
redirects
GET/v1/redirectssem schema na spec
GET /v1/dominios.
Onde é usada
tela de domínios.
Efeitos
uma leitura.
Exemplo
curl -X GET 'http://asender-pages:8006/v1/redirects'
POST/v1/redirectssem schema na spec
POST /v1/dominios.
Onde é usada
tela de domínios.
Efeitos
uma escrita. # O domínio nasce NÃO verificado E é isso que impede o pior desfecho do produto: sem a prova de DNS, qualquer conta declararia banco.com.br e passaria a receber certificado TLS para esse nome — servindo uma página de captura no domínio de outra pessoa, com cadeado verde.
uma escrita. Apagar TIRA a autorização de TLS, e é o único caminho para isso — de propósito: um domínio verificado que pudesse ser "desverificado" por outro caminho sairia do ar sem ninguém entender por quê.
uma leitura. Página SEM experimento responde 200 com o corpo vazio de variantes, e não 404: "esta página não tem teste" é uma resposta sobre o mundo, e a tela precisa dela para mostrar o botão de criar. 404 mandaria a tela tratar o caso normal como erro.
Exemplo
curl -X GET 'http://asender-pages:8006/v1/redirects/id_AQUI'
uma leitura. Devolve o CONTADOR, e não uma lista de eventos: a pergunta é sempre "quantos", nunca "quais". Uma linha por clique transformaria o caminho mais quente do redirect num INSERT por requisição.
Exemplo
curl -X GET 'http://asender-pages:8006/v1/redirects/id_AQUI/cliques'
Runtime — publicação e analytics
interno
Alcançável apenas pela malha privada — não é superfície pública.
http://asender-runtime:8007 · 40 operações
{slug}
GET/{slug}sem schema na spec
resolve host+caminho na página publicada e devolve o HTML renderizado.
Onde é usada
rota curinga do runtime, a de maior tráfego do sistema.
Efeitos
consulta a fonte; escreve a resposta. 404 quando não há página: o host pode apontar para nós sem ter página naquele caminho, e devolver erro de servidor nesse caso confundiria monitoramento com tráfego normal de quem digitou a URL errada.
Exemplo
curl -X GET 'http://asender-runtime:8007/slug_AQUI'
POST/{slug}sem schema na spec
resolve a página pela mesma rota que a serviu, monta a submissão com `Modo = pagina` e chama o MESMO pipeline dos outros dois modos.
Onde é usada
POST no caminho da própria página.
Efeitos
os do pipeline (banco, outbox). O tenant e o form vêm da PÁGINA resolvida no servidor, nunca do corpo: o form da página é HTML público, e qualquer um pode reenviá-lo com outro form_id. Resolver pelo host+caminho é o que impede postar no formulário de outro tenant.
curl -X GET 'http://asender-runtime:8007/v1/analytics/ab'
GET/v1/analytics/breakdownsem schema na spec
GET /v1/analytics/breakdown?dimensao=utm_source&…
Onde é usada
tela de origens.
Efeitos
uma leitura.
Exemplo
curl -X GET 'http://asender-runtime:8007/v1/analytics/breakdown'
GET/v1/analytics/eventssem schema na spec
GET /v1/analytics/events?pagina=&limite=.
Onde é usada
tela de depuração da instrumentação.
Efeitos
uma leitura.
Exemplo
curl -X GET 'http://asender-runtime:8007/v1/analytics/events'
GET/v1/analytics/heatmapsem schema na spec
GET /v1/analytics/heatmap?pagina=&lado=.
Onde é usada
tela de mapa de calor.
Efeitos
uma leitura. # As posições são PORCENTAGEM, e não pixel Pixel absoluto é da tela de quem clicou: o mesmo botão sai em x=320 no celular e x=980 no monitor. A célula em porcentagem compara telas diferentes — que é a única pergunta que um mapa de calor responde.
Exemplo
curl -X GET 'http://asender-runtime:8007/v1/analytics/heatmap'
GET/v1/analytics/livesem schema na spec
GET /v1/analytics/live — sessões e eventos da janela curta.
Onde é usada
cabeçalho da tela ao vivo, no primeiro carregamento.
Efeitos
uma leitura. Existe além do stream porque quem abre a tela precisa ver ALGO antes do primeiro evento chegar. Sem isso, uma conta com movimento baixo mostraria tela vazia por minutos e pareceria quebrada.
Exemplo
curl -X GET 'http://asender-runtime:8007/v1/analytics/live'
GET/v1/analytics/live/streamsem schema na spec
GET /v1/analytics/live/stream — Server-Sent Events.
Onde é usada
tela ao vivo.
Efeitos
mantém uma conexão aberta e uma inscrição no hub. # SSE, e não WebSocket O fluxo é de mão única (servidor → tela) e o cliente é um navegador. SSE atravessa proxy comum, reconecta sozinho e cabe em quatro linhas de JS; WebSocket traria um protocolo inteiro para carregar dado que só desce.
Exemplo
curl -X GET 'http://asender-runtime:8007/v1/analytics/live/stream'
GET/v1/analytics/overviewsem schema na spec
GET /v1/analytics/overview?pagina=&de=&ate=.
Onde é usada
tela inicial de analytics.
Efeitos
duas leituras. Funil e atribuição juntos porque a tela mostra os dois lado a lado, e duas requisições poderiam ler janelas diferentes — o total da atribuição não fecharia com o topo do funil, e o número "errado" seria o certo.
Exemplo
curl -X GET 'http://asender-runtime:8007/v1/analytics/overview'
GET/v1/analytics/session/{id}sem schema na spec
GET /v1/analytics/session/{id}.
Onde é usada
tela de sessão — o que aquela visita fez, em ordem.
Efeitos
uma leitura. Sessão de outra conta responde lista VAZIA, e não 404: o id de sessão é opaco e gerado no browser, e distinguir "não existe" de "não é sua" transformaria a rota num oráculo de existência de sessão alheia.
Exemplo
curl -X GET 'http://asender-runtime:8007/v1/analytics/session/id_AQUI'
GET/v1/analytics/traffic-qualitysem schema na spec
GET /v1/analytics/traffic-quality.
Onde é usada
tela de qualidade — "esse tráfego pago é gente?".
Efeitos
uma leitura.
Exemplo
curl -X GET 'http://asender-runtime:8007/v1/analytics/traffic-quality'
collect
POST/collectsem schema na spec
POST /collect — abre a credencial, normaliza cada evento e publica.
Onde é usada
rota pública, a de maior volume do sistema depois da própria página.
Efeitos
publica no broker; NUNCA escreve no banco. # Responde 204 quase sempre, e isso é deliberado Credencial inválida é 403 (o cliente precisa saber que aquele HTML está velho). Fora isso — evento inválido, tipo desconhecido, lote parcialmente ruim — a resposta é 204 e o que dava para aproveitar foi publicado. O pixel roda no browser de terceiros: transformar um evento malformado em erro visível não conserta nada e enche o console de quem só quer ver a página.
valida a ORIGEM contra os domínios verificados do tenant, ecoa os cabeçalhos de CORS e chama o mesmo pipeline.
Onde é usada
rota pública, chamada de outro domínio pelo navegador.
Efeitos
os do pipeline. Aqui não há API key: quem chama é o navegador do visitante, e uma key no snippet seria segredo no cliente (§13.4). A defesa é a allowlist de origem, verificada NO SERVIDOR — o header Origin é posto pelo navegador e não pode ser forjado por script da própria página.
POST/v1/forms/{formID}/submissionssem schema na spec
autentica por API key (resolvida pelo middleware), lê o JSON e chama o pipeline.
Onde é usada
rota autenticada do runtime.
Efeitos
os do pipeline (banco, outbox). O tenant vem do CONTEXTO, posto pelo middleware que validou a API key — nunca do corpo (§37: confiar em tenant_id do cliente). É isso que faz a key de um tenant não alcançar o form de outro.
responde 200 enquanto o processo está vivo. Não toca dependência: um liveness que consulta o banco derruba o pod quando o banco oscila.
Onde é usada
rota pública /healthz.
Efeitos
escreve JSON na resposta.
Exemplo
curl -X GET 'http://asender-runtime:8007/healthz'
leads
POST/v1/leadssem schema na spec
autentica por API key (resolvida pelo middleware), lê o JSON e chama o pipeline.
Onde é usada
rota autenticada do runtime.
Efeitos
os do pipeline (banco, outbox). O tenant vem do CONTEXTO, posto pelo middleware que validou a API key — nunca do corpo (§37: confiar em tenant_id do cliente). É isso que faz a key de um tenant não alcançar o form de outro.
toda página publicada, uma vez por visita (depois é cache).
Efeitos
escreve o script. Cache de 1 hora com ETag: o script muda com o deploy, e revalidar de hora em hora custa um 304. immutable seria errado — o conteúdo muda no mesmo caminho.
Exemplo
curl -X GET 'http://asender-runtime:8007/pixel.js'
pixels
GET/v1/pixelssem schema na spec
GET /v1/pixels.
Onde é usada
tela de pixels.
Efeitos
uma leitura.
Exemplo
curl -X GET 'http://asender-runtime:8007/v1/pixels'
curl -X GET 'http://asender-runtime:8007/v1/pixels/id_AQUI'
PATCH/v1/pixels/{id}sem schema na spec
PATCH /v1/pixels/{id}.
Onde é usada
tela de detalhe.
Efeitos
uma escrita. PATCH parcial de verdade: campo ausente não é tocado. Sem isso, editar só o nome DESATIVARIA o pixel — e a coleta pararia sem ninguém ter pedido.
resolve host+caminho na página publicada e devolve o HTML renderizado.
Onde é usada
rota curinga do runtime, a de maior tráfego do sistema.
Efeitos
consulta a fonte; escreve a resposta. 404 quando não há página: o host pode apontar para nós sem ter página naquele caminho, e devolver erro de servidor nesse caso confundiria monitoramento com tráfego normal de quem digitou a URL errada.
Exemplo
curl -X GET 'http://asender-runtime:8007/r/slug_AQUI'
raiz
GET/sem schema na spec
resolve host+caminho na página publicada e devolve o HTML renderizado.
Onde é usada
rota curinga do runtime, a de maior tráfego do sistema.
Efeitos
consulta a fonte; escreve a resposta. 404 quando não há página: o host pode apontar para nós sem ter página naquele caminho, e devolver erro de servidor nesse caso confundiria monitoramento com tráfego normal de quem digitou a URL errada.
Exemplo
curl -X GET 'http://asender-runtime:8007/'
POST/sem schema na spec
resolve a página pela mesma rota que a serviu, monta a submissão com `Modo = pagina` e chama o MESMO pipeline dos outros dois modos.
Onde é usada
POST no caminho da própria página.
Efeitos
os do pipeline (banco, outbox). O tenant e o form vêm da PÁGINA resolvida no servidor, nunca do corpo: o form da página é HTML público, e qualquer um pode reenviá-lo com outro form_id. Resolver pelo host+caminho é o que impede postar no formulário de outro tenant.
503 `starting` antes do fim do boot; depois disso, CONSULTA as sondas e responde 200 `ready` ou 503 `degraded` com o nome de cada dependência.
Onde é usada
rota pública /readyz.
Efeitos
escreve JSON na resposta; pode tocar a dependência (com cache de 1s). # Por que ele não é mais um trinco de boot A readiness era MarkReady() uma vez e pronto — memória do boot, não estado atual. Num incidente real de 2026-08-27 o serviço voltou com a credencial errada do Postgres, falhou ~30 vezes por minuto ao drenar o outbox, e /healthz e /readyz responderam 200 o tempo todo: o gate de saúde do deploy passou e quem descobriu foi o pentest, depois. Readiness que só lembra do boot não enxerga a dependência que caiu DEPOIS dele. /healthz continua sem tocar em nada, e isso é de propósito: liveness que consulta o banco derruba o processo quando o banco oscila. Quem tem de dizer "não me mande tráfego" é o readiness. O que sai na resposta é o NOME da dependência e "indisponível" — nunca a mensagem do driver, que carrega usuário, host e base. Rota pública não conta topologia; o erro inteiro vai para o log.
Exemplo
curl -X GET 'http://asender-runtime:8007/readyz'
replay
POST/replaysem schema na spec
POST /replay — abre a credencial, saneia e publica.
Onde é usada
rota pública, chamada pelo pixel a cada poucos segundos de uma sessão gravada.
Efeitos
publica no broker; NUNCA escreve no banco. Responde 204 quase sempre, como o /collect: credencial inválida é 403 (o HTML está velho), e o resto é aproveitado. O pixel roda no browser de terceiros, e transformar um lote parcialmente ruim em erro visível não conserta nada.
curl -X GET 'http://asender-runtime:8007/v1/replay/sessions'
GET/v1/replay/sessions/{id}/eventssem schema na spec
GET /v1/replay/sessions/{id}/events.
Onde é usada
player da tela de replay.
Efeitos
uma leitura. Sessão de outra conta responde lista VAZIA, e não 404: o id é opaco e gerado no browser, e distinguir "não existe" de "não é sua" transformaria a rota num oráculo de existência de sessão alheia.
Exemplo
curl -X GET 'http://asender-runtime:8007/v1/replay/sessions/id_AQUI/events'
robots.txt
GET/robots.txtsem schema na spec
libera a indexação e aponta o sitemap do MESMO host.
Onde é usada
rota pública.
Efeitos
nenhum. # Por que liberar, e não bloquear A página existe para ser achada: é landing page de campanha, e quem a publica quer tráfego. Um Disallow: / como padrão transformaria uma decisão de produto ("esta página não deve ser indexada") em comportamento da plataforma — e o cliente descobriria meses depois, sem entender por que o anúncio orgânico nunca apareceu. O sitemap é apontado com o HOST da requisição, e não com um domínio configurado: cada cliente tem o dele, e um endereço fixo aqui mandaria o buscador de todo mundo para o sitemap de um só.
Exemplo
curl -X GET 'http://asender-runtime:8007/robots.txt'
sitemap.xml
GET/sitemap.xmlsem schema na spec
lista as páginas PUBLICADAS daquele host.
Onde é usada
rota pública.
Efeitos
uma leitura no asender_pages. # Host sem página responde XML VAZIO, e não 404 Um 404 no sitemap faz o buscador registrar erro e tentar de novo; um documento vazio diz "não há nada para indexar aqui", que é a verdade. E o host que ainda não tem página publicada é o caso mais comum logo depois de alguém cadastrar um domínio. # Rascunho não entra A consulta do asender_pages filtra por versão publicada. Listar rascunho entregaria ao buscador o endereço de uma página que o cliente ainda não quis mostrar — e o buscador não esquece.
Exemplo
curl -X GET 'http://asender-runtime:8007/sitemap.xml'
version
GET/versionsem schema na spec
nome do serviço, versão e o commit que gerou o binário.
Onde é usada
rota pública, usada por ops e por quem investiga incidente.
Efeitos
escreve JSON na resposta. # Por que ela é PÚBLICA, e o que ela não conta A primeira pergunta de todo incidente é "que versão está no ar?" — e ela tem de ser respondível sem credencial, porque quem investiga muitas vezes ainda não tem uma. O que sai é o que já está no docker inspect de quem tem acesso ao host: nome, versão e commit. Nunca configuração, nunca dependência, nunca endereço interno — isso é topologia, e topologia é do console de plataforma. A versão vem de LDFLAGS no build, e o default é dev: um binário sem carimbo diz dev em vez de mentir uma versão que ninguém emitiu.
Exemplo
curl -X GET 'http://asender-runtime:8007/version'
Mail — e-mail corporativo
interno
Alcançável apenas pela malha privada — não é superfície pública.
http://asender-mail:8016 · 29 operações
domains
PUT/domains/{id}/autocreatesem schema na spec
confere posse do domínio, lê {enabled} e grava o flag.
Onde é usada
PUT /domains/{id}/autocreate. Alternativa ao catch-all.
Envia de uma caixa verificada da conta, assinado com o DKIM do domínio. Idempotente por header Idempotency-Key (a mesma chave não dispara dois e-mails). Fora de produção o piso 18 bloqueia destinos não-allowlistados.
Onde é usada
POST /v1/emails. Fluxo: API key -> tenant -> caixa por endereço -> outbound -> relay /raw.
prova que o serviço consegue atender (banco de pé). Degradação graciosa (piso 10): se o banco cai, o readyz avisa e o orquestrador não manda tráfego — o processo não morre.
Onde é usada
GET /readyz e /healthz.
Exemplo
curl -X GET 'http://asender-mail:8016/healthz' \
-H 'Authorization: Bearer SEU_TOKEN'
livez
GET/livezsem schema na spec
Livez responde 200 sempre — o processo está vivo.
Onde é usada
GET /livez.
Exemplo
curl -X GET 'http://asender-mail:8016/livez' \
-H 'Authorization: Bearer SEU_TOKEN'
mailboxes
GET/mailboxes/{id}/folders
Pastas da caixa, com total e não-lidas
Contadores exatos, na mesma consulta que lista as pastas. Ordem de uso (Entrada primeiro, Lixeira por último).
Onde é usada
GET /mailboxes/{id}/folders, que desenha o rail.
Respostas
código
significado
200
ok
404
caixa inexistente ou sem acesso
Exemplo
curl -X GET 'http://asender-mail:8016/mailboxes/id_AQUI/folders' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/mailboxes/{id}/grantssem schema na spec
ListarGrants lista as concessões de uma caixa.
Onde é usada
GET /mailboxes/{id}/grants. Admin only.
Exemplo
curl -X GET 'http://asender-mail:8016/mailboxes/id_AQUI/grants' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/mailboxes/{id}/grantssem schema na spec
ConcederGrant concede acesso de um usuário a uma caixa.
Onde é usada
POST /mailboxes/{id}/grants {user_id, role}. Admin only.
Listar mensagens da caixa (por pasta, com marcadores e busca)
Paginada por cursor opaco. O escopo é sempre a caixa da rota — a query string recorta, nunca amplia. Cursor inválido/velho volta à primeira página em vez de virar erro.
Onde é usada
GET /mailboxes/{id}/messages?folder=&unread=1&starred=1&q=&limit=N&cursor=… O recorte vem da query string, mas o ESCOPO não: a caixa sai da rota já autorizada, e é ela que a consulta usa. Um ?mailbox= no filtro seria a própria pessoa escolhendo o que pode ver.
Parâmetros
nome
em
obrigatório
descrição
folder
query
não
kind ('inbox','sent','archive','spam','trash','drafts') ou uuid da pasta; ausente = todas
unread
query
não
só não-lidas
starred
query
não
só estreladas
q
query
não
busca full-text (assunto, remetente e corpo); acento é dobrado dos dois lados
limit
query
não
cursor
query
não
o next_cursor da página anterior (opaco)
Respostas
código
significado
200
ok
404
caixa inexistente ou sem acesso (deny-default não revela existência)
Exemplo
curl -X GET 'http://asender-mail:8016/mailboxes/id_AQUI/messages' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/mailboxes/{id}/messages/{msgId}
Ler mensagem completa (marca lida)
Onde é usada
GET /mailboxes/{id}/messages/{msgId}. Cross-tenant/outra caixa -> 404.
Respostas
código
significado
200
ok
404
mensagem de outra caixa
Exemplo
curl -X GET 'http://asender-mail:8016/mailboxes/id_AQUI/messages/msgId_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
PATCH/mailboxes/{id}/messages/{msgId}
Marcar como lida, estrelar ou mover uma mensagem
Campo ausente = não mexe. Corpo sem nenhum campo é 400, e não sucesso silencioso — quem chamou acreditou ter mudado alguma coisa.
Onde é usada
PATCH /mailboxes/{id}/messages/{msgId}.
Erros
patch vazio -> 400 (não é sucesso silencioso); mensagem de outra caixa -> 404, o mesmo da leitura, que não revela a existência dela.
GET/mailboxes/{id}/messages/{msgId}/attachments/{attId}sem schema na spec
caixa->domínio->tenant + AcharAnexo (scopeado à mensagem/caixa); responde com o conteúdo e Content-Disposition.
Onde é usada
GET /mailboxes/{id}/messages/{msgId}/attachments/{attId}.
Exemplo
curl -X GET 'http://asender-mail:8016/mailboxes/id_AQUI/messages/msgId_AQUI/attachments/attId_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/mailboxes/{id}/messages/{msgId}/rawsem schema na spec
authz em cadeia (caixa -> domínio -> tenant) + leitura do blob; sai como `message/rfc822` para download.
Onde é usada
GET /mailboxes/{id}/messages/{msgId}/raw. Por que a rota existe: o .eml é o único artefato fiel do que chegou ou saiu — com cabeçalhos, ordem das partes e assinatura DKIM. É o que se abre noutro cliente, o que se anexa a uma disputa e o que se reprocessa quando o parser melhora. Sem rota, ele seria um arquivo num volume que ninguém alcança. Mensagem anterior ao ADR-0028 não tem bruto: 404, que é a verdade — e não um arquivo vazio com cara de download bem-sucedido.
Exemplo
curl -X GET 'http://asender-mail:8016/mailboxes/id_AQUI/messages/msgId_AQUI/raw' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/mailboxes/{id}/messages/batch
Aplicar a mesma alteração a várias mensagens da caixa
A autorização da rota é por CAIXA; dentro dela, cada id é conferido. Id que não pertence à caixa volta em negados — não é aplicado e não é ignorado em silêncio, para a tela poder dizer o que não foi feito. Máximo de 500 ids por chamada.
Onde é usada
POST /mailboxes/{id}/messages/batch. # A resposta diz o que NÃO foi feito Id que não é desta caixa volta em negados. Não é aplicado — e também não é ignorado em silêncio: a tela precisa poder dizer "3 de 5 movidas" em vez de mostrar cinco linhas somindo e duas voltando no próximo carregamento (piso 4). O status é 200 mesmo com negados: a operação aconteceu, parcialmente, e a lista dos dois lados é a resposta — um 4xx obrigaria o cliente a adivinhar o que foi aplicado.
o que saiu, para quem, com que resultado e, quando falhou, por quê.
Onde é usada
GET /orgs/{orgID}/envios?status=&limit=, a tela de Envios do admin. # Por que esta rota existe Porque "mandei e não sei o que aconteceu" é o pior estado possível de um serviço de e-mail. O log por destinatário já era gravado desde o F16; o que não existia era um jeito de OLHAR para ele sem abrir o banco. Um dado que só o desenvolvedor alcança não é observabilidade, é arquivo morto. O escopo é o tenant do principal verificado — nunca o orgID da rota sozinho, que é do cliente. O orgID fica no caminho por coerência com as outras rotas de org; a autorização é o tenant.
Exemplo
curl -X GET 'http://asender-mail:8016/orgs/orgID_AQUI/envios' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/orgs/{orgID}/mailbox-accountssem schema na spec
POST /orgs/{orgID}/mailbox-accounts {domain_id, local_part, name, password?}. Sem password, gera uma forte (CSPRNG) e devolve UMA vez para o admin repassar.
Onde é usada
mail-admin. Admin only. Se o auth cair, a caixa não é criada órfã (ordem: conta primeiro, depois caixa+grant) — falha limpa.
prova que o serviço consegue atender (banco de pé). Degradação graciosa (piso 10): se o banco cai, o readyz avisa e o orquestrador não manda tráfego — o processo não morre.
Onde é usada
GET /readyz e /healthz.
Exemplo
curl -X GET 'http://asender-mail:8016/readyz' \
-H 'Authorization: Bearer SEU_TOKEN'