Referência

Toda operação registrada no código, com o que ela faz, o que recebe e o que devolve.

API pública (BFF)

A superfície que integradores chamam. Autenticação por Bearer, tudo escopado por tenant.

https://api.asender.net · 89 operações

auth

GET /api/auth/me/emails sem schema na spec

histórico paginado dos emails do tenant. Era 501; agora lê do asender_messages com o canal fixado em `email`.

Onde é usadaAPI pública do cliente. Fluxo do dado: API key → Principal.TenantID → GET /v1/messages?channel=email.
Entradasstatus, q, cursor, limit (allowlist). channel do cliente é IGNORADO: nesta rota o canal é do contrato, não do parâmetro.
Saídas200 com {Messages, NextCursor, HasMore}; 422 em query malformada.

Exemplo

curl -X GET 'https://api.asender.net/api/auth/me/emails' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /api/auth/me/emails sem schema na spec

Add: POST /api/auth/me/emails.

Exemplo

curl -X POST 'https://api.asender.net/api/auth/me/emails' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /api/auth/me/emails/{id} sem schema na spec

Remove: DELETE /api/auth/me/emails/{id}.

Exemplo

curl -X DELETE 'https://api.asender.net/api/auth/me/emails/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /api/auth/me/emails/{id}/primary sem schema na spec

Primary: POST /api/auth/me/emails/{id}/primary.

Exemplo

curl -X POST 'https://api.asender.net/api/auth/me/emails/id_AQUI/primary' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /api/auth/me/emails/{id}/verify/resend sem schema na spec

Resend: POST /api/auth/me/emails/{id}/verify/resend.

Exemplo

curl -X POST 'https://api.asender.net/api/auth/me/emails/id_AQUI/verify/resend' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

bff-auth

GET /api/auth/2fa

Estado do segundo fator do usuário da sessão.

Onde é usadatela de segurança do painel.
Efeitosuma 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ódigosignificado
200OK
401credencial 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 é usadatela /2fa, para onde o login manda quem tem segundo fator.
Efeitoscria 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.

Respostas

códigosignificado
200OK

Exemplo

curl -X POST 'https://api.asender.net/api/auth/2fa' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /api/auth/2fa/disable

Desliga o segundo fator.

Onde é usadatela de segurança.
Efeitoso login deixa de exigir o segundo fator. Exige a SENHA, e não o código: aceitar o próprio TOTP para removê-lo faria o fator se autorizar sozinho.

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X POST 'https://api.asender.net/api/auth/2fa/disable' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /api/auth/2fa/setup

Inicia a ativação do segundo fator.

Onde é usadatela de segurança.
Efeitosgrava 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.

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X POST 'https://api.asender.net/api/auth/2fa/setup' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /api/auth/2fa/verify

Confirma a ativação do segundo fator.

Onde é usadatela de segurança, depois de ler o QR.
Efeitoso login passa a EXIGIR o segundo fator.

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X POST 'https://api.asender.net/api/auth/2fa/verify' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /api/auth/login

Autentica e devolve o token de sessão.

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ódigosignificado
200Sessã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).
403Conta 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.

Exemplo

curl -X POST 'https://api.asender.net/api/auth/login' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /api/auth/logout

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 é usadachamado pelas rotas /api/auth/logout dos DOIS frontends (dashboard e backoffice), que depois apagam o próprio cookie HttpOnly.
Entradasheader Authorization: Bearer <jwt de sessão>.
Saídas200 {Status:"ok"} com a sessão revogada; 401 sem Bearer; 502 LogoutFailed quando o asender-auth não confirmou.
Efeitosmarca 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ódigosignificado
200Revogado, 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).

Exemplo

curl -X POST 'https://api.asender.net/api/auth/logout' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /api/auth/me

Principal da sessão.

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ódigosignificado
200OK
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 é usadatela de perfil do painel.
Efeitosescreve no asender-auth; e-mail novo desverifica a conta e dispara a verificação do endereço novo (lá).

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X PATCH 'https://api.asender.net/api/auth/me' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /api/auth/me/verify/resend

Reenvia o e-mail de verificação do usuário da sessão.

Onde é usadaaviso "confirme seu e-mail" do painel.
Efeitosum 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á.

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X POST 'https://api.asender.net/api/auth/me/verify/resend' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /api/auth/register

Cria usuário e (best-effort) o tenant raiz dele.

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ódigosignificado
201Usuá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.

Exemplo

curl -X POST 'https://api.asender.net/api/auth/register' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /api/auth/sessions

Dispositivos e sessões ativas do usuário.

Onde é usadatela de segurança do painel.
Efeitosuma 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ódigosignificado
200OK
401credencial 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 é usadabotão "remover" da tela de segurança.
Efeitosa 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.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X DELETE 'https://api.asender.net/api/auth/sessions/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /api/auth/verify

Confirma o e-mail a partir do token do link.

Onde é usadatela /verify, com o token da URL.
Efeitoscarimba email_verified_at no auth. # Também é PÚBLICA Quem abre o link do e-mail pode estar em outro navegador, ou nem ter sessão. Exigir login para verificar o e-mail cria a dependência circular clássica — e a pessoa que mais precisa verificar é justamente a que ainda não conseguiu entrar.

Respostas

códigosignificado
200OK

Exemplo

curl -X POST 'https://api.asender.net/api/auth/verify' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

bff-messaging

GET /api/contacts

Audiência do tenant.

Onde é usadatela de audiência do dashboard.
Entradasq, list_id, cursor, limit (allowlist).
Saídas200 com {Contacts, NextCursor, HasMore}.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader nãoTenant 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).
qquery não
list_idquery não
cursorquery não
limitquery não

Respostas

códigosignificado
200OK
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 é usadaformulário de novo contato da tela de audiência.
Saídas201 com {Contact}; 422 em validação (email/telefone inválidos).
Efeitosescreve em messages.contacts.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader nãoTenant 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ódigosignificado
201Criado.
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.

Exemplo

curl -X POST 'https://api.asender.net/api/contacts' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /api/contacts/bulk

Importa até 5000 contatos, com upsert pela identidade natural.

Import parcial é resultado legítimo: itens ruins voltam em Errors com o índice de origem, e o lote não é invalidado por causa deles. Lote vazio ou acima do teto é 422.

Onde é usadaimportador CSV da tela de audiência.
Saídas200 com {Created, Updated, Errors}; 422 quando a lista vem vazia ou acima do teto.
Efeitosescreve em messages.contacts.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader nãoTenant 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ódigosignificado
200Lote 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.

Exemplo

curl -X POST 'https://api.asender.net/api/contacts/bulk' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /api/lists

Listas de contatos com contagem de membros.

Onde é usadatela de audiência.
Saídas200 com {Lists}.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader nãoTenant 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).
cursorquery não
limitquery não

Respostas

códigosignificado
200OK
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 é usadatela de audiência.
Saídas201 com {List}; 422 em validação.
Efeitosescreve em messages.contact_lists.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader nãoTenant 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ódigosignificado
201Criada (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.

Exemplo

curl -X POST 'https://api.asender.net/api/lists' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /api/lists/{id}/members

Anexa contatos a uma lista.

NotFound traz os ids que não existem no tenant — o servidor diz o que NÃO pôde fazer, em vez de descartar em silêncio. Idempotente.

Onde é usadatela de audiência (seleção múltipla).
Saídas200 com {Added}; 404 se a lista não é do tenant; 422 se a lista de ids vem vazia.
Efeitosescreve em messages.contact_list_members.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader nãoTenant 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).
idpath simPublic id da lista (`lst_<hex>`).

Respostas

códigosignificado
200OK
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.

Exemplo

curl -X POST 'https://api.asender.net/api/lists/id_AQUI/members' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /api/messages

Histórico de envios do tenant corrente.

Onde é usadatela de histórico do dashboard.
EntradasChannel/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ídas200 com {Messages, NextCursor, HasMore}; 422 em query malformada.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader nãoTenant 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).
channelquery não
statusquery não
qquery não
cursorquery não
limitquery não

Respostas

códigosignificado
200OK
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 é usadatela de detalhe do envio.
Saídas200 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

nomeemobrigatóriodescrição
X-Asender-Tenantheader nãoTenant 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).
idpath simPublic id da mensagem (`msg_<hex>`).

Respostas

códigosignificado
200OK
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 é usadacomposer 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ídas202 com {Messages, ReusedIdempotency}; 422 quando o asender_messages recusa a validação (canal inválido, destinatário vazio, corpo ausente).
Efeitosescreve mensagens no banco de mensageria.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader nãoTenant 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ódigosignificado
202Enfileirado.
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.

Exemplo

curl -X POST 'https://api.asender.net/api/messages/send' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /api/push/devices

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 é usadadashboard, tela /send/push. Fluxo do dado: messages.push_devices → asender_messages → aqui → seletor.
Entradascursor e limit, na mesma allowlist dos outros List.
Saídas200 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

nomeemobrigatóriodescrição
X-Asender-Tenantheader nãoTenant 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).
cursorquery não
limitquery não

Respostas

códigosignificado
200Pá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 é usadatela 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ídas200 com {PublicKey, ApiBase, Manifest, ServiceWorker, Snippet}.
Efeitospode 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

nomeemobrigatóriodescrição
X-Asender-Tenantheader nãoTenant 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ódigosignificado
200Artefatos 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 é usadadashboard.
Saídas200 com {Templates}.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader nãoTenant 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).
limitquery não

Respostas

códigosignificado
200OK
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 é usadaeditor de templates do dashboard.
Saídas200 com {Template}; 422 em validação do asender_messages.
Efeitosescreve em messages.templates.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader nãoTenant 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ódigosignificado
200Gravado (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.

Exemplo

curl -X POST 'https://api.asender.net/api/templates' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

bff-plataforma

GET /api/admin/blacklist

Lista os bloqueios da plataforma.

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X GET 'https://api.asender.net/api/admin/blacklist' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /api/admin/blacklist

Acrescenta uma entrada à lista de bloqueio.

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X POST 'https://api.asender.net/api/admin/blacklist' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /api/admin/blacklist/{id}

Remove uma entrada da lista de bloqueio.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X DELETE 'https://api.asender.net/api/admin/blacklist/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /api/admin/blacklist/suggestions

Sugestões de bloqueio ainda não decididas.

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X GET 'https://api.asender.net/api/admin/blacklist/suggestions' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /api/admin/blacklist/suggestions

Registra uma sugestão de bloqueio.

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X POST 'https://api.asender.net/api/admin/blacklist/suggestions' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /api/admin/blacklist/suggestions/{id}/apply

Aceita a sugestão e a promove a bloqueio.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X POST 'https://api.asender.net/api/admin/blacklist/suggestions/id_AQUI/apply' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /api/admin/blacklist/suggestions/{id}/dismiss

Descarta a sugestão.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X POST 'https://api.asender.net/api/admin/blacklist/suggestions/id_AQUI/dismiss' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /api/admin/datacenter-asn

ASNs classificados como datacenter.

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X GET 'https://api.asender.net/api/admin/datacenter-asn' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /api/admin/datacenter-asn

Classifica um ASN como datacenter.

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X POST 'https://api.asender.net/api/admin/datacenter-asn' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /api/admin/datacenter-asn/{asn}

Remove a classificação de um ASN.

Parâmetros

nomeemobrigatóriodescrição
asnpath sim

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X DELETE 'https://api.asender.net/api/admin/datacenter-asn/asn_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /api/admin/impersonation/end

Encerra a sessão de suporte em andamento.

Onde é usadabotão "sair da conta".
Efeitosa sessão de suporte para NA HORA.

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X POST 'https://api.asender.net/api/admin/impersonation/end' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /api/admin/me

Diz se o usuário da sessão é da plataforma.

Onde é usadao console, ao abrir — é o que decide entre mostrar o console e mostrar "não disponível".
Efeitosuma 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ódigosignificado
200OK
401credencial 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ódigosignificado
200OK
401credencial 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ódigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X GET 'https://api.asender.net/api/admin/platform-alerts' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /api/admin/platform-alerts

Cria uma regra de alerta de plataforma.

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X POST 'https://api.asender.net/api/admin/platform-alerts' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /api/admin/platform-alerts/{id}

Remove uma regra de alerta.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X DELETE 'https://api.asender.net/api/admin/platform-alerts/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
PUT /api/admin/platform-alerts/{id}

Substitui uma regra de alerta.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X PUT 'https://api.asender.net/api/admin/platform-alerts/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /api/admin/platform-alerts/events

Disparos de alerta da plataforma.

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

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 é usadatela /impersonate.
Efeitosuma leitura no core.

Respostas

códigosignificado
200OK
401credencial 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 é usadaconsole, ao abrir um cliente.
Efeitosuma 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

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200OK
401credencial 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 é usadaconsole.
Efeitosuma 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.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X PATCH 'https://api.asender.net/api/admin/tenants/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /api/admin/tenants/{id}/enter

Abre sessão de suporte na conta (somente leitura, ADR-0017).

Onde é usadatela /impersonate.
Efeitosuma 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.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X POST 'https://api.asender.net/api/admin/tenants/id_AQUI/enter' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

bff-reports

GET /api/reports/overview

Funil do período com quebra por canal.

Os três canais SEMPRE aparecem em ByChannel, zerados quando não houve tráfego. As chaves de ByChannel (email/sms/push) ficam em minúsculas porque são DADO, não nome de campo.

Taxas em [0,1], arredondadas a 4 casas, grampeadas no teto (a base pode ter bounced > sent, porque bounce não exige sent_at).

Onde é usadadashboard de relatórios (cards do topo). Fluxo do dado: sessão → tenant resolvido → asender_messages GET /v1/reports/overview → PascalCase.
Entradasfrom/to em ISO-8601 UTC; ausentes = últimos 30 dias (default resolvido pelo asender_messages, não duplicado aqui).
Saídas200 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

nomeemobrigatóriodescrição
X-Asender-Tenantheader nãoTenant 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).
fromquery nãoISO-8601 (`YYYY-MM-DD` ou RFC 3339). Default: 30 dias antes de `to`.
toquery nãoISO-8601. Default: agora. **Fim EXCLUSIVO**; data pura cobre o dia inteiro.

Respostas

códigosignificado
200OK
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 é usadadashboard de relatórios (gráfico principal).
Entradasfrom, 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ídas200 com {Points:[...]}.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader nãoTenant 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).
fromquery nãoISO-8601 (`YYYY-MM-DD` ou RFC 3339). Default: 30 dias antes de `to`.
toquery nãoISO-8601. Default: agora. **Fim EXCLUSIVO**; data pura cobre o dia inteiro.
channelquery não
intervalquery não`week` e `month` são aceitos aqui e recusados pelo upstream (422).

Respostas

códigosignificado
200OK
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 é usadadashboard de relatórios (tabela lateral).
Entradasfrom, to, limit (1..100).
Saídas200 com {Templates:[...]}.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader nãoTenant 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).
fromquery nãoISO-8601 (`YYYY-MM-DD` ou RFC 3339). Default: 30 dias antes de `to`.
toquery nãoISO-8601. Default: agora. **Fim EXCLUSIVO**; data pura cobre o dia inteiro.
limitquery não

Respostas

códigosignificado
200OK
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 é usadatela de convite.
Efeitosuma 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.

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X POST 'https://api.asender.net/api/invitations/accept' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /api/tenants

Vínculos de tenant do usuário logado.

Onde é usadaAPI pública do cliente. Fluxo do dado: API key → Principal.TenantID → GET /v1/messages?channel=email.
Entradasstatus, q, cursor, limit (allowlist). channel do cliente é IGNORADO: nesta rota o canal é do contrato, não do parâmetro.
Saídas200 com {Messages, NextCursor, HasMore}; 422 em query malformada.

Respostas

códigosignificado
200OK
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 é usadafluxo 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ídas201 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.
Efeitosescreve no asender-core. O owner_user_id vem SEMPRE do principal verificado, nunca do corpo.

Respostas

códigosignificado
201Tenant 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.

Exemplo

curl -X POST 'https://api.asender.net/api/tenants' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /api/tenants/{id}/children

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 é usadatela de hierarquia de tenants.
Entradas{Name, Slug, MonthlyQuota} — MonthlyQuota nulo/ausente significa herdar do ancestral mais próximo com valor.
Saídas201 com {Tenant}; 404 se {id} não é acessível; 422 quando o core recusa (slug duplicado, ciclo detectado pelo trigger de closure).
Efeitosescreve no asender-core.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
201Sub-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.

Exemplo

curl -X POST 'https://api.asender.net/api/tenants/id_AQUI/children' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /api/tenants/{id}/impersonations

Trilha de sessões de suporte na conta.

Onde é usadatela de segurança da conta — do CLIENTE.
Efeitosuma 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

nomeemobrigatóriodescrição
idpath simPublic id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
200OK
401credencial 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

nomeemobrigatóriodescrição
idpath simPublic id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
200OK
401credencial 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 é usadatela de equipe.
Efeitosuma escrita no core (e, quando houver envio, um e-mail).

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X POST 'https://api.asender.net/api/tenants/id_AQUI/invitations' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /api/tenants/{id}/invitations/{token}

Cancela um convite pendente.

Onde é usadatela de equipe.
Efeitosuma escrita no core.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
tokenpath sim

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X DELETE 'https://api.asender.net/api/tenants/id_AQUI/invitations/token_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /api/tenants/{id}/members

Membros da conta.

Onde é usadatela de equipe.
Efeitosuma leitura no core.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/members' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /api/tenants/{id}/members

Vincula um usuário à conta.

Onde é usadatela de equipe.
Efeitosuma escrita no core.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X POST 'https://api.asender.net/api/tenants/id_AQUI/members' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /api/tenants/{id}/members/{userId}

Desvincula um usuário da conta.

Onde é usadatela de equipe.
Efeitosuma escrita no core.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
userIdpath sim

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X DELETE 'https://api.asender.net/api/tenants/id_AQUI/members/userId_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
PATCH /api/tenants/{id}/parent

Move o tenant para outro pai (ou para a raiz).

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 é usadatela 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ídas200 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).
Efeitosreescreve a closure de tenants no core.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
200Movido; 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.

Exemplo

curl -X PATCH 'https://api.asender.net/api/tenants/id_AQUI/parent' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
PATCH /api/tenants/{id}/quota

Define ou limpa a quota mensal do tenant.

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 é usadatela 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ídas200 com {Tenant}; 404 se {id} não é acessível; 422 em valor inválido.
Efeitosescreve no asender-core.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
200Quota 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.

Exemplo

curl -X PATCH 'https://api.asender.net/api/tenants/id_AQUI/quota' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /api/tenants/tree

Subárvore do tenant corrente, em pré-ordem.

Onde é usadadashboard (settings/tenants) e backoffice. Fluxo do dado: sessão → tenant resolvido → asender-core GET /v1/tenants/{id}/tree → PascalCase.
Entradasdepth — inteiro positivo; valor não numérico é 422, não "sem limite" silencioso.
Saídas200 com {TenantId, Tree:[...]}; 404 se o tenant pedido não é acessível.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader nãoTenant 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).
depthquery não

Respostas

códigosignificado
200OK
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ódigosignificado
200OK

Exemplo

curl -X GET 'https://api.asender.net/' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /healthz

Liveness. Não toca dependência.

Respostas

códigosignificado
200Processo vivo.

Exemplo

curl -X GET 'https://api.asender.net/healthz' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /metrics

Métricas Prometheus.

Respostas

códigosignificado
200Texto 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ódigosignificado
200Pronto (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 é usadarota 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ódigosignificado
200OK

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ódigosignificado
200OK
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 é usadasuperfí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ídas200 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ódigosignificado
200Consumo 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`.
502Core 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 é usadaAPI pública do cliente. Fluxo do dado: API key → Principal.TenantID → GET /v1/messages?channel=email.
Entradasstatus, q, cursor, limit (allowlist). channel do cliente é IGNORADO: nesta rota o canal é do contrato, não do parâmetro.
Saídas200 com {Messages, NextCursor, HasMore}; 422 em query malformada.

Parâmetros

nomeemobrigatóriodescrição
statusquery não
qquery nãoBusca. `%` e `_` são escapados no repositório.
cursorquery não
limitquery não

Respostas

códigosignificado
200Pá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

nomeemobrigatóriodescrição
idpath simPublic id da mensagem (`msg_<hex>`).

Respostas

códigosignificado
200Detalhe.
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 é usadaAPI pública do cliente. Fluxo do dado: igual ao Send, uma chamada ao asender_messages por item.
Saídas202 com {Results, Count}; cada item traz MessageId ou Error.
Efeitosescrita no serviço de mensageria por item aceito.

Respostas

códigosignificado
202Lote 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`.

Exemplo

curl -X POST 'https://api.asender.net/v1/emails/batch' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/emails/send

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 é usadaAPI 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ídas202 com {MessageId, MessageIds, Status, ReusedIdempotency}; 400 em validação local; 422 quando o asender_messages recusa; 502 se ele está fora.
Efeitosescrita no serviço de mensageria. Idempotente por IdempotencyKey.

Parâmetros

nomeemobrigatóriodescrição
Idempotency-Keyheader 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ódigosignificado
202Aceito 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.

Exemplo

curl -X POST 'https://api.asender.net/v1/emails/send' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

public-push

GET /v1/push/devices

Lista os devices de push da conta da API key.

Contraparte de leitura do registro: quem envia token precisa poder conferir o que está registrado. O tenant vem da API key, NUNCA da query — aceitar tenant_id do cliente aqui seria vazamento cross-tenant.

Onde é usadadashboard, tela /send/push. Fluxo do dado: messages.push_devices → asender_messages → aqui → seletor.
Entradascursor e limit, na mesma allowlist dos outros List.
Saídas200 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

nomeemobrigatóriodescrição
cursorquery não
limitquery não

Respostas

códigosignificado
200Pá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 é usadaSDK 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ídas200 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.
Efeitosescrita no serviço de mensageria.

Respostas

códigosignificado
200Device 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`.

Exemplo

curl -X POST 'https://api.asender.net/v1/push/devices' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/push/send

Enfileira um push para tokens ou para um tópico.

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 é usadaAPI 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ídas202 com {MessageId, MessageIds, Status, ReusedIdempotency}; 400 em validação local; 422 quando o asender_messages recusa; 502 se ele está fora.
Efeitosescrita no serviço de mensageria. Idempotente por IdempotencyKey.

Parâmetros

nomeemobrigatóriodescrição
Idempotency-Keyheader 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ódigosignificado
202Aceito 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.

Exemplo

curl -X POST 'https://api.asender.net/v1/push/send' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

public-sms

POST /v1/sms/send

Enfileira um SMS.

Onde é usadaAPI 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ídas202 com {MessageId, MessageIds, Status, ReusedIdempotency}; 400 em validação local; 422 quando o asender_messages recusa; 502 se ele está fora.
Efeitosescrita no serviço de mensageria. Idempotente por IdempotencyKey.

Parâmetros

nomeemobrigatóriodescrição
Idempotency-Keyheader 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ódigosignificado
202Aceito 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.

Exemplo

curl -X POST 'https://api.asender.net/v1/sms/send' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

tenants

GET /api/tenants/{id}/api-keys sem schema na spec

ListKeys lista as chaves da conta.

Onde é usadaGET /api/tenants/{id}/api-keys.

Exemplo

curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/api-keys' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /api/tenants/{id}/api-keys sem schema na spec

CreateKey emite uma chave. O texto puro volta UMA vez, no corpo do core.

Onde é usadaPOST /api/tenants/{id}/api-keys.

Exemplo

curl -X POST 'https://api.asender.net/api/tenants/id_AQUI/api-keys' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /api/tenants/{id}/api-keys/{keyId} sem schema na spec

RevokeKey revoga uma chave.

Onde é usadaDELETE /api/tenants/{id}/api-keys/{keyId}.

Exemplo

curl -X DELETE 'https://api.asender.net/api/tenants/id_AQUI/api-keys/keyId_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'

Identidade (OIDC / OAuth 2.1)

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.json sem schema na spec

JWKS serve /.well-known/jwks.json — a(s) chave(s) pública(s) RS256. Os clients

Onde é usadaverificação no client.

Exemplo

curl -X GET 'https://auth.asender.net/.well-known/jwks.json' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /.well-known/openid-configuration sem schema na spec

anuncia os endpoints e as capacidades (só `code`, só S256, só RS256 — o perfil OAuth2.1 da casa).

Onde é usadaos 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ódigosignificado
200Sessã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.

Exemplo

curl -X POST 'https://auth.asender.net/v1/auth/login' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/auth/logout

Revoga a sessão do Bearer apresentado.

Credencial: Authorization: Bearer <token de sessão>não o token de serviço. Ausente → 401 MissingToken.

Corrigido na onda 7: revoga de fato e o validate passa a consultar o estado da sessão, então o token deixa de valer imediatamente.

Respostas

códigosignificado
200Revogada.
401`401` — `MissingToken` (sem `Authorization: Bearer`), `InvalidToken` (assinatura/formato) ou `ExpiredToken` (expirada **ou revogada por logout**).

Exemplo

curl -X POST 'https://auth.asender.net/v1/auth/logout' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/auth/password-reset

Solicita o token de redefinição de senha.

Respostas

códigosignificado
200Sempre `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.

Exemplo

curl -X POST 'https://auth.asender.net/v1/auth/password-reset' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/auth/password-reset/confirm

Redefine a senha com o token emitido.

Respostas

códigosignificado
200Senha alterada.
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`InvalidToken` ou `ExpiredToken`.

Exemplo

curl -X POST 'https://auth.asender.net/v1/auth/password-reset/confirm' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/auth/sessions

Sessões ativas do usuário.

Onde é usadatela de segurança do painel, através do BFF.
Efeitosuma 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ódigosignificado
200OK
401credencial 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 é usadabotão "remover" da tela de segurança.
Efeitosa 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.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X DELETE 'https://auth.asender.net/v1/auth/sessions/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/auth/validate

Resolve um token de sessão no usuário dono.

É 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ódigosignificado
200Sessã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 é usadatela /verify do front, com o token da URL.
Efeitosduas 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.

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X POST 'https://auth.asender.net/v1/auth/verify' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

authorize

GET /authorize sem schema na spec

Authorize implementa GET /authorize (OAuth2.1 authorization code + PKCE).

Onde é usadao 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/decide sem schema na spec

`{permitido, motivo, papel, nivel}`.

Onde é usadaPOST /v1/authz/decide.
Efeitosgrava 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).

Exemplo

curl -X POST 'https://auth.asender.net/v1/authz/decide' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/authz/eu sem schema na spec

`{produtos:[{slug,nome,subdominio,papel,nivel}]}`.

Onde é usada 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/produtos sem schema na spec

Catalogo devolve as ferramentas que existem.

Onde é usadaGET /v1/authz/produtos.

Exemplo

curl -X GET 'https://auth.asender.net/v1/authz/produtos' \
  -H 'Authorization: Bearer SEU_TOKEN'

logout

GET /logout sem 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 é usadarota raiz.
Efeitosescreve 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ódigosignificado
200OK

Exemplo

curl -X GET 'https://auth.asender.net/' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /healthz

Liveness.

Onde é usadaliveness probe.
Efeitosescreve 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ódigosignificado
200OK

Exemplo

curl -X GET 'https://auth.asender.net/healthz' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /metrics

Métricas Prometheus.

Respostas

códigosignificado
200Texto.

Exemplo

curl -X GET 'https://auth.asender.net/metrics' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /readyz

Readiness — ping no Postgres.

Onde é usadareadiness probe.
Efeitosuma 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ódigosignificado
200Banco 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 é usadarota 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ódigosignificado
200OK

Exemplo

curl -X GET 'https://auth.asender.net/version' \
  -H 'Authorization: Bearer SEU_TOKEN'

tenants

GET /v1/tenants/{id}/acessos sem schema na spec

ListarAcessos devolve quem acessa o quê na conta.

Onde é usadaGET /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}/acessos sem schema na spec

Conceder dá acesso de uma pessoa a uma ferramenta da conta.

Onde é usadaPOST /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.

Exemplo

curl -X POST 'https://auth.asender.net/v1/tenants/id_AQUI/acessos' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/tenants/{id}/acessos/{userId}/{produto} sem schema na spec

Revogar tira o acesso.

Onde é usadaDELETE /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.

Exemplo

curl -X DELETE 'https://auth.asender.net/v1/tenants/id_AQUI/acessos/userId_AQUI/produto_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/tenants/{id}/authz/decisoes sem schema na spec

Trilha devolve as decisões recentes da conta.

Onde é usadaGET /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}/produtos sem schema na spec

ListarAssinatura devolve o que a conta assina.

Onde é usadaGET /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}/produtos sem schema na spec

Assinar liga ou suspende um produto na conta.

Onde é usadaPOST /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.

Exemplo

curl -X POST 'https://auth.asender.net/v1/tenants/id_AQUI/produtos' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

token

POST /token sem schema na spec

Token implementa POST /token. Despacha por grant_type.

Onde é usadao client troca aqui, servidor-a-servidor (ou pelo BFF), o code/refresh por tokens.

Exemplo

curl -X POST 'https://auth.asender.net/token' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

twofa

POST /v1/2fa/check

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ódigosignificado
200Có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).
401`401 TwoFactorFailed` — código TOTP inválido.
404`404 NotFound`.

Exemplo

curl -X POST 'https://auth.asender.net/v1/2fa/check' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/2fa/disable

Desliga o TOTP do usuário.

Respostas

códigosignificado
200Desligado.
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`.**
404`404 NotFound`.

Exemplo

curl -X POST 'https://auth.asender.net/v1/2fa/disable' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/2fa/setup

Gera o segredo TOTP e a `otpauth://` URL.

Ainda não confirmado: o 2FA só passa a valer depois de POST /v1/2fa/verify.

Respostas

códigosignificado
200Segredo 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`.**
404`404 NotFound`.

Exemplo

curl -X POST 'https://auth.asender.net/v1/2fa/setup' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/2fa/verify

Confirma o setup do TOTP.

NÃO emite sessão — isso é o check.

Respostas

códigosignificado
200Confirmado.
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 TwoFactorFailed` — código TOTP inválido.
404`404 NotFound`.

Exemplo

curl -X POST 'https://auth.asender.net/v1/2fa/verify' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

userinfo

GET /userinfo sem schema na spec

valida o Bearer (RS256, iss, exp) e devolve sub/email/name.

Onde é usadao 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ódigosignificado
201Criado.
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.

Exemplo

curl -X POST 'https://auth.asender.net/v1/users' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/users/{id}

Remove o usuário.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`usr_<hex>`). O BIGSERIAL interno nunca aparece.

Respostas

códigosignificado
204Removido.
401`401` — token de serviço ausente ou inválido. **Não ocorre com `ENV=development`.**
404`404 NotFound`.

Exemplo

curl -X DELETE 'https://auth.asender.net/v1/users/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/users/{id}

Um usuário pelo public id.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`usr_<hex>`). O BIGSERIAL interno nunca aparece.

Respostas

códigosignificado
200OK
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

nomeemobrigatóriodescrição
idpath simPublic id (`usr_<hex>`). O BIGSERIAL interno nunca aparece.

Respostas

códigosignificado
200OK
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`.**
404`404 NotFound`.
409`409 Conflict` — email já cadastrado.

Exemplo

curl -X PATCH 'https://auth.asender.net/v1/users/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/users/{id}/emails sem schema na spec

Listar devolve os e-mails do usuário.

Onde é usadaGET /v1/users/{id}/emails.

Exemplo

curl -X GET 'https://auth.asender.net/v1/users/id_AQUI/emails' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /v1/users/{id}/emails sem schema na spec

Adicionar cria um e-mail secundário (não-verificado) e dispara a verificação.

Onde é usadaPOST /v1/users/{id}/emails {email}.

Exemplo

curl -X POST 'https://auth.asender.net/v1/users/id_AQUI/emails' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/users/{id}/emails/{emailId} sem schema na spec

Remover apaga um e-mail secundário.

Onde é usadaDELETE /v1/users/{id}/emails/{emailId}.

Exemplo

curl -X DELETE 'https://auth.asender.net/v1/users/id_AQUI/emails/emailId_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /v1/users/{id}/emails/{emailId}/primary sem schema na spec

DefinirPrimario promove um e-mail verificado a primário.

Onde é usada POST /v1/users/{id}/emails/{emailId}/primary.

Exemplo

curl -X POST 'https://auth.asender.net/v1/users/id_AQUI/emails/emailId_AQUI/primary' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/users/{id}/emails/{emailId}/verify/resend sem schema na spec

Reenviar redispara a verificação de um e-mail.

Onde é usada POST /v1/users/{id}/emails/{emailId}/verify/resend.

Exemplo

curl -X POST 'https://auth.asender.net/v1/users/id_AQUI/emails/emailId_AQUI/verify/resend' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/users/{id}/verify/resend

Reenvia o e-mail de verificação de um usuário.

Onde é usada POST /v1/users/{id}/emails/{emailId}/verify/resend.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200OK
401credencial ausente ou inválida

Exemplo

curl -X POST 'https://auth.asender.net/v1/users/id_AQUI/verify/resend' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

Core — contas e árvore de tenants interno

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 é usadaPOST /internal/v1/api-keys/validate, chamada pelo asender-api a cada request autenticada por chave.
Efeitosescreve 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ódigosignificado
200Chave 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.

Exemplo

curl -X POST 'http://asender-core:8002/v1/api-keys/validate' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/tenants/{id}/api-keys

Chaves do tenant (sem o segredo).

Onde é usadaGET /v1/tenants/{id}/api-keys.
Efeitosescreve a resposta HTTP.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
200OK
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 é usadaPOST /v1/tenants/{id}/api-keys.
Efeitosescreve 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

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
201Emitida.
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`.

Exemplo

curl -X POST 'http://asender-core:8002/v1/tenants/id_AQUI/api-keys' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/tenants/{id}/api-keys/{keyId}

Revoga uma chave.

Onde é usadaDELETE /v1/tenants/{id}/api-keys/{keyID}.
Efeitosescreve a resposta HTTP.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
keyIdpath sim

Respostas

códigosignificado
204Revogada.
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 DELETE 'http://asender-core:8002/v1/tenants/id_AQUI/api-keys/keyId_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'

impersonations

POST /impersonations sem schema na spec

POST /v1/impersonations.

Onde é usadagateway, quando alguém da plataforma pede para entrar numa conta.
Efeitosuma escrita — e, a partir dela, LEITURA da conta alheia.

Exemplo

curl -X POST 'http://asender-core:8002/impersonations' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /impersonations/{id} sem schema na spec

GET /v1/impersonations/{id}.

Onde é usadagateway, a cada requisição que se apresenta como impersonada.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-core:8002/impersonations/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /impersonations/{id}/end sem schema na spec

POST /v1/impersonations/{id}/end.

Onde é usadabotão "sair da conta" e logout do suporte.
Efeitosa sessão de suporte para NA HORA.

Exemplo

curl -X POST 'http://asender-core:8002/impersonations/id_AQUI/end' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

invitations

POST /v1/invitations/accept

Aceita um convite e cria o vínculo de membro.

Onde é usadaPOST /v1/invitations/{token}/accept.
Efeitosescreve 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ódigosignificado
200Aceito; 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`.
409`409 AlreadyUsed` — convite já aceito.
410`410 Expired` — convite vencido.

Exemplo

curl -X POST 'http://asender-core:8002/v1/invitations/accept' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/tenants/{id}/invitations

Convites do tenant.

O token de cada convite vem no corpo. Quem lê esta rota pode aceitar qualquer convite pendente.

Onde é usadaGET /v1/tenants/{id}/invitations.
Efeitosescreve a resposta HTTP.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
200OK
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 é usadaPOST /v1/tenants/{id}/invitations.
Efeitosescreve a resposta HTTP.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
201Criado.
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`.

Exemplo

curl -X POST 'http://asender-core:8002/v1/tenants/id_AQUI/invitations' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/tenants/{id}/invitations/{token}

Revoga um convite pelo token.

Onde é usadaDELETE /v1/tenants/{id}/invitations/{token}.
Efeitosescreve a resposta HTTP.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
tokenpath sim

Respostas

códigosignificado
204Revogado.
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 DELETE 'http://asender-core:8002/v1/tenants/id_AQUI/invitations/token_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'

members

GET /v1/tenants/{id}/members

Membros do tenant.

Onde é usadaGET /v1/tenants/{id}/members.
Efeitosescreve a resposta HTTP.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
200OK
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 é usadaPOST /v1/tenants/{id}/members.
Efeitosescreve a resposta HTTP.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
201Vinculado.
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.

Exemplo

curl -X POST 'http://asender-core:8002/v1/tenants/id_AQUI/members' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/tenants/{id}/members/{userId}

Desvincula um usuário do tenant.

Onde é usadaDELETE /v1/tenants/{id}/members/{userID}.
Efeitosescreve a resposta HTTP.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
userIdpath sim

Respostas

códigosignificado
204Desvinculado.
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 DELETE 'http://asender-core:8002/v1/tenants/id_AQUI/members/userId_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'

meta

GET /

Identidade do serviço.

Onde é usadarota raiz.
Efeitosescreve 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ódigosignificado
200OK

Exemplo

curl -X GET 'http://asender-core:8002/' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /healthz

Liveness.

Onde é usadaliveness probe.
Efeitosescreve 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ódigosignificado
200OK

Exemplo

curl -X GET 'http://asender-core:8002/healthz' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /metrics

Métricas Prometheus.

Respostas

códigosignificado
200Texto.

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 é usadareadiness probe.
Efeitosuma 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ódigosignificado
200Todas as dependências responderam.
503Alguma 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 é usadagateway, antes de mostrar ou aceitar qualquer rota de plataforma.
Efeitosuma 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/alerts sem schema na spec

GET /v1/platform/alerts.

Onde é usadaconsole de plataforma.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-core:8002/platform/alerts' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /platform/alerts sem schema na spec

POST /v1/platform/alerts.

Onde é usadaconsole de plataforma.
Efeitosuma escrita.

Exemplo

curl -X POST 'http://asender-core:8002/platform/alerts' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /platform/alerts/{id} sem schema na spec

DELETE /v1/platform/alerts/{id}.

Onde é usadaconsole de plataforma.
Efeitosuma escrita — os EVENTOS ficam.

Exemplo

curl -X DELETE 'http://asender-core:8002/platform/alerts/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
PUT /platform/alerts/{id} sem schema na spec

PUT /v1/platform/alerts/{id}.

Onde é usadaconsole de plataforma.
Efeitosuma 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.

Exemplo

curl -X PUT 'http://asender-core:8002/platform/alerts/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /platform/alerts/events sem schema na spec

GET /v1/platform/alerts/events?limite=N.

Onde é usadaconsole de plataforma.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-core:8002/platform/alerts/events' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /platform/blacklist sem schema na spec

GET /v1/platform/blacklist?tipo=.

Onde é usadaconsole de plataforma.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-core:8002/platform/blacklist' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /platform/blacklist sem schema na spec

POST /v1/platform/blacklist.

Onde é usadaconsole de plataforma.
Efeitosuma escrita — o valor deixa de ser aceito em TODAS as contas.

Exemplo

curl -X POST 'http://asender-core:8002/platform/blacklist' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /platform/blacklist/{id} sem schema na spec

DELETE /v1/platform/blacklist/{id}.

Onde é usadaconsole de plataforma.
Efeitosuma escrita.

Exemplo

curl -X DELETE 'http://asender-core:8002/platform/blacklist/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /platform/blacklist/suggestions sem schema na spec

GET /v1/platform/blacklist/suggestions?todas=1.

Onde é usadaconsole de plataforma.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-core:8002/platform/blacklist/suggestions' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /platform/blacklist/suggestions sem schema na spec

POST /v1/platform/blacklist/suggestions.

Onde é usadaquem detecta abuso (hoje o ops).
Efeitosuma escrita — NUNCA um bloqueio.

Exemplo

curl -X POST 'http://asender-core:8002/platform/blacklist/suggestions' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /platform/blacklist/suggestions/{id}/apply sem schema na spec

POST /v1/platform/blacklist/suggestions/{id}/apply.

Onde é usadaconsole de plataforma.
Efeitosduas escritas.

Exemplo

curl -X POST 'http://asender-core:8002/platform/blacklist/suggestions/id_AQUI/apply' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /platform/blacklist/suggestions/{id}/dismiss sem schema na spec

POST /v1/platform/blacklist/suggestions/{id}/dismiss.

Onde é usadaconsole de plataforma.
Efeitosuma escrita.

Exemplo

curl -X POST 'http://asender-core:8002/platform/blacklist/suggestions/id_AQUI/dismiss' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /platform/datacenter-asn sem schema na spec

GET /v1/platform/datacenter-asn.

Onde é usadaconsole de plataforma e o consumo interno da captura.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-core:8002/platform/datacenter-asn' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /platform/datacenter-asn sem schema na spec

POST /v1/platform/datacenter-asn.

Onde é usadaconsole de plataforma.
Efeitosuma escrita.

Exemplo

curl -X POST 'http://asender-core:8002/platform/datacenter-asn' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /platform/datacenter-asn/{asn} sem schema na spec

DELETE /v1/platform/datacenter-asn/{asn}.

Onde é usadaconsole de plataforma.
Efeitosuma escrita.

Exemplo

curl -X DELETE 'http://asender-core:8002/platform/datacenter-asn/asn_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /platform/listas sem schema na spec

GET /v1/platform/listas.

Onde é usadaasender_runtime, periodicamente.
Efeitosuma 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/metrics sem schema na spec

GET /v1/platform/metrics.

Onde é usadahome do console de plataforma.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-core:8002/platform/metrics' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /platform/tenants sem schema na spec

GET /v1/platform/tenants?q=&limite=N.

Onde é usadaa tela de suporte, para achar a conta do chamado.
Efeitosuma leitura. # Ela vive aqui, e não junto de /v1/tenants GET /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 é usadaGET /v1/relays, com filtro opcional por conta.
Efeitosescreve a resposta HTTP.

Parâmetros

nomeemobrigatóriodescrição
tenant_idquery não

Respostas

códigosignificado
200OK
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 é usadaPOST /v1/relays.
Efeitosescreve a resposta HTTP. A master key chega em claro e é cifrada ANTES de qualquer escrita.

Respostas

códigosignificado
201Registrado.
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.

Exemplo

curl -X POST 'http://asender-core:8002/v1/relays' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/relays/{id}

Remove um relay.

Onde é usadaDELETE /v1/relays/{id}.
Efeitosescreve a resposta HTTP.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
204Removido.
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 DELETE 'http://asender-core:8002/v1/relays/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/relays/{id}

Um relay.

Onde é usadaGET /v1/relays/{id}.
Efeitosescreve a resposta HTTP.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200OK
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/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 é usadaPOST /v1/relays/{id}/health.
Efeitosescreve 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

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200Sondado; 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`.
502Erro repassado do relay (`relayclient.APIError` preserva o status upstream).

Exemplo

curl -X POST 'http://asender-core:8002/v1/relays/id_AQUI/health-check' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/relays/{id}/provision

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 é usadaPOST /v1/relays/{id}/provision.
Efeitosescreve a resposta HTTP. Decifra a master key só no momento da chamada e não a devolve na resposta.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
201Provisionado.
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`.

Exemplo

curl -X POST 'http://asender-core:8002/v1/relays/id_AQUI/provision' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

tenant-tree

GET /v1/tenants/{id}/ancestors

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 é usadaGET /v1/tenants/{id}/ancestors. Fluxo do dado: path {id} → TreeService.Ancestors → closure → JSON.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
200OK
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 é usadaPOST /v1/tenants/{id}/children. Fluxo do dado: body {name,slug,monthly_quota} + path {id} → TreeService.CreateChild → core.tenants + closure → 201.
Efeitoscria tenant kind='leaf' com parent_id = tenant do path.
Erros400 body inválido/nome vazio; 404 pai inexistente; 409 slug duplicado.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
201Criada.
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.

Exemplo

curl -X POST 'http://asender-core:8002/v1/tenants/id_AQUI/children' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/tenants/{id}/descendants

Só a descendência — EXCLUI o próprio nó.

Para rollup, o escopo é o id do path MAIS estes ids. A exclusão é deliberada.

Onde é usadaGET /v1/tenants/{id}/descendants. Fluxo do dado: path {id} + query depth → TreeService.Descendants → closure → JSON.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
depthquery nãoLimite 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ódigosignificado
200OK
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 é usadaPATCH /v1/tenants/{id}/parent. Fluxo do dado: body {parent_id} (public id) + path {id} → TreeService.SetParent → UPDATE parent_id → trigger recalcula a closure → 200.
Efeitosreescreve closure e depth de toda a subárvore movida.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
200Movido.
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.

Exemplo

curl -X PATCH 'http://asender-core:8002/v1/tenants/id_AQUI/parent' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
PATCH /v1/tenants/{id}/quota

Grava ou limpa a quota mensal própria.

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 é usadaPATCH /v1/tenants/{id}/quota. Fluxo do dado: body {monthly_quota} + path {id} → TreeService.SetQuota → core.tenants → 200.
Erros400 campo ausente, tipo errado (sem coerção) ou valor negativo; 404 tenant inexistente.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
200Gravada.
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`.

Exemplo

curl -X PATCH 'http://asender-core:8002/v1/tenants/id_AQUI/quota' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/tenants/{id}/tree

Subárvore em pré-ordem, INCLUINDO o próprio nó.

Cada filho vem logo abaixo do pai; indente por relative_depth.

Onde é usadaGET /v1/tenants/{id}/tree. Fluxo do dado: path {id} + query depth → TreeService.Tree → closure → JSON.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
depthquery nãoLimite 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ódigosignificado
200OK
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}/impersonations sem schema na spec

GET /v1/tenants/{id}/impersonations?limite=N.

Onde é usadaconsulta do CLIENTE sobre a própria conta, e da plataforma.
Efeitosuma 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

nomeemobrigatóriodescrição
user_idquery sim

Respostas

códigosignificado
200OK
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 é usadaPOST /v1/tenants.
Efeitosescreve a resposta HTTP.

Respostas

códigosignificado
201Criado.
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.

Exemplo

curl -X POST 'http://asender-core:8002/v1/tenants' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/tenants/{id}

Remoção LÓGICA do tenant.

Onde é usadaDELETE /v1/tenants/{id}.
Efeitosescreve 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

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
204Removido.
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 DELETE 'http://asender-core:8002/v1/tenants/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /v1/tenants/{id}

Um tenant pelo public id.

A resposta é o objeto do tenant no TOPO, sem chave de recurso — exceção ao padrão aninhado deste serviço.

Onde é usadaGET /v1/tenants/{id}.
Efeitosescreve a resposta HTTP.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
200OK
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 é usadaPATCH /v1/tenants/{id}.
Efeitosescreve 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

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
200OK
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`.

Exemplo

curl -X PATCH 'http://asender-core:8002/v1/tenants/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

usage

GET /v1/tenants/{id}/usage

Contadores de consumo do período.

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 é usadaGET /v1/tenants/{id}/usage.
Efeitosescreve a resposta HTTP.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
periodquery nãoVazio = período corrente.

Respostas

códigosignificado
200Contadores 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.
404O 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 é usadaPOST /internal/v1/tenants/{id}/usage, chamada pelos serviços de envio.
Efeitosescreve a resposta HTTP.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
200Contador 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`.

Exemplo

curl -X POST 'http://asender-core:8002/v1/tenants/id_AQUI/usage/increment' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

vapid

GET /v1/tenants/{id}/vapid

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 é usadaasender-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ídas200 com {public_key, subject}.
Efeitospode 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

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200Chave 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 é usadachamado pelo asender-push-worker antes de entregar cada notificação. Fluxo do dado: worker → aqui → JWT ES256 → worker → push service.
Saídas200 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.

Parâmetros

nomeemobrigatóriodescrição
idpath sim

Respostas

códigosignificado
200Cabeçalho Authorization pronto.
400

Exemplo

curl -X POST 'http://asender-core:8002/v1/tenants/id_AQUI/vapid/sign' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

webhooks

GET /v1/tenants/{id}/webhooks

Endpoints de webhook do tenant.

O secret NÃO volta aqui — só na criação.

Onde é usadaGET /v1/tenants/{id}/webhooks.
Efeitosescreve a resposta HTTP.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
200OK
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 é usadaPOST /v1/tenants/{id}/webhooks.
Efeitosescreve a resposta HTTP. O segredo de assinatura sai em claro UMA vez, nesta resposta.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.

Respostas

códigosignificado
201Criado.
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`.

Exemplo

curl -X POST 'http://asender-core:8002/v1/tenants/id_AQUI/webhooks' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/tenants/{id}/webhooks/{whkId}

Remove o endpoint.

Onde é usadaDELETE /v1/tenants/{id}/webhooks/{whkID}.
Efeitosescreve a resposta HTTP.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
whkIdpath sim

Respostas

códigosignificado
204Removido.
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 DELETE 'http://asender-core:8002/v1/tenants/id_AQUI/webhooks/whkId_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
PATCH /v1/tenants/{id}/webhooks/{whkId}

Altera url, eventos ou estado ativo.

Onde é usadaPATCH /v1/tenants/{id}/webhooks/{whkID}.
Efeitosescreve a resposta HTTP.

Parâmetros

nomeemobrigatóriodescrição
idpath simPublic id (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
whkIdpath sim

Respostas

códigosignificado
200OK
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`.

Exemplo

curl -X PATCH 'http://asender-core:8002/v1/tenants/id_AQUI/webhooks/whkId_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

Messages — campanhas, contatos e envio interno

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 é usadatela de audiência do dashboard, via asender-api. Fluxo do dado: header de tenant + query → repo → messages.contacts → JSON.
Entradasq, list_id, cursor, limit. Query malformada é 422 (parseQuery), não listagem sem filtro.
Saídas200 com contacts, next_cursor e has_more; 404 se list_id não existir no tenant; 422 em parâmetro inválido.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader sim**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a partir do token verificado. Ausente → 400 `MissingTenant`.
qquery não
list_idquery nãoPublic id (`lst_<hex>`). Lista de outro tenant → 404.
cursorquery não
limitquery não

Respostas

códigosignificado
200Pá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 é usadaformulário "novo contato" da tela de audiência. Fluxo do dado: asender-api → aqui → messages.contacts → resposta 201.
Saídas201 com contact; 400 em JSON inválido ou campo desconhecido; 422 em validação (sem email nem telefone, email malformado, email já usado).
Efeitosescreve em messages.contacts.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader sim**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a partir do token verificado. Ausente → 400 `MissingTenant`.

Respostas

códigosignificado
201Criado.
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 POST 'http://asender-messages:8004/v1/contacts' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/contacts/{id}

Remoção LÓGICA (`deleted_at`).

O histórico de mensagens do contato continua íntegro e o email volta a poder ser cadastrado. Já removido → 404.

Onde é usadatela de audiência.
Saídas204 sem corpo; 404 se não existir no tenant ou já estar removido.
Efeitosescreve em messages.contacts (soft delete).

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader sim**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a partir do token verificado. Ausente → 400 `MissingTenant`.
idpath sim

Respostas

códigosignificado
204Removido.
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.

Exemplo

curl -X DELETE 'http://asender-messages:8004/v1/contacts/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
PATCH /v1/contacts/{id}

Alteração parcial (nome, atributos, consent).

email e phone NÃO são patcháveis: mandá-los é campo desconhecido → 400, não silêncio.

Onde é usadaedição inline da tela de audiência e fluxo de descadastro.
Saídas200 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).
Efeitosescreve em messages.contacts.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader sim**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a partir do token verificado. Ausente → 400 `MissingTenant`.
idpath sim

Respostas

códigosignificado
200Atualizado.
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 PATCH 'http://asender-messages:8004/v1/contacts/id_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/contacts/bulk

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 é usadaimport 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ídas200 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).
Efeitosescreve em messages.contacts.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader sim**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a partir do token verificado. Ausente → 400 `MissingTenant`.

Respostas

códigosignificado
200Lote 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.

Exemplo

curl -X POST 'http://asender-messages:8004/v1/contacts/bulk' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

devices

GET /v1/push/devices

Devices do tenant, mais recentes primeiro.

Onde é usadatela de audiência do dashboard, via asender-api. Fluxo do dado: header de tenant + query → repo → messages.contacts → JSON.
Entradasq, list_id, cursor, limit. Query malformada é 422 (parseQuery), não listagem sem filtro.
Saídas200 com contacts, next_cursor e has_more; 404 se list_id não existir no tenant; 422 em parâmetro inválido.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader sim**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a partir do token verificado. Ausente → 400 `MissingTenant`.
cursorquery não
limitquery não

Respostas

códigosignificado
200OK
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 é usadachamado 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ídas200 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.
Efeitosescreve em messages.push_devices.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader sim**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a partir do token verificado. Ausente → 400 `MissingTenant`.

Respostas

códigosignificado
200Token já registrado; atualizado.
201Novo 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.

Exemplo

curl -X POST 'http://asender-messages:8004/v1/push/devices' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

lists

GET /v1/lists

Listas do tenant com contagem de membros.

Onde é usadatela de audiência do dashboard, via asender-api. Fluxo do dado: header de tenant + query → repo → messages.contacts → JSON.
Entradasq, list_id, cursor, limit. Query malformada é 422 (parseQuery), não listagem sem filtro.
Saídas200 com contacts, next_cursor e has_more; 404 se list_id não existir no tenant; 422 em parâmetro inválido.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader sim**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a partir do token verificado. Ausente → 400 `MissingTenant`.
limitquery não

Respostas

códigosignificado
200OK
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 é usadaformulário "novo contato" da tela de audiência. Fluxo do dado: asender-api → aqui → messages.contacts → resposta 201.
Saídas201 com contact; 400 em JSON inválido ou campo desconhecido; 422 em validação (sem email nem telefone, email malformado, email já usado).
Efeitosescreve em messages.contacts.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader sim**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a partir do token verificado. Ausente → 400 `MissingTenant`.

Respostas

códigosignificado
200Slug já existia; atualizada.
201Criada.
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 POST 'http://asender-messages:8004/v1/lists' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/lists/{id}/contacts

Membros paginados da lista.

Onde é usadadetalhe da lista na tela de audiência e pré-visualização de audiência da campanha. Fluxo do dado: contact_list_memberscontacts → asender-api → UI.
Entradascursor, limit.
Saídas200 com contacts, next_cursor e has_more; 404 se a lista não existir no tenant; 422 em parâmetro inválido.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader sim**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a partir do token verificado. Ausente → 400 `MissingTenant`.
idpath sim
cursorquery não
limitquery não

Respostas

códigosignificado
200Pá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 é usadatela de audiência.
Saídas200 com removed e not_found; 404 se a lista não existir; 422 em payload inválido.
Efeitosapaga linhas de messages.contact_list_members. Idempotente.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader sim**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a partir do token verificado. Ausente → 400 `MissingTenant`.
idpath sim

Respostas

códigosignificado
200OK
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 DELETE 'http://asender-messages:8004/v1/lists/id_AQUI/members' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /v1/lists/{id}/members

Associa contatos à lista.

Idempotente; duplicata é ignorada. not_found diz o que NÃO pôde ser feito.

Onde é usadamontagem da audiência de uma campanha, na tela de audiência. Fluxo do dado: asender-api → aqui → messages.contact_list_members.
Saídas200 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.
Efeitosescreve em messages.contact_list_members. Idempotente.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader sim**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a partir do token verificado. Ausente → 400 `MissingTenant`.
idpath sim

Respostas

códigosignificado
200OK
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 POST 'http://asender-messages:8004/v1/lists/id_AQUI/members' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

messages

GET /v1/messages

Página de mensagens do tenant, sem corpo.

Onde é usadatela de audiência do dashboard, via asender-api. Fluxo do dado: header de tenant + query → repo → messages.contacts → JSON.
Entradasq, list_id, cursor, limit. Query malformada é 422 (parseQuery), não listagem sem filtro.
Saídas200 com contacts, next_cursor e has_more; 404 se list_id não existir no tenant; 422 em parâmetro inválido.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader sim**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a partir do token verificado. Ausente → 400 `MissingTenant`.
X-Asender-Tenant-Scopeheader nãoRollup 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.
channelquery não
statusquery não
qquery nãoBusca. `%` e `_` são escapados (`?q=%` devolve 0, não tudo).
cursorquery não
limitquery 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ódigosignificado
200OK
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 é usadachamado 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ídas201 com a lista de mensagens; 200 quando foi replay de idempotency key (nada novo criado); 422 em validação; 400 em JSON inválido.
Efeitosescrita transacional no banco.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader sim**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a partir do token verificado. Ausente → 400 `MissingTenant`.
Idempotency-Keyheader nãoImplementado de fato. Precede o campo do corpo.

Respostas

códigosignificado
200Replay de `idempotency_key` — nada novo foi criado.
201Criado.
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 POST 'http://asender-messages:8004/v1/messages' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/messages/{id}

Mensagem com corpo, metadata e trilha de eventos.

Onde é usadatela de detalhe do envio.
Saídas200 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

nomeemobrigatóriodescrição
X-Asender-Tenantheader sim**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a partir do token verificado. Ausente → 400 `MissingTenant`.
idpath sim

Respostas

códigosignificado
200OK
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 é usadachamado pelos workers de email/sms/push depois de falar com o provedor. Fluxo do dado: worker → aqui → messages + message_events → relatório/timeline.
Saídas204 em sucesso (não há corpo útil a devolver); 404 se a mensagem não existir no tenant; 422 em status desconhecido.
Efeitosescrita transacional. Idempotente: reprocessar não duplica evento.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader sim**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a partir do token verificado. Ausente → 400 `MissingTenant`.
idpath sim

Respostas

códigosignificado
204Aplicado. 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.
500Falha de infra **ou** `status:"sending"` (ver `x-asender-divergence`).

Exemplo

curl -X POST 'http://asender-messages:8004/v1/messages/id_AQUI/status' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

meta

GET /healthz

Liveness. Não toca dependência.

Onde é usadaprobe de liveness do container.
Saídas200 com {"status":"ok"}.

Respostas

códigosignificado
200Vivo.

Exemplo

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ódigosignificado
200Exposição em texto do Prometheus.
503O 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 é usadaprobe de readiness; consumido pelo smoke test, que assere o conteúdo.
Saídas200 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.
Efeitosuma query de ping no Postgres.

Respostas

códigosignificado
200Banco respondeu.
503Banco 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 é usadacartõ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.
Entradasfrom, to (ISO-8601; default últimos 30 dias).
Saídas200 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.
Efeitossó leitura.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader sim**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a partir do token verificado. Ausente → 400 `MissingTenant`.
X-Asender-Tenant-Scopeheader nãoRollup 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.
fromquery nãoRFC 3339 ou `YYYY-MM-DD`. Default: 30 dias antes de `to`.
toquery nãoRFC 3339 ou `YYYY-MM-DD`. Default: agora. **Fim EXCLUSIVO**; data pura avança para a meia-noite seguinte, cobrindo o dia inteiro.

Respostas

códigosignificado
200OK
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 é usadagrá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.
Entradasfrom, to, channel (email|sms|push; vazio = todos), interval (day|hour; default day).
Saídas200 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).
Efeitossó leitura.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader sim**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a partir do token verificado. Ausente → 400 `MissingTenant`.
X-Asender-Tenant-Scopeheader nãoRollup 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.
fromquery nãoRFC 3339 ou `YYYY-MM-DD`. Default: 30 dias antes de `to`.
toquery nãoRFC 3339 ou `YYYY-MM-DD`. Default: agora. **Fim EXCLUSIVO**; data pura avança para a meia-noite seguinte, cobrindo o dia inteiro.
channelquery não
intervalquery nãoCom `hour` a janela não pode passar de 31 dias (senão 422).

Respostas

códigosignificado
200OK
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 é usadatabela de templates da tela de relatórios, via asender-api GET /api/reports/top-templates. Fluxo do dado: query → repo.ReportsRepo.TopTemplates (messagestemplates) → JSON → asender-api → tabela.
Entradasfrom, to, limit (default 10, teto 50 aplicado no repositório).
Saídas200 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.
Efeitossó leitura.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader sim**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a partir do token verificado. Ausente → 400 `MissingTenant`.
X-Asender-Tenant-Scopeheader nãoRollup 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.
fromquery nãoRFC 3339 ou `YYYY-MM-DD`. Default: 30 dias antes de `to`.
toquery nãoRFC 3339 ou `YYYY-MM-DD`. Default: agora. **Fim EXCLUSIVO**; data pura avança para a meia-noite seguinte, cobrindo o dia inteiro.
limitquery não

Respostas

códigosignificado
200OK
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 é usadatela de audiência do dashboard, via asender-api. Fluxo do dado: header de tenant + query → repo → messages.contacts → JSON.
Entradasq, list_id, cursor, limit. Query malformada é 422 (parseQuery), não listagem sem filtro.
Saídas200 com contacts, next_cursor e has_more; 404 se list_id não existir no tenant; 422 em parâmetro inválido.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader sim**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a partir do token verificado. Ausente → 400 `MissingTenant`.
limitquery não

Respostas

códigosignificado
200OK
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 é usadachamado 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ídas200 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.
Efeitosescreve em messages.push_devices.

Parâmetros

nomeemobrigatóriodescrição
X-Asender-Tenantheader sim**Obrigatório.** Única origem do tenant. Preenchido pelo `asender-api` a partir do token verificado. Ausente → 400 `MissingTenant`.

Respostas

códigosignificado
200Gravado.
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 POST 'http://asender-messages:8004/v1/templates' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

CRM — jornadas, alertas e destinos interno

Alcançável apenas pela malha privada — não é superfície pública.

http://asender-crm:8005 · 42 operações

alertas

GET /v1/alertas sem schema na spec

GET /v1/alertas.

Onde é usadatela de alertas.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-crm:8005/v1/alertas'
POST /v1/alertas sem schema na spec

POST /v1/alertas.

Onde é usadatela de alertas.
Efeitosuma escrita.

Exemplo

curl -X POST 'http://asender-crm:8005/v1/alertas' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/alertas/{id} sem schema na spec

DELETE /v1/alertas/{id}.

Onde é usadatela de alertas.
Efeitosuma escrita.

Exemplo

curl -X DELETE 'http://asender-crm:8005/v1/alertas/id_AQUI'
PUT /v1/alertas/{id} sem schema na spec

PUT /v1/alertas/{id}.

Onde é usadatela de alertas.
Efeitosuma 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.

Exemplo

curl -X PUT 'http://asender-crm:8005/v1/alertas/id_AQUI' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/alertas/eventos sem schema na spec

GET /v1/alertas/eventos?limite=N.

Onde é usadatela de alertas — a linha do tempo.
Efeitosuma 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/retencao sem schema na spec

GET /v1/configuracoes/retencao.

Onde é usadatela de configurações da conta.
Efeitosuma 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/retencao sem schema na spec

PUT /v1/configuracoes/retencao com `{"dias": 90}` ou `{"dias": null}`.

Onde é usadatela de configurações da conta.
Efeitosuma 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.

Exemplo

curl -X PUT 'http://asender-crm:8005/v1/configuracoes/retencao' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

contatos

GET /v1/contatos sem schema na spec

GET /v1/alertas.

Onde é usadatela de alertas.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-crm:8005/v1/contatos'
POST /v1/contatos sem schema na spec

POST /v1/alertas.

Onde é usadatela de alertas.
Efeitosuma escrita.

Exemplo

curl -X POST 'http://asender-crm:8005/v1/contatos' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/contatos/{id} sem schema na spec

DELETE /v1/alertas/{id}.

Onde é usadatela de alertas.
Efeitosuma escrita.

Exemplo

curl -X DELETE 'http://asender-crm:8005/v1/contatos/id_AQUI'
GET /v1/contatos/{id} sem schema na spec

GET /v1/contatos/{id}/consentimento.

Onde é usadatela de contato, seção de preferências.
Efeitosuma 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 é usadatela de alertas.
Efeitosuma 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.

Exemplo

curl -X PATCH 'http://asender-crm:8005/v1/contatos/id_AQUI' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/contatos/{id}/atividades sem schema na spec

GET /v1/alertas.

Onde é usadatela de alertas.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-crm:8005/v1/contatos/id_AQUI/atividades'
POST /v1/contatos/{id}/atividades sem schema na spec

POST /v1/alertas.

Onde é usadatela de alertas.
Efeitosuma escrita.

Exemplo

curl -X POST 'http://asender-crm:8005/v1/contatos/id_AQUI/atividades' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/contatos/{id}/consentimento sem schema na spec

GET /v1/contatos/{id}/consentimento.

Onde é usadatela de contato, seção de preferências.
Efeitosuma 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}/consentimento sem schema na spec

PUT /v1/contatos/{id}/consentimento.

Onde é usadatela de contato, e o link de descadastro via BFF.
Efeitoscria 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.

Exemplo

curl -X PUT 'http://asender-crm:8005/v1/contatos/id_AQUI/consentimento' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/contatos/exportar.csv sem schema na spec

GET /v1/contatos/exportar.csv?limite=N.

Onde é usadabotão "exportar" da lista de contatos.
Efeitosuma 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'

destinos

GET /v1/destinos sem schema na spec

GET /v1/alertas.

Onde é usadatela de alertas.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-crm:8005/v1/destinos'
POST /v1/destinos sem schema na spec

POST /v1/alertas.

Onde é usadatela de alertas.
Efeitosuma escrita.

Exemplo

curl -X POST 'http://asender-crm:8005/v1/destinos' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/destinos/{id} sem schema na spec

DELETE /v1/alertas/{id}.

Onde é usadatela de alertas.
Efeitosuma escrita.

Exemplo

curl -X DELETE 'http://asender-crm:8005/v1/destinos/id_AQUI'
PUT /v1/destinos/{id} sem schema na spec

PUT /v1/alertas/{id}.

Onde é usadatela de alertas.
Efeitosuma 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.

Exemplo

curl -X PUT 'http://asender-crm:8005/v1/destinos/id_AQUI' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/destinos/{id}/entregas sem schema na spec

GET /v1/destinos/{id}/entregas?limite=N.

Onde é usadadiagnóstico da tela de integrações.
Efeitosuma 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}/testar sem schema na spec

POST /v1/destinos/{id}/testar.

Onde é usadabotão "testar" da tela de integrações.
Efeitoscria 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.

Exemplo

curl -X POST 'http://asender-crm:8005/v1/destinos/id_AQUI/testar' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

healthz

GET /healthz sem schema na spec

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 é usadarota pública /healthz.
Efeitosescreve JSON na resposta.

Exemplo

curl -X GET 'http://asender-crm:8005/healthz'

integracoes-de-conversao

GET /v1/integracoes-de-conversao sem schema na spec

GET /v1/alertas.

Onde é usadatela de alertas.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-crm:8005/v1/integracoes-de-conversao'
POST /v1/integracoes-de-conversao sem schema na spec

POST /v1/alertas.

Onde é usadatela de alertas.
Efeitosuma escrita.

Exemplo

curl -X POST 'http://asender-crm:8005/v1/integracoes-de-conversao' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/integracoes-de-conversao/{id} sem schema na spec

DELETE /v1/alertas/{id}.

Onde é usadatela de alertas.
Efeitosuma escrita.

Exemplo

curl -X DELETE 'http://asender-crm:8005/v1/integracoes-de-conversao/id_AQUI'
PUT /v1/integracoes-de-conversao/{id} sem schema na spec

PUT /v1/alertas/{id}.

Onde é usadatela de alertas.
Efeitosuma 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.

Exemplo

curl -X PUT 'http://asender-crm:8005/v1/integracoes-de-conversao/id_AQUI' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/integracoes-de-conversao/{id}/entregas sem schema na spec

GET /v1/destinos/{id}/entregas?limite=N.

Onde é usadadiagnóstico da tela de integrações.
Efeitosuma 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}/testar sem schema na spec

POST /v1/destinos/{id}/testar.

Onde é usadabotão "testar" da tela de integrações.
Efeitoscria 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.

Exemplo

curl -X POST 'http://asender-crm:8005/v1/integracoes-de-conversao/id_AQUI/testar' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

jornadas

GET /v1/jornadas sem schema na spec

GET /v1/alertas.

Onde é usadatela de alertas.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-crm:8005/v1/jornadas'
POST /v1/jornadas sem schema na spec

POST /v1/alertas.

Onde é usadatela de alertas.
Efeitosuma escrita.

Exemplo

curl -X POST 'http://asender-crm:8005/v1/jornadas' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/jornadas/{id} sem schema na spec

GET /v1/contatos/{id}/consentimento.

Onde é usadatela de contato, seção de preferências.
Efeitosuma 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}/ativa sem schema na spec

PUT /v1/jornadas/{id}/ativa.

Onde é usadapainel, interruptor da jornada.
Efeitosmuda 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.

Exemplo

curl -X PUT 'http://asender-crm:8005/v1/jornadas/id_AQUI/ativa' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

readyz

GET /readyz sem 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 é usadarota pública /readyz.
Efeitosescreve 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/segmentos sem schema na spec

GET /v1/alertas.

Onde é usadatela de alertas.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-crm:8005/v1/segmentos'
POST /v1/segmentos sem schema na spec

POST /v1/alertas.

Onde é usadatela de alertas.
Efeitosuma escrita.

Exemplo

curl -X POST 'http://asender-crm:8005/v1/segmentos' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/segmentos/{id} sem schema na spec

DELETE /v1/alertas/{id}.

Onde é usadatela de alertas.
Efeitosuma escrita.

Exemplo

curl -X DELETE 'http://asender-crm:8005/v1/segmentos/id_AQUI'
GET /v1/segmentos/{id} sem schema na spec

GET /v1/contatos/{id}/consentimento.

Onde é usadatela de contato, seção de preferências.
Efeitosuma 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 é usadatela de alertas.
Efeitosuma 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.

Exemplo

curl -X PUT 'http://asender-crm:8005/v1/segmentos/id_AQUI' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/segmentos/previa sem schema na spec

POST /v1/segmentos/previa.

Onde é usadaenquanto o usuário monta os critérios na tela.
Efeitosuma 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".

Exemplo

curl -X POST 'http://asender-crm:8005/v1/segmentos/previa' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

uso

GET /v1/uso sem schema na spec

GET /v1/uso?periodo=YYYY-MM — números locais do CRM mais a quota e os contadores do core.

Onde é usadatela de conta/plano.
Efeitosuma 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/marca sem schema na spec

GET /v1/paginas/{id}/experimento.

Onde é usadatela de A/B do painel.
Efeitosuma 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/marca sem schema na spec

PUT /v1/configuracoes/marca — os quatro campos de uma vez.

Onde é usadatela de marca do painel.
Efeitosuma 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.

Exemplo

curl -X PUT 'http://asender-pages:8006/v1/configuracoes/marca' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

dominios

GET /v1/dominios sem schema na spec

GET /v1/dominios.

Onde é usadatela de domínios.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-pages:8006/v1/dominios'
POST /v1/dominios sem schema na spec

POST /v1/dominios.

Onde é usadatela de domínios.
Efeitosuma 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.

Exemplo

curl -X POST 'http://asender-pages:8006/v1/dominios' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/dominios/{id} sem schema na spec

DELETE /v1/dominios/{id}.

Onde é usadatela de domínios.
Efeitosuma 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ê.

Exemplo

curl -X DELETE 'http://asender-pages:8006/v1/dominios/id_AQUI'
POST /v1/dominios/{id}/verificar sem schema na spec

POST /v1/dominios/{id}/verificar.

Onde é usadabotão "verificar" da tela de domínios.
Efeitosuma 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.

Exemplo

curl -X POST 'http://asender-pages:8006/v1/dominios/id_AQUI/verificar' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

formularios

GET /v1/formularios sem schema na spec

GET /v1/dominios.

Onde é usadatela de domínios.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-pages:8006/v1/formularios'
POST /v1/formularios sem schema na spec

POST /v1/dominios.

Onde é usadatela de domínios.
Efeitosuma 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.

Exemplo

curl -X POST 'http://asender-pages:8006/v1/formularios' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/formularios/{id} sem schema na spec

DELETE /v1/dominios/{id}.

Onde é usadatela de domínios.
Efeitosuma 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ê.

Exemplo

curl -X DELETE 'http://asender-pages:8006/v1/formularios/id_AQUI'
GET /v1/formularios/{id} sem schema na spec

GET /v1/paginas/{id}/experimento.

Onde é usadatela de A/B do painel.
Efeitosuma 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'
PUT /v1/formularios/{id} sem schema na spec

PUT /v1/formularios/{id}.

Onde é usadaedição.
Efeitosescreve a linha.

Exemplo

curl -X PUT 'http://asender-pages:8006/v1/formularios/id_AQUI' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

healthz

GET /healthz sem schema na spec

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 é usadarota pública /healthz.
Efeitosescreve JSON na resposta.

Exemplo

curl -X GET 'http://asender-pages:8006/healthz'

ia

POST /v1/ia/gerar-pagina sem schema na spec

POST /v1/paginas/gerar — manda o pedido ao provedor, saneia a saída e devolve o template.

Onde é usadabotão "gerar com IA" do editor.
Efeitosuma 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).

Exemplo

curl -X POST 'http://asender-pages:8006/v1/ia/gerar-pagina' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

internal

GET /internal/v1/formularios/{id} sem schema na spec

GET /internal/v1/formularios/{id}.

Onde é usadachamado pelo asender_runtime para validar uma submissão.
Efeitosuma 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/paginas sem schema na spec

GET /internal/v1/paginas?dominio=&slug=.

Onde é usadachamado pelo asender_runtime a cada requisição de página pública.
Efeitosuma 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}/cliques sem schema na spec

POST /internal/v1/paginas/{id}/cliques.

Onde é usadadescarga periódica do contador em memória do asender_runtime.
Efeitosuma 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.

Exemplo

curl -X POST 'http://asender-pages:8006/internal/v1/paginas/id_AQUI/cliques' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /internal/v1/paginas/publicadas sem schema na spec

GET /internal/v1/paginas/publicadas?dominio=.

Onde é usadachamado pelo asender_runtime para montar /sitemap.xml.
Efeitosuma 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/redirects sem schema na spec

GET /internal/v1/redirects?dominio=&slug=.

Onde é usadachamado pelo asender_runtime a cada clique.
Efeitosuma 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/cliques sem schema na spec

POST /internal/v1/redirects/cliques.

Onde é usadadescarga periódica do contador em memória do runtime.
Efeitosuma 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.

Exemplo

curl -X POST 'http://asender-pages:8006/internal/v1/redirects/cliques' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /internal/v1/tls/authorize sem schema na spec

GET /internal/v1/tls/authorize?host=.

Onde é usadao servidor de borda, no meio do handshake TLS.
Efeitosuma 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/midias sem schema na spec

GET /v1/dominios.

Onde é usadatela de domínios.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-pages:8006/v1/midias'
POST /v1/midias sem schema na spec

POST /v1/midias (multipart, campo `arquivo`).

Onde é usadabotão de upload do editor.
Efeitosescreve 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.

Exemplo

curl -X POST 'http://asender-pages:8006/v1/midias' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/midias/{id} sem schema na spec

DELETE /v1/dominios/{id}.

Onde é usadatela de domínios.
Efeitosuma 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ê.

Exemplo

curl -X DELETE 'http://asender-pages:8006/v1/midias/id_AQUI'
POST /v1/midias/de-url sem schema na spec

POST /v1/midias/de-url.

Onde é usadacolar o endereço de uma imagem no editor.
Efeitosuma 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.

Exemplo

curl -X POST 'http://asender-pages:8006/v1/midias/de-url' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

paginas

GET /v1/paginas sem schema na spec

GET /v1/dominios.

Onde é usadatela de domínios.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-pages:8006/v1/paginas'
POST /v1/paginas sem schema na spec

POST /v1/dominios.

Onde é usadatela de domínios.
Efeitosuma 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.

Exemplo

curl -X POST 'http://asender-pages:8006/v1/paginas' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/paginas/{id} sem schema na spec

GET /v1/paginas/{id}/experimento.

Onde é usadatela de A/B do painel.
Efeitosuma 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 é usadatela de páginas do painel (renomear) e configurações da página.
Efeitosuma 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.

Exemplo

curl -X PATCH 'http://asender-pages:8006/v1/paginas/id_AQUI' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/paginas/{id}/ab/promover sem schema na spec

POST /v1/paginas/{id}/ab/promover.

Onde é usadabotão "promover vencedora" e botão "iniciar teste" da tela de A/B.
Efeitosuma 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.

Exemplo

curl -X POST 'http://asender-pages:8006/v1/paginas/id_AQUI/ab/promover' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/paginas/{id}/cliques sem schema na spec

GET /v1/redirects/{id}/cliques.

Onde é usadarelatório da tela de links curtos.
Efeitosuma 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}/experimento sem schema na spec

GET /v1/paginas/{id}/experimento.

Onde é usadatela de A/B do painel.
Efeitosuma 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-stop sem schema na spec

PUT /v1/paginas/{id}/experimento/auto-stop.

Onde é usadatela de A/B.
Efeitosuma 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.

Exemplo

curl -X PUT 'http://asender-pages:8006/v1/paginas/id_AQUI/experimento/auto-stop' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/paginas/{id}/publicar sem schema na spec

POST /v1/paginas/{id}/publicar.

Onde é usadabotão publicar.
Efeitosmove o ponteiro da página; o visitante passa a ver esta versão.

Exemplo

curl -X POST 'http://asender-pages:8006/v1/paginas/id_AQUI/publicar' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/paginas/{id}/roteamento sem schema na spec

DELETE /v1/dominios/{id}.

Onde é usadatela de domínios.
Efeitosuma 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ê.

Exemplo

curl -X DELETE 'http://asender-pages:8006/v1/paginas/id_AQUI/roteamento'
GET /v1/paginas/{id}/roteamento sem schema na spec

GET /v1/paginas/{id}/experimento.

Onde é usadatela de A/B do painel.
Efeitosuma 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'
PUT /v1/paginas/{id}/roteamento sem schema na spec

POST /v1/paginas/{id}/versoes.

Onde é usadabotão salvar do editor.
Efeitoscria uma versão.

Exemplo

curl -X PUT 'http://asender-pages:8006/v1/paginas/id_AQUI/roteamento' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/paginas/{id}/variantes sem schema na spec

GET /v1/paginas/{id}/variantes.

Onde é usadatela de A/B.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-pages:8006/v1/paginas/id_AQUI/variantes'
POST /v1/paginas/{id}/variantes sem schema na spec

POST /v1/paginas/{id}/variantes.

Onde é usadabotão "nova variante" da tela de A/B.
Efeitosuma 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.

Exemplo

curl -X POST 'http://asender-pages:8006/v1/paginas/id_AQUI/variantes' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/paginas/{id}/variantes/{varianteID} sem schema na spec

DELETE /v1/paginas/{id}/variantes/{varianteID}.

Onde é usadatela de A/B.
Efeitosuma escrita.

Exemplo

curl -X DELETE 'http://asender-pages:8006/v1/paginas/id_AQUI/variantes/varianteID_AQUI'
PUT /v1/paginas/{id}/variantes/{varianteID} sem schema na spec

PUT /v1/paginas/{id}/variantes/{varianteID}.

Onde é usadaajustar peso ou versão na tela de A/B.
Efeitosuma escrita.

Exemplo

curl -X PUT 'http://asender-pages:8006/v1/paginas/id_AQUI/variantes/varianteID_AQUI' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /v1/paginas/{id}/versoes sem schema na spec

POST /v1/paginas/{id}/versoes.

Onde é usadabotão salvar do editor.
Efeitoscria uma versão.

Exemplo

curl -X POST 'http://asender-pages:8006/v1/paginas/id_AQUI/versoes' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

public

GET /v1/public/midias/{id} sem schema na spec

GET e HEAD /v1/public/midias/{id}.

Onde é usadaa tag <img> de uma landing publicada.
Efeitosabre 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 /readyz sem 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 é usadarota pública /readyz.
Efeitosescreve 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/redirects sem schema na spec

GET /v1/dominios.

Onde é usadatela de domínios.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-pages:8006/v1/redirects'
POST /v1/redirects sem schema na spec

POST /v1/dominios.

Onde é usadatela de domínios.
Efeitosuma 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.

Exemplo

curl -X POST 'http://asender-pages:8006/v1/redirects' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/redirects/{id} sem schema na spec

DELETE /v1/dominios/{id}.

Onde é usadatela de domínios.
Efeitosuma 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ê.

Exemplo

curl -X DELETE 'http://asender-pages:8006/v1/redirects/id_AQUI'
GET /v1/redirects/{id} sem schema na spec

GET /v1/paginas/{id}/experimento.

Onde é usadatela de A/B do painel.
Efeitosuma 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'
PUT /v1/redirects/{id} sem schema na spec

PUT /v1/formularios/{id}.

Onde é usadaedição.
Efeitosescreve a linha.

Exemplo

curl -X PUT 'http://asender-pages:8006/v1/redirects/id_AQUI' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/redirects/{id}/cliques sem schema na spec

GET /v1/redirects/{id}/cliques.

Onde é usadarelatório da tela de links curtos.
Efeitosuma 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 é usadarota curinga do runtime, a de maior tráfego do sistema.
Efeitosconsulta 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 é usadaPOST no caminho da própria página.
Efeitosos 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.

Exemplo

curl -X POST 'http://asender-runtime:8007/slug_AQUI' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

analytics

GET /v1/analytics/ab sem schema na spec

GET /v1/analytics/ab?pagina=.

Onde é usadatela do experimento.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-runtime:8007/v1/analytics/ab'
GET /v1/analytics/breakdown sem schema na spec

GET /v1/analytics/breakdown?dimensao=utm_source&…

Onde é usadatela de origens.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-runtime:8007/v1/analytics/breakdown'
GET /v1/analytics/events sem schema na spec

GET /v1/analytics/events?pagina=&limite=.

Onde é usadatela de depuração da instrumentação.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-runtime:8007/v1/analytics/events'
GET /v1/analytics/heatmap sem schema na spec

GET /v1/analytics/heatmap?pagina=&lado=.

Onde é usadatela de mapa de calor.
Efeitosuma 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/live sem schema na spec

GET /v1/analytics/live — sessões e eventos da janela curta.

Onde é usadacabeçalho da tela ao vivo, no primeiro carregamento.
Efeitosuma 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/stream sem schema na spec

GET /v1/analytics/live/stream — Server-Sent Events.

Onde é usadatela ao vivo.
Efeitosmanté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/overview sem schema na spec

GET /v1/analytics/overview?pagina=&de=&ate=.

Onde é usadatela inicial de analytics.
Efeitosduas 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 é usadatela de sessão — o que aquela visita fez, em ordem.
Efeitosuma 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-quality sem schema na spec

GET /v1/analytics/traffic-quality.

Onde é usadatela de qualidade — "esse tráfego pago é gente?".
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-runtime:8007/v1/analytics/traffic-quality'

collect

POST /collect sem schema na spec

POST /collect — abre a credencial, normaliza cada evento e publica.

Onde é usadarota pública, a de maior volume do sistema depois da própria página.
Efeitospublica 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.

Exemplo

curl -X POST 'http://asender-runtime:8007/collect' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

f

POST /f/{formID} sem schema na spec

valida a ORIGEM contra os domínios verificados do tenant, ecoa os cabeçalhos de CORS e chama o mesmo pipeline.

Onde é usadarota pública, chamada de outro domínio pelo navegador.
Efeitosos 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.

Exemplo

curl -X POST 'http://asender-runtime:8007/f/formID_AQUI' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

forms

POST /v1/forms/{formID}/submissions sem schema na spec

autentica por API key (resolvida pelo middleware), lê o JSON e chama o pipeline.

Onde é usadarota autenticada do runtime.
Efeitosos 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.

Exemplo

curl -X POST 'http://asender-runtime:8007/v1/forms/formID_AQUI/submissions' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

healthz

GET /healthz sem schema na spec

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 é usadarota pública /healthz.
Efeitosescreve JSON na resposta.

Exemplo

curl -X GET 'http://asender-runtime:8007/healthz'

leads

POST /v1/leads sem schema na spec

autentica por API key (resolvida pelo middleware), lê o JSON e chama o pipeline.

Onde é usadarota autenticada do runtime.
Efeitosos 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.

Exemplo

curl -X POST 'http://asender-runtime:8007/v1/leads' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

pixel.js

GET /pixel.js sem schema na spec

GET /pixel.js.

Onde é usadatoda página publicada, uma vez por visita (depois é cache).
Efeitosescreve 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/pixels sem schema na spec

GET /v1/pixels.

Onde é usadatela de pixels.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-runtime:8007/v1/pixels'
POST /v1/pixels sem schema na spec

POST /v1/pixels.

Onde é usadatela de pixels.
Efeitosuma escrita.

Exemplo

curl -X POST 'http://asender-runtime:8007/v1/pixels' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/pixels/{id} sem schema na spec

DELETE /v1/pixels/{id}.

Onde é usadatela de detalhe.
Efeitosuma escrita; a coleta daquele pixel para.

Exemplo

curl -X DELETE 'http://asender-runtime:8007/v1/pixels/id_AQUI'
GET /v1/pixels/{id} sem schema na spec

GET /v1/pixels/{id}.

Onde é usadatela de detalhe do pixel.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-runtime:8007/v1/pixels/id_AQUI'
PATCH /v1/pixels/{id} sem schema na spec

PATCH /v1/pixels/{id}.

Onde é usadatela de detalhe.
Efeitosuma 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.

Exemplo

curl -X PATCH 'http://asender-runtime:8007/v1/pixels/id_AQUI' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/pixels/{id}/conversions sem schema na spec

GET /v1/pixels/{id}/conversions.

Onde é usadatela de detalhe.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-runtime:8007/v1/pixels/id_AQUI/conversions'
POST /v1/pixels/{id}/conversions sem schema na spec

POST /v1/pixels/{id}/conversions.

Onde é usadatela de detalhe.
Efeitosuma escrita.

Exemplo

curl -X POST 'http://asender-runtime:8007/v1/pixels/id_AQUI/conversions' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/pixels/{id}/conversions/{cid} sem schema na spec

DELETE /v1/pixels/{id}/conversions/{cid}.

Onde é usadatela de detalhe.
Efeitosuma escrita.

Exemplo

curl -X DELETE 'http://asender-runtime:8007/v1/pixels/id_AQUI/conversions/cid_AQUI'
PATCH /v1/pixels/{id}/conversions/{cid} sem schema na spec

PATCH /v1/pixels/{id}/conversions/{cid}.

Onde é usadatela de detalhe.
Efeitosuma escrita.

Exemplo

curl -X PATCH 'http://asender-runtime:8007/v1/pixels/id_AQUI/conversions/cid_AQUI' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/pixels/{id}/domains sem schema na spec

GET /v1/pixels/{id}/domains.

Onde é usadatela de detalhe.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-runtime:8007/v1/pixels/id_AQUI/domains'
POST /v1/pixels/{id}/domains sem schema na spec

POST /v1/pixels/{id}/domains.

Onde é usadatela de detalhe.
Efeitosuma escrita — e, a partir dela, o /collect ecoa CORS para aquele host.

Exemplo

curl -X POST 'http://asender-runtime:8007/v1/pixels/id_AQUI/domains' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /v1/pixels/{id}/domains/{domainId} sem schema na spec

DELETE /v1/pixels/{id}/domains/{domainId}.

Onde é usadatela de detalhe.
Efeitosuma escrita — a origem para de coletar na hora.

Exemplo

curl -X DELETE 'http://asender-runtime:8007/v1/pixels/id_AQUI/domains/domainId_AQUI'
PATCH /v1/pixels/{id}/domains/{domainId} sem schema na spec

PATCH /v1/pixels/{id}/domains/{domainId}.

Onde é usadatela de detalhe.
Efeitosuma escrita.

Exemplo

curl -X PATCH 'http://asender-runtime:8007/v1/pixels/id_AQUI/domains/domainId_AQUI' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

r

GET /r/{slug} sem schema na spec

resolve host+caminho na página publicada e devolve o HTML renderizado.

Onde é usadarota curinga do runtime, a de maior tráfego do sistema.
Efeitosconsulta 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 é usadarota curinga do runtime, a de maior tráfego do sistema.
Efeitosconsulta 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 é usadaPOST no caminho da própria página.
Efeitosos 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.

Exemplo

curl -X POST 'http://asender-runtime:8007/' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

readyz

GET /readyz sem 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 é usadarota pública /readyz.
Efeitosescreve 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 /replay sem schema na spec

POST /replay — abre a credencial, saneia e publica.

Onde é usadarota pública, chamada pelo pixel a cada poucos segundos de uma sessão gravada.
Efeitospublica 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.

Exemplo

curl -X POST 'http://asender-runtime:8007/replay' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /v1/replay/sessions sem schema na spec

GET /v1/replay/sessions?limite=N.

Onde é usadatela de replay.
Efeitosuma leitura.

Exemplo

curl -X GET 'http://asender-runtime:8007/v1/replay/sessions'
GET /v1/replay/sessions/{id}/events sem schema na spec

GET /v1/replay/sessions/{id}/events.

Onde é usadaplayer da tela de replay.
Efeitosuma 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.txt sem schema na spec

libera a indexação e aponta o sitemap do MESMO host.

Onde é usadarota pública.
Efeitosnenhum. # 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.xml sem schema na spec

lista as páginas PUBLICADAS daquele host.

Onde é usadarota pública.
Efeitosuma 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 /version sem schema na spec

nome do serviço, versão e o commit que gerou o binário.

Onde é usadarota pública, usada por ops e por quem investiga incidente.
Efeitosescreve 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}/autocreate sem schema na spec

confere posse do domínio, lê {enabled} e grava o flag.

Onde é usadaPUT /domains/{id}/autocreate. Alternativa ao catch-all.

Exemplo

curl -X PUT 'http://asender-mail:8016/domains/id_AQUI/autocreate' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
PUT /domains/{id}/catchall

Definir/limpar catch-all

Onde é usadaPUT /domains/{id}/catchall.

Respostas

códigosignificado
200ok

Exemplo

curl -X PUT 'http://asender-mail:8016/domains/id_AQUI/catchall' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /domains/{id}/dns

Registros de DNS a publicar (posse/MX/SPF/DKIM/DMARC)

Onde é usadaGET /domains/{id}/dns.

Respostas

códigosignificado
200ok

Exemplo

curl -X GET 'http://asender-mail:8016/domains/id_AQUI/dns' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /domains/{id}/mailboxes

Listar caixas

Onde é usadaGET /domains/{id}/mailboxes.

Respostas

códigosignificado
200ok

Exemplo

curl -X GET 'http://asender-mail:8016/domains/id_AQUI/mailboxes' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /domains/{id}/mailboxes

Criar caixa

Onde é usadaPOST /domains/{id}/mailboxes.

Respostas

códigosignificado
201criada

Exemplo

curl -X POST 'http://asender-mail:8016/domains/id_AQUI/mailboxes' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /domains/{id}/verify

Verificar publicação no DNS

Onde é usadaPOST /domains/{id}/verify.

Respostas

códigosignificado
200estado

Exemplo

curl -X POST 'http://asender-mail:8016/domains/id_AQUI/verify' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

emails

POST /v1/emails

Enviar e-mail transacional/marketing

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 é usadaPOST /v1/emails. Fluxo: API key -> tenant -> caixa por endereço -> outbound -> relay /raw.

Parâmetros

nomeemobrigatóriodescrição
Idempotency-Keyheader não

Respostas

códigosignificado
200replay idempotente (já enviado antes)
202aceito e entregue
400validação
401API key ausente/inválida
404caixa remetente não é da conta
502envio recusado (piso 18) ou entrega falhou

Exemplo

curl -X POST 'http://asender-mail:8016/v1/emails' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

healthz

GET /healthz sem schema na spec

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 é usadaGET /readyz e /healthz.

Exemplo

curl -X GET 'http://asender-mail:8016/healthz' \
  -H 'Authorization: Bearer SEU_TOKEN'

livez

GET /livez sem schema na spec

Livez responde 200 sempre — o processo está vivo.

Onde é usadaGET /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 é usadaGET /mailboxes/{id}/folders, que desenha o rail.

Respostas

códigosignificado
200ok
404caixa inexistente ou sem acesso

Exemplo

curl -X GET 'http://asender-mail:8016/mailboxes/id_AQUI/folders' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /mailboxes/{id}/grants sem schema na spec

ListarGrants lista as concessões de uma caixa.

Onde é usadaGET /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}/grants sem schema na spec

ConcederGrant concede acesso de um usuário a uma caixa.

Onde é usadaPOST /mailboxes/{id}/grants {user_id, role}. Admin only.

Exemplo

curl -X POST 'http://asender-mail:8016/mailboxes/id_AQUI/grants' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
DELETE /mailboxes/{id}/grants/{userId} sem schema na spec

RevogarGrant remove o acesso de um usuário a uma caixa.

Onde é usadaDELETE /mailboxes/{id}/grants/{userId}. Admin only.

Exemplo

curl -X DELETE 'http://asender-mail:8016/mailboxes/id_AQUI/grants/userId_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN'
GET /mailboxes/{id}/messages

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

nomeemobrigatóriodescrição
folderquery nãokind ('inbox','sent','archive','spam','trash','drafts') ou uuid da pasta; ausente = todas
unreadquery nãosó não-lidas
starredquery nãosó estreladas
qquery nãobusca full-text (assunto, remetente e corpo); acento é dobrado dos dois lados
limitquery não
cursorquery nãoo next_cursor da página anterior (opaco)

Respostas

códigosignificado
200ok
404caixa 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 é usadaGET /mailboxes/{id}/messages/{msgId}. Cross-tenant/outra caixa -> 404.

Respostas

códigosignificado
200ok
404mensagem 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}.
Errospatch vazio -> 400 (não é sucesso silencioso); mensagem de outra caixa -> 404, o mesmo da leitura, que não revela a existência dela.

Respostas

códigosignificado
200aplicado
400corpo inválido ou sem campo algum
404mensagem não é desta caixa

Exemplo

curl -X PATCH 'http://asender-mail:8016/mailboxes/id_AQUI/messages/msgId_AQUI' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
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}/raw sem 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.

Respostas

códigosignificado
200aplicado (possivelmente em parte)
400lista vazia
404caixa inexistente ou sem acesso

Exemplo

curl -X POST 'http://asender-mail:8016/mailboxes/id_AQUI/messages/batch' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /mailboxes/{id}/send sem schema na spec

confere posse (caixa -> domínio -> org -> tenant), monta o pedido, assina com a chave do domínio e entrega pelo relay; grava o outbound_log.

Onde é usadaPOST /mailboxes/{id}/send. Fluxo: JSON -> outbound.Service.Enviar (assina d=domínio) -> relay /raw.

Exemplo

curl -X POST 'http://asender-mail:8016/mailboxes/id_AQUI/send' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /mailboxes/minhas sem schema na spec

ListarMinhasCaixas devolve as caixas CONCEDIDAS ao usuário logado (visão

Onde é usadaGET /mailboxes/minhas. Sem usuário (API key/admin genérico), devolve vazio — o admin usa a listagem por domínio.

Exemplo

curl -X GET 'http://asender-mail:8016/mailboxes/minhas' \
  -H 'Authorization: Bearer SEU_TOKEN'

orgs

GET /orgs

Listar organizações da conta

Onde é usadaGET /orgs.

Respostas

códigosignificado
200ok

Exemplo

curl -X GET 'http://asender-mail:8016/orgs' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /orgs

Criar organização

Onde é usadaPOST /orgs.

Respostas

códigosignificado
201criada

Exemplo

curl -X POST 'http://asender-mail:8016/orgs' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /orgs/{orgID}/domains

Listar domínios da org

Onde é usadaGET /orgs/{orgID}/domains.

Respostas

códigosignificado
200ok

Exemplo

curl -X GET 'http://asender-mail:8016/orgs/orgID_AQUI/domains' \
  -H 'Authorization: Bearer SEU_TOKEN'
POST /orgs/{orgID}/domains

Criar domínio (gera DKIM próprio)

Onde é usadaPOST /orgs/{orgID}/domains.

Respostas

códigosignificado
201criado

Exemplo

curl -X POST 'http://asender-mail:8016/orgs/orgID_AQUI/domains' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
GET /orgs/{orgID}/envios sem schema na spec

o que saiu, para quem, com que resultado e, quando falhou, por quê.

Onde é usadaGET /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-accounts sem 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 é usadamail-admin. Admin only. Se o auth cair, a caixa não é criada órfã (ordem: conta primeiro, depois caixa+grant) — falha limpa.

Exemplo

curl -X POST 'http://asender-mail:8016/orgs/orgID_AQUI/mailbox-accounts' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'
POST /orgs/{orgID}/mailbox-accounts/reset sem schema na spec

ResetarSenha dispara o e-mail de redefinição para a conta da caixa.

Onde é usada POST /orgs/{orgID}/mailbox-accounts/{userId}/reset. Admin only.

Exemplo

curl -X POST 'http://asender-mail:8016/orgs/orgID_AQUI/mailbox-accounts/reset' \
  -H 'Authorization: Bearer SEU_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid-v4>' \
  -d '{ }'

readyz

GET /readyz sem schema na spec

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 é usadaGET /readyz e /healthz.

Exemplo

curl -X GET 'http://asender-mail:8016/readyz' \
  -H 'Authorization: Bearer SEU_TOKEN'