uma leitura no asender-auth. Responde o estado CONFIRMADO, e não "existe segredo": um setup interrompido deixa segredo gravado sem confirmação, e mostrar "2FA ligado" aí faria a pessoa acreditar numa proteção que o login não exige.
Respostas
código
significado
200
OK
401
credencial ausente ou inválida
Exemplo
curl -X GET 'https://api.asender.net/api/auth/2fa' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/auth/2fa
Segunda etapa do login — confirma o código do segundo fator.
Onde é usada
tela /2fa, para onde o login manda quem tem segundo fator.
Efeitos
cria sessão no asender-auth. # Por que esta rota é PÚBLICA Quem chega aqui ainda não tem sessão — é justamente o que ela está tentando obter. Exigir sessão tornaria o 2FA impossível de completar: a armadilha de aplicar a mesma guarda em toda rota "porque é mais seguro". O que protege é o par (user_id, código): o user_id sozinho não abre nada, e o código vale 30 segundos. Sem esta rota, o login de quem tem 2FA ficava sem passo seguinte — a conta ficava inacessível pelo painel.
grava o segredo PENDENTE de confirmação. # A senha é a invariante do piso 17, e ela para AQUI se faltar Para mexer num fator é preciso apresentar um fator diferente dele. O serviço recusa senha vazia, e o gateway recusa antes — não por desconfiança do serviço, mas porque um corpo sem senha é pedido malformado, e mandá-lo adiante gastaria uma viagem para receber a mesma recusa.
Corpo aceito em snake_case ({"email","password"}) porque é o que o asender-auth define e o proxy repassa; a RESPOSTA é PascalCase.
Com 2FA habilitado a resposta é 200 com Token:"" e TwoFactorRequired:true — explícito, para o chamador não confundir com upstream quebrado.
Respostas
código
significado
200
Sessão criada, ou 2FA pendente.
400
`400 ValidationError` — JSON inválido, campo desconhecido ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
Conta bloqueada ou desabilitada.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Revoga a sessão do Bearer apresentado. IDEMPOTENTE.
Revoga no asender-auth; a validação de sessão passou a consultar o estado, então o token deixa de valer (401 no /api/auth/me seguinte).
Esta rota fica FORA do grupo SessionAuth, de propósito — é o único caso em /api/*. Dentro do grupo, o segundo logout (retry, aba duplicada, refresh) morria em 401 no middleware, quebrando a idempotência de uma operação que descreve um ESTADO desejado ("estar fora"), não uma transição.
Consequência observável, e é por isso que a classe declarada é bearer-unverified e não session: o handler exige o header Authorization (sem ele, 401) mas não valida a sessão. Qualquer string não vazia como Bearer — inclusive uma API key ou lixo — recebe 200. Não abre nada (revogar um token que não existe é no-op no asender-auth), mas o 401 desta rota vem do HANDLER, não do middleware: quem for auditar a superfície não pode contá-la como autenticada.
Onde é usada
chamado pelas rotas /api/auth/logout dos DOIS frontends (dashboard e backoffice), que depois apagam o próprio cookie HttpOnly.
Entradas
header Authorization: Bearer <jwt de sessão>.
Saídas
200 {Status:"ok"} com a sessão revogada; 401 sem Bearer; 502 LogoutFailed quando o asender-auth não confirmou.
Efeitos
marca auth.sessions.revoked_at no asender-auth. DEFEITO QUE ISTO FECHA (mesma família do item 17 do _INTEGRACAO-pendente): o handler logava a falha em nível Warn e respondia **200 {"Status":"ok"} assim mesmo. A premissa embutida era "logout é best-effort, o cliente só precisa apagar o cookie" — errada pelo mesmo motivo que a do backoffice: com a revogação por sid no asender-auth, é a resposta desta rota que diz se a sessão morreu ou não. Afirmar ok sem revogar dá ao chamador (e à tela) a garantia de que a sessão acabou quando ela continua aceita, e ainda apaga o rastro: quem lê 200 não procura o Warn no log. Por que 502 e não 500: a falha é do upstream (asender-auth fora, ou erro de banco na revogação), não desta camada — mesmo tratamento dado a todo erro de upstream do BFF. Falta de credencial continua sendo 401 do próprio handler: esta rota fica FORA do grupo SessionAuth de propósito (ver server.go).
Respostas
código
significado
200
Revogado, ou já estava — inclusive para um Bearer que nunca foi sessão.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
TenantId vem do asender-auth e costuma vir vazio — a resolução real do tenant é feita por rota (resolveSessionTenant), a partir dos vínculos no core. Não use este campo como escopo.
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
Exemplo
curl -X GET 'https://api.asender.net/api/auth/me' \
-H 'Authorization: Bearer SEU_TOKEN'
PATCH/api/auth/me
Altera o perfil do usuário da sessão.
Onde é usada
tela de perfil do painel.
Efeitos
escreve no asender-auth; e-mail novo desverifica a conta e dispara a verificação do endereço novo (lá).
Reenvia o e-mail de verificação do usuário da sessão.
Onde é usada
aviso "confirme seu e-mail" do painel.
Efeitos
um e-mail (o guard do auth decide se ele sai fora de prd). O 429 do upstream é REPASSADO como 429: quem pediu demais precisa saber que basta esperar, e um 502 aqui mandaria a pessoa procurar defeito onde não há.
A criação do tenant é best-effort: se falhar, a resposta ainda é 201 com Tenant: null e um campo Note dizendo para repetir via POST /api/tenants. Falha silenciosa não é opção.
Respostas
código
significado
201
Usuário criado.
400
`400 ValidationError` — JSON inválido, campo desconhecido ou tipo errado.
409
`409 Conflict` — email já registrado, slug em uso.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
uma leitura no asender-auth. O token repassado é o da REQUISIÇÃO, e é ele que define de quem é a lista. Não existe parâmetro de usuário: um ?user_id= seria a rota mais barata do sistema para descobrir de onde outra pessoa se conecta.
Respostas
código
significado
200
OK
401
credencial ausente ou inválida
Exemplo
curl -X GET 'https://api.asender.net/api/auth/sessions' \
-H 'Authorization: Bearer SEU_TOKEN'
DELETE/api/auth/sessions/{id}
Revoga uma sessão específica.
Onde é usada
botão "remover" da tela de segurança.
Efeitos
a sessão para de autenticar imediatamente. # Por que a impersonação NÃO passa por aqui A sessão de suporte é somente leitura (ADR-0017), e o middleware de impersonação já recusa escrita. Isto é uma escrita — e derrubar o dispositivo de um cliente durante uma sessão de suporte é exatamente o poder que o ADR tira de quem entra na conta alheia.
carimba email_verified_at no auth. # Também é PÚBLICA Quem abre o link do e-mail pode estar em outro navegador, ou nem ter sessão. Exigir login para verificar o e-mail cria a dependência circular clássica — e a pessoa que mais precisa verificar é justamente a que ainda não conseguiu entrar.
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
q
query
não
list_id
query
não
cursor
query
não
limit
query
não
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Exemplo
curl -X GET 'https://api.asender.net/api/contacts' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/contacts
Cria um contato.
Subscribed ausente vira true — opt-in é o default do cadastro manual.
Onde é usada
formulário de novo contato da tela de audiência.
Saídas
201 com {Contact}; 422 em validação (email/telefone inválidos).
Efeitos
escreve em messages.contacts.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
Respostas
código
significado
201
Criado.
400
`400 ValidationError` — JSON inválido, campo desconhecido ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
413
`413 PayloadTooLarge` — corpo acima de 2 MiB.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Importa até 5000 contatos, com upsert pela identidade natural.
Import parcial é resultado legítimo: itens ruins voltam em Errors com o índice de origem, e o lote não é invalidado por causa deles. Lote vazio ou acima do teto é 422.
Onde é usada
importador CSV da tela de audiência.
Saídas
200 com {Created, Updated, Errors}; 422 quando a lista vem vazia ou acima do teto.
Efeitos
escreve em messages.contacts.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
Respostas
código
significado
200
Lote processado.
400
`400 ValidationError` — JSON inválido, campo desconhecido ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
413
`413 PayloadTooLarge` — corpo acima de 2 MiB.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
cursor
query
não
limit
query
não
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Exemplo
curl -X GET 'https://api.asender.net/api/lists' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/lists
Cria uma lista de contatos.
O upstream faz upsert por slug; este BFF responde sempre 201.
Onde é usada
tela de audiência.
Saídas
201 com {List}; 422 em validação.
Efeitos
escreve em messages.contact_lists.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
Respostas
código
significado
201
Criada (ou atualizada pelo slug).
400
`400 ValidationError` — JSON inválido, campo desconhecido ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
NotFound traz os ids que não existem no tenant — o servidor diz o que NÃO pôde fazer, em vez de descartar em silêncio. Idempotente.
Onde é usada
tela de audiência (seleção múltipla).
Saídas
200 com {Added}; 404 se a lista não é do tenant; 422 se a lista de ids vem vazia.
Efeitos
escreve em messages.contact_list_members.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
id
path
sim
Public id da lista (`lst_<hex>`).
Respostas
código
significado
200
OK
400
`400 ValidationError` — JSON inválido, campo desconhecido ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Channel/Status/Q/Cursor/Limit são aceitos em snake_case (channel, status, q, cursor, limit) e repassados por allowlist — nenhum outro parâmetro atravessa.
Saídas
200 com {Messages, NextCursor, HasMore}; 422 em query malformada.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
channel
query
não
status
query
não
q
query
não
cursor
query
não
limit
query
não
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/api/messages' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/messages/{id}
Detalhe do envio com timeline.
Onde é usada
tela de detalhe do envio.
Saídas
200 com {Message, Events}; 404 quando o id não existe NO TENANT — mensagem de outro tenant também é 404, para não confirmar existência (§22.8).
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
id
path
sim
Public id da mensagem (`msg_<hex>`).
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Exemplo
curl -X GET 'https://api.asender.net/api/messages/id_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/messages/send
Composer do dashboard — envia em qualquer canal.
Corpo em PascalCase, campo a campo espelhando o contrato interno. Campo desconhecido é 400 (DisallowUnknownFields), não silêncio.
Onde é usada
composer da tela de envio do dashboard. Fluxo do dado: cookie de sessão → SessionAuth → Principal → tenant resolvido no core → POST /v1/messages no asender_messages → outbox → NATS → worker.
Saídas
202 com {Messages, ReusedIdempotency}; 422 quando o asender_messages recusa a validação (canal inválido, destinatário vazio, corpo ausente).
Efeitos
escreve mensagens no banco de mensageria.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
Respostas
código
significado
202
Enfileirado.
400
`400 ValidationError` — JSON inválido, campo desconhecido ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
413
`413 PayloadTooLarge` — corpo acima de 2 MiB.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Devices de push do tenant, para o seletor da tela de disparo.
Fonte do seletor de dispositivos em /t/{conta}/send/push. Antes de existir, o caminho respondia 404 e o seletor ficava vazio mesmo com devices no banco — a tela levava o usuário a concluir que a conta não tinha dispositivo nenhum.
Onde é usada
dashboard, tela /send/push. Fluxo do dado: messages.push_devices → asender_messages → aqui → seletor.
Entradas
cursor e limit, na mesma allowlist dos outros List.
Saídas
200 com {Devices, NextCursor, HasMore} (PascalCase, §51). Antes desta rota o caminho respondia 404 e a tela não tinha como listar: dava para disparar digitando o token à mão, mas não escolher um device existente.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
cursor
query
não
limit
query
não
Respostas
código
significado
200
Página de devices, em PascalCase.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
422
Exemplo
curl -X GET 'https://api.asender.net/api/push/devices' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/push/site-config
Pacote de integração de Web Push para o site do cliente.
Devolve, num JSON só, tudo que o site precisa para receber Web Push: chave pública VAPID do tenant, manifest.json, o service worker e o snippet de inscrição — os três já preenchidos com os valores desta instalação. Gerar no servidor evita o chamado clássico do integrador que esqueceu de substituir um <SUA_CHAVE_AQUI>.
Onde é usada
tela de push do dashboard (botão de copiar/baixar cada arquivo). Fluxo do dado: core.vapid_keys → aqui → tela → arquivos no site do cliente → browser → POST /v1/push/devices → messages.push_devices.
Saídas
200 com {PublicKey, ApiBase, Manifest, ServiceWorker, Snippet}.
Efeitos
pode criar o par VAPID do tenant na primeira chamada (no core). ## Por que o servidor gera, em vez de documentar Os três arquivos dependem de dois valores que variam por instalação: a chave pública do tenant e a base da API. Documentação com <SUA_CHAVE_AQUI> produz exatamente um tipo de chamado — o do integrador que esqueceu de substituir, e cujo sintoma é "não chega notificação", sem erro em lugar nenhum. Gerando aqui, o que o cliente cola já está correto.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
Respostas
código
significado
200
Artefatos prontos para o site.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
Exemplo
curl -X GET 'https://api.asender.net/api/push/site-config' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/templates
Templates do tenant.
Onde é usada
dashboard.
Saídas
200 com {Templates}.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
limit
query
não
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Exemplo
curl -X GET 'https://api.asender.net/api/templates' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/templates
Cria ou atualiza um template pelo par (tenant, slug).
Onde é usada
editor de templates do dashboard.
Saídas
200 com {Template}; 422 em validação do asender_messages.
Efeitos
escreve em messages.templates.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
Respostas
código
significado
200
Gravado (upsert — 200 tanto na criação quanto na atualização).
400
`400 ValidationError` — JSON inválido, campo desconhecido ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
413
`413 PayloadTooLarge` — corpo acima de 2 MiB.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
o console, ao abrir — é o que decide entre mostrar o console e mostrar "não disponível".
Efeitos
uma leitura na allowlist. Existe em vez de o console deduzir de outra rota: sem ela, o front descobriria que não é plataforma pelo 404 da PRIMEIRA rota que chamasse — e mostraria um erro de carregamento onde a resposta certa é "esta área não é sua".
Respostas
código
significado
200
OK
401
credencial ausente ou inválida
Exemplo
curl -X GET 'https://api.asender.net/api/admin/me' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/admin/metrics
Métricas agregadas da plataforma.
Respostas
código
significado
200
OK
401
credencial ausente ou inválida
Exemplo
curl -X GET 'https://api.asender.net/api/admin/metrics' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/admin/platform-alerts
Regras de alerta da plataforma.
Respostas
código
significado
200
OK
401
credencial ausente ou inválida
Exemplo
curl -X GET 'https://api.asender.net/api/admin/platform-alerts' \
-H 'Authorization: Bearer SEU_TOKEN'
curl -X GET 'https://api.asender.net/api/admin/platform-alerts/events' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/admin/tenants
Lista as contas, na visão da plataforma.
Onde é usada
tela /impersonate.
Efeitos
uma leitura no core.
Respostas
código
significado
200
OK
401
credencial ausente ou inválida
Exemplo
curl -X GET 'https://api.asender.net/api/admin/tenants' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/admin/tenants/{id}
Detalhe de uma conta, na visão da plataforma.
Onde é usada
console, ao abrir um cliente.
Efeitos
uma leitura no core. # Por que não reusar GET /api/tenants/{id} Aquela rota exige PERTINÊNCIA: o usuário tem de ser membro da conta. Quem opera a plataforma não é membro de nenhuma conta de cliente — e não deve virar, porque virar membro para poder ver é exatamente o atalho que o ADR-0017 existe para impedir. A autorização aqui é a allowlist de plataforma, e a rota é outra.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Respostas
código
significado
200
OK
401
credencial ausente ou inválida
Exemplo
curl -X GET 'https://api.asender.net/api/admin/tenants/id_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
PATCH/api/admin/tenants/{id}
Altera uma conta, na visão da plataforma.
Onde é usada
console.
Efeitos
uma escrita no core. # Isto NÃO é a sessão de suporte A impersonação é somente leitura (ADR-0017) porque entrar na conta é ver o que o cliente vê. Isto é outra coisa: é a plataforma agindo COMO plataforma — suspender uma conta abusiva, por exemplo — e a ação fica no log com o ator. A distinção importa: se a sessão de suporte pudesse escrever, "entrar para ajudar" viraria "entrar para consertar", e o limite que o ADR desenhou desapareceria na prática.
Abre sessão de suporte na conta (somente leitura, ADR-0017).
Onde é usada
tela /impersonate.
Efeitos
uma escrita no core — e, a partir dela, LEITURA da conta alheia. O motivo é obrigatório: uma trilha sem motivo responde "alguém entrou", que é a metade inútil da pergunta que ela existe para responder.
Os três canais SEMPRE aparecem em ByChannel, zerados quando não houve tráfego. As chaves de ByChannel (email/sms/push) ficam em minúsculas porque são DADO, não nome de campo.
Taxas em [0,1], arredondadas a 4 casas, grampeadas no teto (a base pode ter bounced > sent, porque bounce não exige sent_at).
Onde é usada
dashboard de relatórios (cards do topo). Fluxo do dado: sessão → tenant resolvido → asender_messages GET /v1/reports/overview → PascalCase.
Entradas
from/to em ISO-8601 UTC; ausentes = últimos 30 dias (default resolvido pelo asender_messages, não duplicado aqui).
Saídas
200 com {Totals, Rates, ByChannel} — as chaves de ByChannel seguem sendo email/sms/push, porque ali são dado e não nome de campo.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
from
query
não
ISO-8601 (`YYYY-MM-DD` ou RFC 3339). Default: 30 dias antes de `to`.
to
query
não
ISO-8601. Default: agora. **Fim EXCLUSIVO**; data pura cobre o dia inteiro.
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/api/reports/overview' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/reports/timeseries
Série temporal por bucket e canal.
Um ponto por (bucket, canal), buckets vazios inclusos. O campo continua se chamando Day mesmo com interval=hour — é o início do bucket; a granularidade vem em Period.Interval.
Este BFF aceita hour|day|week|month, mas o asender_messages só implementa day|hour: week/month passam a validação daqui e são recusados com 422 pelo upstream.
Onde é usada
dashboard de relatórios (gráfico principal).
Entradas
from, to, channel (email|sms|push), interval (hour|day|week|month; default day). Valor fora da allowlist é 422 — filtro descartado em silêncio mente para quem consulta.
Saídas
200 com {Points:[...]}.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
from
query
não
ISO-8601 (`YYYY-MM-DD` ou RFC 3339). Default: 30 dias antes de `to`.
to
query
não
ISO-8601. Default: agora. **Fim EXCLUSIVO**; data pura cobre o dia inteiro.
channel
query
não
interval
query
não
`week` e `month` são aceitos aqui e recusados pelo upstream (422).
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/api/reports/timeseries' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/reports/top-templates
Ranking de templates por volume, com taxas.
limit aceito de 1 a 100 nesta borda; o teto REAL aplicado pelo repositório do asender_messages é 50.
Onde é usada
dashboard de relatórios (tabela lateral).
Entradas
from, to, limit (1..100).
Saídas
200 com {Templates:[...]}.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
from
query
não
ISO-8601 (`YYYY-MM-DD` ou RFC 3339). Default: 30 dias antes de `to`.
to
query
não
ISO-8601. Default: agora. **Fim EXCLUSIVO**; data pura cobre o dia inteiro.
limit
query
não
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/api/reports/top-templates' \
-H 'Authorization: Bearer SEU_TOKEN'
bff-tenants
POST/api/invitations/accept
Aceita um convite e vincula o usuário à conta.
Onde é usada
tela de convite.
Efeitos
uma escrita no core. # Esta rota NÃO passa por contaAutorizada E não pode: quem aceita ainda NÃO pertence à conta — exigir pertinência aqui tornaria o convite impossível de aceitar. Quem autoriza é o TOKEN, que o core valida (existente, não expirado, não usado), e o usuário vem da sessão.
API pública do cliente. Fluxo do dado: API key → Principal.TenantID → GET /v1/messages?channel=email.
Entradas
status, q, cursor, limit (allowlist). channel do cliente é IGNORADO: nesta rota o canal é do contrato, não do parâmetro.
Saídas
200 com {Messages, NextCursor, HasMore}; 422 em query malformada.
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/api/tenants' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/tenants
Cria um tenant RAIZ pertencente ao usuário logado.
Allowlist tipada de dois campos.parent_id, monthly_quota, status, plan, owner_user_id e id são IGNORADOS e registrados — em nível ERROR, como tentativa de escalonamento de privilégio. O owner_user_id vem sempre do principal verificado.
Ignorar (em vez de 422) preserva chamadores antigos que mandam owner_user_id; ignorar aqui não é engolir: fica no log com o usuário que enviou.
Para pendurar um tenant sob outro use POST /api/tenants/{id}/children (autoriza o pai) ou PATCH /api/tenants/{id}/parent (autoriza os dois lados). É o único caminho autorizado para mexer na árvore.
Onde é usada
fluxo de cadastro (AuthHandler.Register faz o equivalente internamente) e recuperação manual quando o provisionamento automático falhou.
Entradas
{Name, Slug} — allowlist explícita, ver tenantCreateInput.
Saídas
201 com {Tenant:{Id,...}} em PascalCase; 400 JSON inválido OU campo fora da allowlist; 422 nome vazio; o status 4xx do core preservado; 502 quando o core está fora.
Efeitos
escreve no asender-core. O owner_user_id vem SEMPRE do principal verificado, nunca do corpo.
Respostas
código
significado
201
Tenant criado. **O objeto vem NO TOPO, sem a chave `Tenant`** — o
`asender-core` responde o tenant sem chave de recurso nesta rota e o
BFF só pascaliza o que recebeu. É inconsistente com
`POST /api/tenants/{id}/children` e com `POST /api/auth/register`,
que devolvem `{"Tenant":{...}}`. Documentado como está no ar.
400
`400 ValidationError` — JSON inválido, campo desconhecido ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
413
`413 PayloadTooLarge` — corpo acima de 2 MiB.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Cria uma sub-conta sob um tenant administrado pelo usuário.
Autoriza {id} por vínculo direto ou herdado (ser membro de um ancestral manda na subárvore). MonthlyQuota ausente/nulo = herda do ancestral mais próximo com valor.
Onde é usada
tela de hierarquia de tenants.
Entradas
{Name, Slug, MonthlyQuota} — MonthlyQuota nulo/ausente significa herdar do ancestral mais próximo com valor.
Saídas
201 com {Tenant}; 404 se {id} não é acessível; 422 quando o core recusa (slug duplicado, ciclo detectado pelo trigger de closure).
Efeitos
escreve no asender-core.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
201
Sub-conta criada.
400
`400 ValidationError` — JSON inválido, campo desconhecido ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
409
`409 Conflict` — email já registrado, slug em uso.
413
`413 PayloadTooLarge` — corpo acima de 2 MiB.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
uma leitura no core. # Esta rota NÃO exige plataforma Ela exige PERTINÊNCIA, como qualquer leitura de conta. Uma trilha que só a plataforma consegue ler serve para a plataforma se defender, não para o cliente se proteger (ADR-0017).
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
OK
401
credencial ausente ou inválida
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/impersonations' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/api/tenants/{id}/invitations
Convites pendentes da conta.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
OK
401
credencial ausente ou inválida
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/id_AQUI/invitations' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/api/tenants/{id}/invitations
Convida alguém para a conta.
Onde é usada
tela de equipe.
Efeitos
uma escrita no core (e, quando houver envio, um e-mail).
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Autoriza os DOIS lados: {id} e o ParentId de destino. Autorizar só a origem deixaria pendurar um tenant na árvore de outro cliente, que passaria a vê-lo em rollup de relatório.
Corpo com EXATAMENTE um campo ParentId. null promove a raiz; campo ausente é 422 (um {} acidental não pode promover uma sub-conta). Ciclo → 422 (o trigger de closure do core levanta check_violation), nunca 500.
Onde é usada
tela de hierarquia de tenants. Segurança: autoriza os DOIS lados. Autorizar só {id} deixaria um usuário pendurar o tenant dele sob a árvore de outro cliente, o que é escalada de escopo — o novo pai passaria a "ver" a subárvore em rollup.
Saídas
200 com {Tenant}; 404 se qualquer um dos lados não é acessível; 422 quando o movimento cria ciclo (o trigger do core rejeita com check_violation).
Efeitos
reescreve a closure de tenants no core.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
Movido; a closure foi reescrita.
400
`400 ValidationError` — JSON inválido, campo desconhecido ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
Corpo com EXATAMENTE um campo MonthlyQuota. null volta a herdar do ancestral mais próximo. Campo ausente é 422 — um nome digitado errado não pode limpar a quota em silêncio.
Onde é usada
tela de hierarquia de tenants.
Entradas
{MonthlyQuota} — número define o teto; null volta a herdar do ancestral. A chave é OBRIGATÓRIA: aceitar corpo sem ela faria um nome de campo digitado errado limpar a quota em silêncio.
Saídas
200 com {Tenant}; 404 se {id} não é acessível; 422 em valor inválido.
Efeitos
escreve no asender-core.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id do tenant (`acc_<hex>`). O BIGSERIAL interno nunca aparece na fronteira.
Respostas
código
significado
200
Quota gravada; `EffectiveQuota` já recalculada.
400
`400 ValidationError` — JSON inválido, campo desconhecido ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
dashboard (settings/tenants) e backoffice. Fluxo do dado: sessão → tenant resolvido → asender-core GET /v1/tenants/{id}/tree → PascalCase.
Entradas
depth — inteiro positivo; valor não numérico é 422, não "sem limite" silencioso.
Saídas
200 com {TenantId, Tree:[...]}; 404 se o tenant pedido não é acessível.
Parâmetros
nome
em
obrigatório
descrição
X-Asender-Tenant
header
não
Tenant que o dashboard está olhando. **Nunca é usado como veio:** só
vale depois de provada a pertinência do usuário (vínculo direto ou a um
ancestral). Fora do escopo → **404**, não 403. Ausente → o menor id
entre os tenants do usuário, em ordem lexicográfica (escolha estável, e
a razão de o login novo cair numa conta vazia).
depth
query
não
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 NoTenant` — sessão válida de um usuário sem NENHUM vínculo de tenant.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/api/tenants/tree' \
-H 'Authorization: Bearer SEU_TOKEN'
meta
GET/
Identidade do serviço.
Respostas
código
significado
200
OK
Exemplo
curl -X GET 'https://api.asender.net/' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/healthz
Liveness. Não toca dependência.
Respostas
código
significado
200
Processo vivo.
Exemplo
curl -X GET 'https://api.asender.net/healthz' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/metrics
Métricas Prometheus.
Respostas
código
significado
200
Texto no formato de exposição do Prometheus.
Exemplo
curl -X GET 'https://api.asender.net/metrics' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/readyz
Readiness.
Reporta o estado do NATS em checks.nats mas responde 200 mesmo degradado (decisão explícita: o orquestrador não deve matar o processo por NATS momentaneamente fora). Redis não é dependência dura — o limiter cai para no-op.
Respostas
código
significado
200
Pronto (possivelmente degradado).
Exemplo
curl -X GET 'https://api.asender.net/readyz' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/version
Versão e commit do binário em execução.
Onde é usada
rota pública, usada por ops e por quem investiga incidente. Pública porque a primeira pergunta de todo incidente é "que versão está no ar?", e ela tem de ser respondível sem credencial — quem investiga muitas vezes ainda não tem uma. O que sai é só isso: nunca configuração, nunca endereço interno, que é topologia e pertence ao console de plataforma.
Respostas
código
significado
200
OK
Exemplo
curl -X GET 'https://api.asender.net/version' \
-H 'Authorization: Bearer SEU_TOKEN'
public-account
GET/v1/account
Conta da API key usada, mais o contexto da própria chave.
Respostas
código
significado
200
OK
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/v1/account' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/account/usage
Contadores de consumo do tenant no período corrente.
Onde é usada
superfície pública /v1, consumida por integrações de cliente. Fluxo do dado: API key verificada → Principal.TenantID → asender-core GET /v1/tenants/{id}/usage (snake_case) → PascalMap → resposta pública.
Saídas
200 com {Usage:{Period,EmailsSent,SmsSent,PushSent,ApiCalls}}; 404 se o tenant da API key não existe mais no core; 502 se o core está fora. DEFEITO QUE ISTO TAMBÉM FECHA (item 7 do briefing Y2, metade da borda): o core respondia 404 quando não havia linha em core.usage_counters — e o seed não cria nenhuma —, e este handler traduzia QUALQUER erro do upstream em 502. Ou seja: a rota respondia "serviço indisponível" para todo tenant que ainda não enviou nada, mandando o alerta para o time errado. O 404 do core agora só significa "tenant inexistente" (a correção principal está em asender-core/internal/service/usage.Get) e é traduzido como 404, não 502. DEFEITO QUE ISTO FECHA (T3 §3.6): o payload do core saía CRU sob Usage ({"Usage":{"sent":42}}), fora do contrato PascalCase da API pública (API_DESIGN.md §51). O cliente que segue o contrato lê Usage.Sent e recebe undefined — mesma classe de divergência de fronteira do GET /api/tenants.
Respostas
código
significado
200
Consumo do período. Tenant sem nenhum envio devolve os contadores em
zero, com `UpdatedAt: null` — ausência de linha é consumo zero, não
ausência de recurso.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
Core fora **ou** tenant sem contador de uso (ver `x-asender-divergence`).
Exemplo
curl -X GET 'https://api.asender.net/v1/account/usage' \
-H 'Authorization: Bearer SEU_TOKEN'
public-email
GET/v1/emails
Histórico de emails do tenant da API key.
channel é FIXO em email nesta rota — mandar ?channel=sms é 422 (parâmetro fora da allowlist), não filtro silencioso.
Onde é usada
API pública do cliente. Fluxo do dado: API key → Principal.TenantID → GET /v1/messages?channel=email.
Entradas
status, q, cursor, limit (allowlist). channel do cliente é IGNORADO: nesta rota o canal é do contrato, não do parâmetro.
Saídas
200 com {Messages, NextCursor, HasMore}; 422 em query malformada.
Parâmetros
nome
em
obrigatório
descrição
status
query
não
q
query
não
Busca. `%` e `_` são escapados no repositório.
cursor
query
não
limit
query
não
Respostas
código
significado
200
Página de mensagens.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Exemplo
curl -X GET 'https://api.asender.net/v1/emails' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/emails/{id}
Um email com corpo, metadata e timeline de eventos.
Mensagem de outro tenant → 404 (não confirma existência). Mensagem deste tenant em outro canal também → 404: nesta rota só email existe.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id da mensagem (`msg_<hex>`).
Respostas
código
significado
200
Detalhe.
400
`400 ValidationError` — JSON inválido, campo desconhecido ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
404
`404 NotFound`. **Cobre dois casos de propósito:** o recurso não existe,
ou existe e não é seu. Diferenciar seria oráculo de enumeração.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
Exemplo
curl -X GET 'https://api.asender.net/v1/emails/id_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/emails/batch
Até 500 emails independentes numa chamada.
Resultado parcial é legítimo e explícito. Falha de um item não derruba os outros: cada entrada de Results traz MessageId ou Error, sempre com o Index de origem.
Onde é usada
API pública do cliente. Fluxo do dado: igual ao Send, uma chamada ao asender_messages por item.
Saídas
202 com {Results, Count}; cada item traz MessageId ou Error.
Efeitos
escrita no serviço de mensageria por item aceito.
Respostas
código
significado
202
Lote processado (com ou sem itens rejeitados).
400
`400 ValidationError` — JSON inválido, campo desconhecido ou tipo errado.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
Enfileira um email (payload compatível com SES SendEmail).
Persiste no asender_messages (mensagem + outbox no MESMO commit) e o publisher de outbox entrega ao NATS. Não publica direto no broker — seria dual-write.
Cc e Bcc entram como destinatários independentes: o serviço materializa uma mensagem por destinatário, que é o comportamento correto de cópia oculta.
Onde é usada
API pública do cliente (autenticada por API key). Fluxo do dado: API key → APIKeyAuth → Principal.TenantID → POST /v1/messages (header X-Asender-Tenant) → messages + outbox no mesmo commit → NATS → worker.
Saídas
202 com {MessageId, MessageIds, Status, ReusedIdempotency}; 400 em validação local; 422 quando o asender_messages recusa; 502 se ele está fora.
Efeitos
escrita no serviço de mensageria. Idempotente por IdempotencyKey.
Parâmetros
nome
em
obrigatório
descrição
Idempotency-Key
header
não
**DIVERGÊNCIA CONHECIDA (RELATORIO-FINAL §3 item 10).** Nesta superfície
pública o header é ACEITO E IGNORADO — o handler só lê o campo
`IdempotencyKey` do CORPO. Replay com o mesmo header e corpo sem o campo
**duplica o envio**.
O `asender_messages` implementa idempotência de verdade e dá precedência
ao header sobre o campo do corpo; o buraco está apenas em não propagar o
header daqui para lá. Enquanto não for corrigido, use o campo
`IdempotencyKey` no corpo.
Respostas
código
significado
202
Aceito e persistido; a entrega é assíncrona.
400
`400 ValidationError` com `Error.Details` mapeando campo → motivo.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Contraparte de leitura do registro: quem envia token precisa poder conferir o que está registrado. O tenant vem da API key, NUNCA da query — aceitar tenant_id do cliente aqui seria vazamento cross-tenant.
Onde é usada
dashboard, tela /send/push. Fluxo do dado: messages.push_devices → asender_messages → aqui → seletor.
Entradas
cursor e limit, na mesma allowlist dos outros List.
Saídas
200 com {Devices, NextCursor, HasMore} (PascalCase, §51). Antes desta rota o caminho respondia 404 e a tela não tinha como listar: dava para disparar digitando o token à mão, mas não escolher um device existente.
Parâmetros
nome
em
obrigatório
descrição
cursor
query
não
limit
query
não
Respostas
código
significado
200
Página de devices, em PascalCase.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
422
Exemplo
curl -X GET 'https://api.asender.net/v1/push/devices' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/push/devices
Registra (upsert por token) um device de push.
Idempotente pelo par (tenant, token). Responde 200 sempre, mesmo quando o asender_messages respondeu 201 no primeiro registro.
Onde é usada
SDK do cliente, na primeira abertura do app / renovação do token. Fluxo do dado: API key → Principal.TenantID → POST /v1/push/devices no asender_messages → messages.push_devices.
Saídas
200 com {Device} (upsert: o mesmo token duas vezes não cria dois registros); 400 em validação local; 422 quando o upstream recusa a plataforma.
Efeitos
escrita no serviço de mensageria.
Respostas
código
significado
200
Device registrado ou reativado.
400
`400 ValidationError` com `Error.Details` mapeando campo → motivo.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
Envio só por tópico entra como destinatário sintético topic:<nome> — o asender_messages exige ao menos um destinatário e não modela tópico.
Onde é usada
API pública do cliente (autenticada por API key). Fluxo do dado: API key → APIKeyAuth → Principal.TenantID → POST /v1/messages (header X-Asender-Tenant) → messages + outbox no mesmo commit → NATS → worker.
Saídas
202 com {MessageId, MessageIds, Status, ReusedIdempotency}; 400 em validação local; 422 quando o asender_messages recusa; 502 se ele está fora.
Efeitos
escrita no serviço de mensageria. Idempotente por IdempotencyKey.
Parâmetros
nome
em
obrigatório
descrição
Idempotency-Key
header
não
**DIVERGÊNCIA CONHECIDA (RELATORIO-FINAL §3 item 10).** Nesta superfície
pública o header é ACEITO E IGNORADO — o handler só lê o campo
`IdempotencyKey` do CORPO. Replay com o mesmo header e corpo sem o campo
**duplica o envio**.
O `asender_messages` implementa idempotência de verdade e dá precedência
ao header sobre o campo do corpo; o buraco está apenas em não propagar o
header daqui para lá. Enquanto não for corrigido, use o campo
`IdempotencyKey` no corpo.
Respostas
código
significado
202
Aceito e persistido; a entrega é assíncrona.
400
`400 ValidationError` com `Error.Details` mapeando campo → motivo.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
API pública do cliente (autenticada por API key). Fluxo do dado: API key → APIKeyAuth → Principal.TenantID → POST /v1/messages (header X-Asender-Tenant) → messages + outbox no mesmo commit → NATS → worker.
Saídas
202 com {MessageId, MessageIds, Status, ReusedIdempotency}; 400 em validação local; 422 quando o asender_messages recusa; 502 se ele está fora.
Efeitos
escrita no serviço de mensageria. Idempotente por IdempotencyKey.
Parâmetros
nome
em
obrigatório
descrição
Idempotency-Key
header
não
**DIVERGÊNCIA CONHECIDA (RELATORIO-FINAL §3 item 10).** Nesta superfície
pública o header é ACEITO E IGNORADO — o handler só lê o campo
`IdempotencyKey` do CORPO. Replay com o mesmo header e corpo sem o campo
**duplica o envio**.
O `asender_messages` implementa idempotência de verdade e dá precedência
ao header sobre o campo do corpo; o buraco está apenas em não propagar o
header daqui para lá. Enquanto não for corrigido, use o campo
`IdempotencyKey` no corpo.
Respostas
código
significado
202
Aceito e persistido; a entrega é assíncrona.
400
`400 ValidationError` com `Error.Details` mapeando campo → motivo.
401
`401 AuthorizationError` — credencial ausente, malformada, inválida ou
expirada. **Também** quando o upstream recusa o SERVICE_SECRET (erro de
configuração, logado em ERROR).
403
`403 TenantSuspended` — a API key é válida mas o tenant não está `active`. Fail closed: status vazio também barra.
422
`422 ValidationError` — semântica inválida: parâmetro de query fora da
allowlist (nomeando o parâmetro), `from > to`, `depth` negativa, corpo
de PATCH sem o campo obrigatório, ou recusa do upstream repassada.
429
`429 Throttling` — rate limit por API key/usuário. Acompanha `Retry-After`.
502
`502 InternalError` — upstream inalcançável ou 5xx. O motivo real fica
no log com `RequestId`; o cliente recebe mensagem genérica.
Authorization Server. É daqui que sai o token que todas as outras APIs exigem.
https://auth.asender.net · 42 operações
.well-known
GET/.well-known/jwks.jsonsem schema na spec
JWKS serve /.well-known/jwks.json — a(s) chave(s) pública(s) RS256. Os clients
Onde é usada
verificação no client.
Exemplo
curl -X GET 'https://auth.asender.net/.well-known/jwks.json' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/.well-known/openid-configurationsem schema na spec
anuncia os endpoints e as capacidades (só `code`, só S256, só RS256 — o perfil OAuth2.1 da casa).
Onde é usada
os clients leem isto no bootstrap para se autoconfigurar. Cacheável (muda só em deploy).
Exemplo
curl -X GET 'https://auth.asender.net/.well-known/openid-configuration' \
-H 'Authorization: Bearer SEU_TOKEN'
auth
POST/v1/auth/login
Autentica e emite sessão.
Com 2FA ativo, a resposta traz two_factor_required:true e sem token; a sessão só é emitida por POST /v1/2fa/check.
User-Agent e IP do chamador entram no registro da sessão (o IP vem do RealIP do chi, portanto de X-Forwarded-For quando houver proxy).
Respostas
código
significado
200
Sessão emitida, ou 2FA pendente.
400
`400` — `InvalidRequest` (JSON inválido, **campo desconhecido**, corpo
vazio, corpo acima de 1 MiB), `InvalidInput` (validação de domínio) ou
`TwoFactorMissing` (2FA não configurado).
401
`401 InvalidCredentials` — credenciais inválidas.
500
`500 InternalError`.
**DIVERGÊNCIA:** o `Message` recebe `err.Error()` CRU, como no
`asender-core`. Pode vazar estrutura interna.
Sempre `ok:true`; `reset_token` presente quando um token foi gerado.
400
`400` — `InvalidRequest` (JSON inválido, **campo desconhecido**, corpo
vazio, corpo acima de 1 MiB), `InvalidInput` (validação de domínio) ou
`TwoFactorMissing` (2FA não configurado).
401
`401` — token de serviço ausente ou inválido. **Não ocorre com
`ENV=development`.**
500
`500 InternalError`.
**DIVERGÊNCIA:** o `Message` recebe `err.Error()` CRU, como no
`asender-core`. Pode vazar estrutura interna.
`400` — `InvalidRequest` (JSON inválido, **campo desconhecido**, corpo
vazio, corpo acima de 1 MiB), `InvalidInput` (validação de domínio) ou
`TwoFactorMissing` (2FA não configurado).
uma leitura. Não recebe usuário por parâmetro: a lista é sempre a do dono do token. Um ?user_id= seria a rota mais barata do sistema para descobrir de onde outra pessoa se conecta.
Respostas
código
significado
200
OK
401
credencial ausente ou inválida
Exemplo
curl -X GET 'https://auth.asender.net/v1/auth/sessions' \
-H 'Authorization: Bearer SEU_TOKEN'
DELETE/v1/auth/sessions/{id}
Revoga uma sessão específica.
Onde é usada
botão "remover" da tela de segurança.
Efeitos
a sessão para de autenticar imediatamente. 404 tanto para sessão inexistente quanto para sessão de OUTRA pessoa: a distinção transformaria a rota num oráculo de id de sessão alheia.
É o hop que o middleware SessionAuth do asender-api faz em toda requisição /api/* autenticada.
A resposta DEVE trazer user.id. Um 200 sem ele é violação de contrato: o cliente do asender-api recusa com ErrContractViolation e o middleware barra o principal anônimo (defesa em profundidade). Esta rota não devolve tenant — quem sabe de tenant é o asender-core.
Respostas
código
significado
200
Sessão válida.
401
`401` — `MissingToken` (sem `Authorization: Bearer`), `InvalidToken`
(assinatura/formato) ou `ExpiredToken` (expirada **ou revogada por
logout**).
Exemplo
curl -X GET 'https://auth.asender.net/v1/auth/validate' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/auth/verify
Confirma o e-mail a partir do token do link.
Onde é usada
tela /verify do front, com o token da URL.
Efeitos
duas escritas. Token inválido, expirado e já usado respondem IGUAL: distingui-los diria a quem tem um link velho se ele já foi usado por outra pessoa.
Authorize implementa GET /authorize (OAuth2.1 authorization code + PKCE).
Onde é usada
o browser é redirecionado para cá pelo client (app) que quer logar.
Exemplo
curl -X GET 'https://auth.asender.net/authorize' \
-H 'Authorization: Bearer SEU_TOKEN'
authz
POST/v1/authz/decidesem schema na spec
`{permitido, motivo, papel, nivel}`.
Onde é usada
POST /v1/authz/decide.
Efeitos
grava a trilha (dentro do PDP). Erro de banco vira 503, e não {permitido:false}: o PEP precisa distinguir "a política diz não" de "não consegui perguntar" — o primeiro ele mostra ao usuário, o segundo ele resolve com o cache que já tem (piso 10).
GET /v1/authz/eu?sub=&tenant= — alimenta o switcher e a home do hub.
Exemplo
curl -X GET 'https://auth.asender.net/v1/authz/eu' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/authz/produtossem schema na spec
Catalogo devolve as ferramentas que existem.
Onde é usada
GET /v1/authz/produtos.
Exemplo
curl -X GET 'https://auth.asender.net/v1/authz/produtos' \
-H 'Authorization: Bearer SEU_TOKEN'
logout
GET/logoutsem schema na spec
Expects Authorization: Bearer <token>.
Exemplo
curl -X GET 'https://auth.asender.net/logout' \
-H 'Authorization: Bearer SEU_TOKEN'
meta
GET/
Identidade do serviço.
Onde é usada
rota raiz.
Efeitos
escreve a resposta. Serve para saber QUAL binário está rodando quando o comportamento diverge do esperado — a primeira pergunta de todo diagnóstico de deploy.
Respostas
código
significado
200
OK
Exemplo
curl -X GET 'https://auth.asender.net/' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/healthz
Liveness.
Onde é usada
liveness probe.
Efeitos
escreve a resposta. Não consulta o banco de propósito: liveness que falha por causa do Postgres faz o orquestrador REINICIAR um serviço saudável durante uma instabilidade do banco — e reiniciar o serviço de login em massa transforma degradação em queda de autenticação para todo mundo.
Respostas
código
significado
200
OK
Exemplo
curl -X GET 'https://auth.asender.net/healthz' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/metrics
Métricas Prometheus.
Respostas
código
significado
200
Texto.
Exemplo
curl -X GET 'https://auth.asender.net/metrics' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/readyz
Readiness — ping no Postgres.
Onde é usada
readiness probe.
Efeitos
uma consulta trivial ao Postgres. É aqui que a dependência entra — o oposto do /healthz. O prazo curto é deliberado: uma sonda que espera indefinidamente nunca reporta "não pronto", e a instância continua recebendo login que vai falhar.
Respostas
código
significado
200
Banco respondeu.
503
`NotReady` — o erro do ping vai no `Message`.
Exemplo
curl -X GET 'https://auth.asender.net/readyz' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/version
Versão e commit do binário em execução.
Onde é usada
rota pública, usada por ops e por quem investiga incidente. Pública porque a primeira pergunta de todo incidente é "que versão está no ar?", e ela tem de ser respondível sem credencial — quem investiga muitas vezes ainda não tem uma. O que sai é só isso: nunca configuração, nunca endereço interno.
Respostas
código
significado
200
OK
Exemplo
curl -X GET 'https://auth.asender.net/version' \
-H 'Authorization: Bearer SEU_TOKEN'
tenants
GET/v1/tenants/{id}/acessossem schema na spec
ListarAcessos devolve quem acessa o quê na conta.
Onde é usada
GET /v1/tenants/{id}/acessos.
Exemplo
curl -X GET 'https://auth.asender.net/v1/tenants/id_AQUI/acessos' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/tenants/{id}/acessossem schema na spec
Conceder dá acesso de uma pessoa a uma ferramenta da conta.
Onde é usada
POST /v1/tenants/{id}/acessos {user_id, produto, papel}. Papel que o produto não declara é recusado pela FK — 422 com a mensagem, e não 500: "papel inventado" é erro de quem chamou, não do servidor.
DELETE/v1/tenants/{id}/acessos/{userId}/{produto}sem schema na spec
Revogar tira o acesso.
Onde é usada
DELETE /v1/tenants/{id}/acessos/{userId}/{produto}. Idempotente: revogar duas vezes responde ok nas duas. O efeito chega às ferramentas em até 60s (TTL do cache do PEP) — contrato escrito no ADR-0025.
GET/v1/tenants/{id}/authz/decisoessem schema na spec
Trilha devolve as decisões recentes da conta.
Onde é usada
GET /v1/tenants/{id}/authz/decisoes?negadas=1&limite=50.
Exemplo
curl -X GET 'https://auth.asender.net/v1/tenants/id_AQUI/authz/decisoes' \
-H 'Authorization: Bearer SEU_TOKEN'
GET/v1/tenants/{id}/produtossem schema na spec
ListarAssinatura devolve o que a conta assina.
Onde é usada
GET /v1/tenants/{id}/produtos.
Exemplo
curl -X GET 'https://auth.asender.net/v1/tenants/id_AQUI/produtos' \
-H 'Authorization: Bearer SEU_TOKEN'
POST/v1/tenants/{id}/produtossem schema na spec
Assinar liga ou suspende um produto na conta.
Onde é usada
POST /v1/tenants/{id}/produtos {produto, status}. status é validado aqui E no CHECK da tabela: a borda dá a mensagem, o banco dá a garantia — a borda pode ser contornada por outro caminho de escrita.
Segunda etapa do login — valida o código e EMITE a sessão.
Sucesso devolve o mesmo login de POST /v1/auth/login, agora com token.
Respostas
código
significado
200
Código válido; sessão emitida.
400
`400` — `InvalidRequest` (JSON inválido, **campo desconhecido**, corpo
vazio, corpo acima de 1 MiB), `InvalidInput` (validação de domínio) ou
`TwoFactorMissing` (2FA não configurado).
`400` — `InvalidRequest` (JSON inválido, **campo desconhecido**, corpo
vazio, corpo acima de 1 MiB), `InvalidInput` (validação de domínio) ou
`TwoFactorMissing` (2FA não configurado).
401
`401` — token de serviço ausente ou inválido. **Não ocorre com
`ENV=development`.**
Ainda não confirmado: o 2FA só passa a valer depois de POST /v1/2fa/verify.
Respostas
código
significado
200
Segredo gerado.
400
`400` — `InvalidRequest` (JSON inválido, **campo desconhecido**, corpo
vazio, corpo acima de 1 MiB), `InvalidInput` (validação de domínio) ou
`TwoFactorMissing` (2FA não configurado).
401
`401` — token de serviço ausente ou inválido. **Não ocorre com
`ENV=development`.**
`400` — `InvalidRequest` (JSON inválido, **campo desconhecido**, corpo
vazio, corpo acima de 1 MiB), `InvalidInput` (validação de domínio) ou
`TwoFactorMissing` (2FA não configurado).
valida o Bearer (RS256, iss, exp) e devolve sub/email/name.
Onde é usada
o client chama para hidratar o perfil. Sem token válido -> 401 invalid_token.
Exemplo
curl -X GET 'https://auth.asender.net/userinfo' \
-H 'Authorization: Bearer SEU_TOKEN'
users
POST/v1/users
Cria um usuário.
A rota é declarada como r.Post("/") dentro de r.Route("/users"), ou seja o padrão registrado é /v1/users/. Ambas as formas respondem 201 (verificado na stack): POST /v1/users e POST /v1/users/.
Senha é hasheada com bcrypt no custo configurado (BCRYPT_COST, ≥ 12). O hash NUNCA aparece em resposta alguma.
Respostas
código
significado
201
Criado.
400
`400` — `InvalidRequest` (JSON inválido, **campo desconhecido**, corpo
vazio, corpo acima de 1 MiB), `InvalidInput` (validação de domínio) ou
`TwoFactorMissing` (2FA não configurado).
401
`401` — token de serviço ausente ou inválido. **Não ocorre com
`ENV=development`.**
409
`409 Conflict` — email já cadastrado.
500
`500 InternalError`.
**DIVERGÊNCIA:** o `Message` recebe `err.Error()` CRU, como no
`asender-core`. Pode vazar estrutura interna.
Public id (`usr_<hex>`). O BIGSERIAL interno nunca aparece.
Respostas
código
significado
200
OK
401
`401` — token de serviço ausente ou inválido. **Não ocorre com
`ENV=development`.**
404
`404 NotFound`.
Exemplo
curl -X GET 'https://auth.asender.net/v1/users/id_AQUI' \
-H 'Authorization: Bearer SEU_TOKEN'
PATCH/v1/users/{id}
Altera nome e/ou email.
Senha NÃO é alterável por aqui — o caminho é POST /v1/auth/password-reset/confirm.
Parâmetros
nome
em
obrigatório
descrição
id
path
sim
Public id (`usr_<hex>`). O BIGSERIAL interno nunca aparece.
Respostas
código
significado
200
OK
400
`400` — `InvalidRequest` (JSON inválido, **campo desconhecido**, corpo
vazio, corpo acima de 1 MiB), `InvalidInput` (validação de domínio) ou
`TwoFactorMissing` (2FA não configurado).
401
`401` — token de serviço ausente ou inválido. **Não ocorre com
`ENV=development`.**
uma escrita. Mudar a configuração ACALMA o alerta: ele passa a observar outra coisa, e manter o estado faria a próxima avaliação comparar maçã com laranja — ele não avisaria pela condição NOVA, porque já estaria "disparado" pela antiga.
uma leitura. Da CONTA, e não de um alerta: quem investiga "o que aconteceu ontem" quer a linha do tempo inteira, e paginar por alerta a obrigaria a abrir um por um.
Exemplo
curl -X GET 'https://api.crm.asender.net/v1/alertas/eventos'
configuracoes
GET/v1/configuracoes/retencaosem schema na spec
GET /v1/configuracoes/retencao.
Onde é usada
tela de configurações da conta.
Efeitos
uma leitura. Conta sem política configurada responde 200 com dias: null — e não 404: "não configurei retenção" é o estado NORMAL, e a tela precisa dele para desenhar o formulário vazio.
Exemplo
curl -X GET 'https://api.crm.asender.net/v1/configuracoes/retencao'
PUT/v1/configuracoes/retencaosem schema na spec
PUT /v1/configuracoes/retencao com `{"dias": 90}` ou `{"dias": null}`.
Onde é usada
tela de configurações da conta.
Efeitos
uma escrita; passa a anonimizar contato antigo em até uma hora. # null desliga, ausente é erro Os dois cairiam no mesmo ponteiro nil se o corpo fosse decodificado direto — e um cliente que esquecesse o campo desligaria a retenção sem querer. null é uma ORDEM ("guarde para sempre"); campo ausente é um pedido malformado.
uma leitura. Responde TODOS os canais conhecidos, inclusive os sem opt-out. Devolver só os que saíram faria a tela ter de saber a lista completa para desenhar as caixas — e a primeira que ficasse desatualizada esconderia um canal do operador.
Exemplo
curl -X GET 'https://api.crm.asender.net/v1/contatos/id_AQUI'
PATCH/v1/contatos/{id}sem schema na spec
PUT /v1/alertas/{id}.
Onde é usada
tela de alertas.
Efeitos
uma escrita. Mudar a configuração ACALMA o alerta: ele passa a observar outra coisa, e manter o estado faria a próxima avaliação comparar maçã com laranja — ele não avisaria pela condição NOVA, porque já estaria "disparado" pela antiga.
GET/v1/contatos/{id}/consentimentosem schema na spec
GET /v1/contatos/{id}/consentimento.
Onde é usada
tela de contato, seção de preferências.
Efeitos
uma leitura. Responde TODOS os canais conhecidos, inclusive os sem opt-out. Devolver só os que saíram faria a tela ter de saber a lista completa para desenhar as caixas — e a primeira que ficasse desatualizada esconderia um canal do operador.
Exemplo
curl -X GET 'https://api.crm.asender.net/v1/contatos/id_AQUI/consentimento'
PUT/v1/contatos/{id}/consentimentosem schema na spec
PUT /v1/contatos/{id}/consentimento.
Onde é usada
tela de contato, e o link de descadastro via BFF.
Efeitos
cria ou remove a linha de opt-out. # recebe é ponteiro, e a ausência é ERRO Com um bool comum, um corpo sem o campo viria como false — e "esqueci de mandar o campo" seria indistinguível de "descadastra esta pessoa". O ponteiro transforma a omissão em 422 explícito.
uma leitura; decifra a PII; registra a exportação na timeline. # A PII é decifrada AQUI, e só para quem é dono O tenant vem da sessão verificada e a RLS já filtra a consulta. Este handler nunca vê contato de outra conta, e a decifragem acontece com a chave do processo — não há caminho em que o CSV carregue dado alheio. # Cada exportação vira uma ATIVIDADE, e isso não é opcional A LGPD (art. 37) exige registro das operações de tratamento, e exportar é tratamento: a partir dali o dado existe num arquivo que ninguém controla. Sem esse registro, "quem baixou a base e quando" não tem resposta — e é a primeira pergunta de todo incidente de vazamento. O registro é gravado ANTES do arquivo sair. Se a gravação falhar, a exportação não acontece: um download sem trilha é exatamente o que a trilha existe para impedir, e "o arquivo já foi" não se desfaz.
Exemplo
curl -X GET 'https://api.crm.asender.net/v1/contatos/exportar.csv'
destinos
GET/v1/destinossem schema na spec
GET /v1/alertas.
Onde é usada
tela de alertas.
Efeitos
uma leitura.
Exemplo
curl -X GET 'https://api.crm.asender.net/v1/destinos'
uma escrita. Mudar a configuração ACALMA o alerta: ele passa a observar outra coisa, e manter o estado faria a próxima avaliação comparar maçã com laranja — ele não avisaria pela condição NOVA, porque já estaria "disparado" pela antiga.
uma leitura. O PAYLOAD não volta: ele carrega a PII do contato, e quem pergunta "por que não chegou" precisa do estado e do código — não do dado da pessoa repetido numa segunda tela.
Exemplo
curl -X GET 'https://api.crm.asender.net/v1/destinos/id_AQUI/entregas'
POST/v1/destinos/{id}/testarsem schema na spec
POST /v1/destinos/{id}/testar.
Onde é usada
botão "testar" da tela de integrações.
Efeitos
cria uma entrega pendente. # ENFILEIRA, e não entrega na hora A entrega passa pelo MESMO dreno das entregas de verdade — e portanto pelo mesmo guard do piso 18, pela mesma guarda de rede e pela mesma assinatura. Um caminho de teste que chamasse o webhook direto seria um segundo caminho de saída, e o segundo caminho é sempre o que esquece uma guarda. O operador vê o resultado em GET /entregas, que é a mesma tela onde ele vê as entregas reais — então "o teste funcionou" e "a integração funciona" são a mesma leitura, e não duas afirmações que podem divergir.
PUT/v1/integracoes-de-conversao/{id}sem schema na spec
PUT /v1/alertas/{id}.
Onde é usada
tela de alertas.
Efeitos
uma escrita. Mudar a configuração ACALMA o alerta: ele passa a observar outra coisa, e manter o estado faria a próxima avaliação comparar maçã com laranja — ele não avisaria pela condição NOVA, porque já estaria "disparado" pela antiga.
GET/v1/integracoes-de-conversao/{id}/entregassem schema na spec
GET /v1/destinos/{id}/entregas?limite=N.
Onde é usada
diagnóstico da tela de integrações.
Efeitos
uma leitura. O PAYLOAD não volta: ele carrega a PII do contato, e quem pergunta "por que não chegou" precisa do estado e do código — não do dado da pessoa repetido numa segunda tela.
Exemplo
curl -X GET 'https://api.crm.asender.net/v1/integracoes-de-conversao/id_AQUI/entregas'
POST/v1/integracoes-de-conversao/{id}/testarsem schema na spec
POST /v1/destinos/{id}/testar.
Onde é usada
botão "testar" da tela de integrações.
Efeitos
cria uma entrega pendente. # ENFILEIRA, e não entrega na hora A entrega passa pelo MESMO dreno das entregas de verdade — e portanto pelo mesmo guard do piso 18, pela mesma guarda de rede e pela mesma assinatura. Um caminho de teste que chamasse o webhook direto seria um segundo caminho de saída, e o segundo caminho é sempre o que esquece uma guarda. O operador vê o resultado em GET /entregas, que é a mesma tela onde ele vê as entregas reais — então "o teste funcionou" e "a integração funciona" são a mesma leitura, e não duas afirmações que podem divergir.
uma leitura. Responde TODOS os canais conhecidos, inclusive os sem opt-out. Devolver só os que saíram faria a tela ter de saber a lista completa para desenhar as caixas — e a primeira que ficasse desatualizada esconderia um canal do operador.
Exemplo
curl -X GET 'https://api.crm.asender.net/v1/jornadas/id_AQUI'
PUT/v1/jornadas/{id}/ativasem schema na spec
PUT /v1/jornadas/{id}/ativa.
Onde é usada
painel, interruptor da jornada.
Efeitos
muda o estado; ligada, a jornada passa a inscrever contatos. # Revalida ANTES de ligar A jornada foi validada na criação, mas pode ter sido editada depois — e uma jornada com buraco na numeração deixaria o contato preso no meio do caminho, sem erro em lugar nenhum. Validar de novo aqui custa nada e é o último ponto antes de a coisa começar a mandar mensagem. Desligar NÃO revalida: uma jornada quebrada tem de poder ser desligada, e exigir que ela esteja válida para parar seria prender o operador justamente no caso em que ele mais precisa do botão.
503 `starting` antes do fim do boot; depois disso, CONSULTA as sondas e responde 200 `ready` ou 503 `degraded` com o nome de cada dependência.
Onde é usada
rota pública /readyz.
Efeitos
escreve JSON na resposta; pode tocar a dependência (com cache de 1s). # Por que ele não é mais um trinco de boot A readiness era MarkReady() uma vez e pronto — memória do boot, não estado atual. Num incidente real de 2026-08-27 o serviço voltou com a credencial errada do Postgres, falhou ~30 vezes por minuto ao drenar o outbox, e /healthz e /readyz responderam 200 o tempo todo: o gate de saúde do deploy passou e quem descobriu foi o pentest, depois. Readiness que só lembra do boot não enxerga a dependência que caiu DEPOIS dele. /healthz continua sem tocar em nada, e isso é de propósito: liveness que consulta o banco derruba o processo quando o banco oscila. Quem tem de dizer "não me mande tráfego" é o readiness. O que sai na resposta é o NOME da dependência e "indisponível" — nunca a mensagem do driver, que carrega usuário, host e base. Rota pública não conta topologia; o erro inteiro vai para o log.
Exemplo
curl -X GET 'https://api.crm.asender.net/readyz'
segmentos
GET/v1/segmentossem schema na spec
GET /v1/alertas.
Onde é usada
tela de alertas.
Efeitos
uma leitura.
Exemplo
curl -X GET 'https://api.crm.asender.net/v1/segmentos'
uma leitura. Responde TODOS os canais conhecidos, inclusive os sem opt-out. Devolver só os que saíram faria a tela ter de saber a lista completa para desenhar as caixas — e a primeira que ficasse desatualizada esconderia um canal do operador.
Exemplo
curl -X GET 'https://api.crm.asender.net/v1/segmentos/id_AQUI'
PUT/v1/segmentos/{id}sem schema na spec
PUT /v1/alertas/{id}.
Onde é usada
tela de alertas.
Efeitos
uma escrita. Mudar a configuração ACALMA o alerta: ele passa a observar outra coisa, e manter o estado faria a próxima avaliação comparar maçã com laranja — ele não avisaria pela condição NOVA, porque já estaria "disparado" pela antiga.
uma consulta; NÃO grava nada. Existe para que a pergunta "isto alcança quem?" seja respondida ANTES de salvar. Sem ela, o caminho para descobrir o alcance é criar o segmento — e a tela enche de segmentos descartados chamados "teste 3".
GET /v1/uso?periodo=YYYY-MM — números locais do CRM mais a quota e os contadores do core.
Onde é usada
tela de conta/plano.
Efeitos
uma leitura local e até duas chamadas ao core. # O core fora do ar NÃO derruba esta rota Os números locais (quantos contatos, quantos entraram no mês) são a metade que o CRM sabe sozinho, e são justamente a metade que a tela usa todo dia. Com o core fora, a resposta sai com core_disponivel: false — a tela mostra o que tem e diz o que não conseguiu saber (piso 10).
Exemplo
curl -X GET 'https://api.crm.asender.net/v1/uso'
Páginas
Páginas, formulários, redirects e mídia. Sistema próprio, endereço próprio.
https://api.pages.asender.net · 49 operações
configuracoes
GET/v1/configuracoes/marcasem schema na spec
GET /v1/paginas/{id}/experimento.
Onde é usada
tela de A/B do painel.
Efeitos
uma leitura. Página SEM experimento responde 200 com o corpo vazio de variantes, e não 404: "esta página não tem teste" é uma resposta sobre o mundo, e a tela precisa dela para mostrar o botão de criar. 404 mandaria a tela tratar o caso normal como erro.
Exemplo
curl -X GET 'https://api.pages.asender.net/v1/configuracoes/marca'
PUT/v1/configuracoes/marcasem schema na spec
PUT /v1/configuracoes/marca — os quatro campos de uma vez.
Onde é usada
tela de marca do painel.
Efeitos
uma escrita; muda o que o visitante vê na próxima visita. PUT e não PATCH: são quatro campos que a tela mostra e salva juntos, e campo vazio APAGA aquele elemento. Um PATCH criaria a pergunta "como apago o logo?", cuja resposta seria um campo especial que ninguém acerta na primeira leitura.
curl -X GET 'https://api.pages.asender.net/v1/dominios'
POST/v1/dominiossem schema na spec
POST /v1/dominios.
Onde é usada
tela de domínios.
Efeitos
uma escrita. # O domínio nasce NÃO verificado E é isso que impede o pior desfecho do produto: sem a prova de DNS, qualquer conta declararia banco.com.br e passaria a receber certificado TLS para esse nome — servindo uma página de captura no domínio de outra pessoa, com cadeado verde.
uma escrita. Apagar TIRA a autorização de TLS, e é o único caminho para isso — de propósito: um domínio verificado que pudesse ser "desverificado" por outro caminho sairia do ar sem ninguém entender por quê.
uma consulta DNS e uma escrita. # As duas falhas têm respostas DIFERENTES "Não consegui perguntar ao DNS" é 503 com "tente de novo"; "perguntei e o registro não está lá" é 422 com "publique o registro". Colapsar as duas faria o cliente ficar publicando um registro que já está lá. # E o resultado é GRAVADO nos dois casos Sem isso, quem tenta e falha vê a tela exatamente igual à de antes de tentar — e não tem como saber se o sistema chegou a olhar.
curl -X GET 'https://api.pages.asender.net/v1/formularios'
POST/v1/formulariossem schema na spec
POST /v1/dominios.
Onde é usada
tela de domínios.
Efeitos
uma escrita. # O domínio nasce NÃO verificado E é isso que impede o pior desfecho do produto: sem a prova de DNS, qualquer conta declararia banco.com.br e passaria a receber certificado TLS para esse nome — servindo uma página de captura no domínio de outra pessoa, com cadeado verde.
uma escrita. Apagar TIRA a autorização de TLS, e é o único caminho para isso — de propósito: um domínio verificado que pudesse ser "desverificado" por outro caminho sairia do ar sem ninguém entender por quê.
uma leitura. Página SEM experimento responde 200 com o corpo vazio de variantes, e não 404: "esta página não tem teste" é uma resposta sobre o mundo, e a tela precisa dela para mostrar o botão de criar. 404 mandaria a tela tratar o caso normal como erro.
Exemplo
curl -X GET 'https://api.pages.asender.net/v1/formularios/id_AQUI'
responde 200 enquanto o processo está vivo. Não toca dependência: um liveness que consulta o banco derruba o pod quando o banco oscila.
Onde é usada
rota pública /healthz.
Efeitos
escreve JSON na resposta.
Exemplo
curl -X GET 'https://api.pages.asender.net/healthz'
ia
POST/v1/ia/gerar-paginasem schema na spec
POST /v1/paginas/gerar — manda o pedido ao provedor, saneia a saída e devolve o template.
Onde é usada
botão "gerar com IA" do editor.
Efeitos
uma chamada externa; NÃO grava nada. # Não grava: devolve o rascunho Gravar direto faria o modelo publicar na conta do cliente. O que sai daqui é uma sugestão que a pessoa vê no editor e salva se quiser — a versão continua nascendo do gesto dela. # Sem provedor, a rota DEGRADA 503 e não 500: é configuração ausente, e a distinção separa "o produto não tem essa função ligada" de "o produto quebrou". O resto do serviço segue de pé — nenhuma página deixa de ser servida porque não há chave de IA (piso 10).
GET/internal/v1/formularios/{id}sem schema na spec
GET /internal/v1/formularios/{id}.
Onde é usada
chamado pelo asender_runtime para validar uma submissão.
Efeitos
uma leitura. # Sem o tenant na rota, de propósito No modo SNIPPET o formulário está embutido no site de um terceiro e quem posta é o navegador de um visitante anônimo. O runtime não sabe de qual conta é o formulário — a conta é a RESPOSTA, tirada dele. Exigir o tenant na rota tornaria esta chamada impossível de fazer. Isto não afrouxa nada: o public_id é opaco e já está no HTML de quem visita o site. Quem o conhece já podia postar nele. O que protege esta rota é a credencial de serviço — sem ela, ela não responde nada.
Exemplo
curl -X GET 'https://api.pages.asender.net/internal/v1/formularios/id_AQUI'
GET/internal/v1/paginassem schema na spec
GET /internal/v1/paginas?dominio=&slug=.
Onde é usada
chamado pelo asender_runtime a cada requisição de página pública.
Efeitos
uma leitura. # Query string e não caminho O domínio contém pontos e o slug pode conter hífen; os dois em segmentos de rota exigiriam escape que o roteador desfaz de formas diferentes conforme a versão. Query string é literal, e o que trafega aqui é rede interna.
Exemplo
curl -X GET 'https://api.pages.asender.net/internal/v1/paginas'
POST/internal/v1/paginas/{id}/cliquessem schema na spec
POST /internal/v1/paginas/{id}/cliques.
Onde é usada
descarga periódica do contador em memória do asender_runtime.
Efeitos
uma escrita por destino, somando ao que já existe. # Soma, e não atribui O corpo traz o DELTA do lote, nunca um total. Dois processos do runtime descarregando ao mesmo tempo somam certo; com atribuição, o último a escrever apagaria a contagem do outro — e o relatório mostraria menos visitas do que houve, sem nada indicar a perda. # Teto de destinos no corpo O mapa vem da rede. Sem teto, um lote com um milhão de chaves viraria um milhão de INSERT numa transação só. O teto é o mesmo do roteamento (20 destinos), porque é o máximo que uma página pode ter.
GET/internal/v1/paginas/publicadassem schema na spec
GET /internal/v1/paginas/publicadas?dominio=.
Onde é usada
chamado pelo asender_runtime para montar /sitemap.xml.
Efeitos
uma leitura. Devolve só slug e data: o sitemap não precisa de título nem template, e cada campo a mais seria dado do cliente saindo por uma rota que qualquer visitante alcança pelo runtime. Domínio sem página responde lista VAZIA, e não 404: "este host não tem página publicada" é uma resposta sobre o mundo, e o runtime precisa dela para devolver um sitemap vazio em vez de um erro.
Exemplo
curl -X GET 'https://api.pages.asender.net/internal/v1/paginas/publicadas'
GET/internal/v1/redirectssem schema na spec
GET /internal/v1/redirects?dominio=&slug=.
Onde é usada
chamado pelo asender_runtime a cada clique.
Efeitos
uma leitura. Não devolve o tenant_id: quem serve o redirect não precisa saber de quem ele é, e devolvê-lo daria a um visitante um mapa de qual conta encurta o quê.
Exemplo
curl -X GET 'https://api.pages.asender.net/internal/v1/redirects'
POST/internal/v1/redirects/cliquessem schema na spec
POST /internal/v1/redirects/cliques.
Onde é usada
descarga periódica do contador em memória do runtime.
Efeitos
uma escrita por link. SOMA, e não atribui: dois processos do runtime descarregando ao mesmo tempo somam certo; com atribuição, o último a escrever apagaria a contagem do outro.
uma consulta. # Fail-closed, e a resposta é um booleano Host desconhecido é false — negar é a resposta correta. Falha nossa é 503, e NÃO false: o servidor de borda tem de distinguir "não autorizado" de "não consegui decidir", senão um soluço de banco viraria recusa de certificado para domínio legítimo. A resposta não carrega tenant nem token. Quem monta o handshake não precisa disso, e devolvê-lo daria a quem sonda um mapa de quais domínios pertencem à plataforma.
Exemplo
curl -X GET 'https://api.pages.asender.net/internal/v1/tls/authorize'
midias
GET/v1/midiassem schema na spec
GET /v1/dominios.
Onde é usada
tela de domínios.
Efeitos
uma leitura.
Exemplo
curl -X GET 'https://api.pages.asender.net/v1/midias'
POST/v1/midiassem schema na spec
POST /v1/midias (multipart, campo `arquivo`).
Onde é usada
botão de upload do editor.
Efeitos
escreve no disco e no banco. # A ordem é DISCO e depois BANCO Se o disco falhar, nada foi gravado e o erro é honesto. A ordem inversa deixaria uma linha apontando para um arquivo inexistente, e o visitante receberia erro numa página publicada. O caso duplicado não grava nada: a mídia já está lá, com os mesmos bytes.
uma escrita. Apagar TIRA a autorização de TLS, e é o único caminho para isso — de propósito: um domínio verificado que pudesse ser "desverificado" por outro caminho sairia do ar sem ninguém entender por quê.
uma requisição a servidor de terceiro; escreve no disco e no banco. # Esta é a rota mais perigosa do serviço Ela faz o SERVIDOR buscar um endereço que o USUÁRIO escolheu — a definição de SSRF. Sem as guardas, http://169.254.169.254/latest/meta-data/ devolveria as credenciais da instância, e http://redis-interno:6379/ alcançaria um serviço que nunca deveria ver tráfego de fora. Toda a defesa está em domain/midia: allowlist de esquema e porta, recusa de IP interno, conferência REPETIDA no momento de discar (contra DNS rebinding), zero redirecionamento e teto de tamanho durante a leitura.
curl -X GET 'https://api.pages.asender.net/v1/paginas'
POST/v1/paginassem schema na spec
POST /v1/dominios.
Onde é usada
tela de domínios.
Efeitos
uma escrita. # O domínio nasce NÃO verificado E é isso que impede o pior desfecho do produto: sem a prova de DNS, qualquer conta declararia banco.com.br e passaria a receber certificado TLS para esse nome — servindo uma página de captura no domínio de outra pessoa, com cadeado verde.
uma leitura. Página SEM experimento responde 200 com o corpo vazio de variantes, e não 404: "esta página não tem teste" é uma resposta sobre o mundo, e a tela precisa dela para mostrar o botão de criar. 404 mandaria a tela tratar o caso normal como erro.
Exemplo
curl -X GET 'https://api.pages.asender.net/v1/paginas/id_AQUI'
PATCH/v1/paginas/{id}sem schema na spec
PATCH /v1/paginas/{id} — renomear, mudar o endereço público ou o domínio.
Onde é usada
tela de páginas do painel (renomear) e configurações da página.
Efeitos
uma escrita. # Só o que veio é tocado nil é "não mandado" e &"" é "mandado vazio". Sem essa distinção, editar o título apagaria o endereço público da página — e ela sairia do ar sem ninguém ter pedido. # Validação IGUAL à da criação O slug e o título passam pelas mesmas regras do Criar. Um PATCH mais frouxo seria a porta dos fundos: bastaria criar válido e editar para inválido para pôr no ar um endereço que a criação recusa.
POST/v1/paginas/{id}/ab/promoversem schema na spec
POST /v1/paginas/{id}/ab/promover.
Onde é usada
botão "promover vencedora" e botão "iniciar teste" da tela de A/B.
Efeitos
uma escrita. # Duas operações na mesma rota, e por quê O lynz expõe POST /v1/pages/{id}/ab/promote, sem rota de iniciar — o teste começa ao ser promovido a "rodando". Manter uma rota só preserva a superfície do lynz, e o corpo diz qual das duas transições é pedida. # Iniciar EXIGE o experimento válido É o único ponto onde o rascunho inconsistente é barrado, e é o ponto certo: pesos que não somam 100 deixariam uma faixa do tráfego sem dono, e Escolher devolveria a última variante para essa fatia — um viés silencioso que o número final não denuncia.
uma leitura. Devolve o CONTADOR, e não uma lista de eventos: a pergunta é sempre "quantos", nunca "quais". Uma linha por clique transformaria o caminho mais quente do redirect num INSERT por requisição.
Exemplo
curl -X GET 'https://api.pages.asender.net/v1/paginas/id_AQUI/cliques'
GET/v1/paginas/{id}/experimentosem schema na spec
GET /v1/paginas/{id}/experimento.
Onde é usada
tela de A/B do painel.
Efeitos
uma leitura. Página SEM experimento responde 200 com o corpo vazio de variantes, e não 404: "esta página não tem teste" é uma resposta sobre o mundo, e a tela precisa dela para mostrar o botão de criar. 404 mandaria a tela tratar o caso normal como erro.
Exemplo
curl -X GET 'https://api.pages.asender.net/v1/paginas/id_AQUI/experimento'
PUT/v1/paginas/{id}/experimento/auto-stopsem schema na spec
PUT /v1/paginas/{id}/experimento/auto-stop.
Onde é usada
tela de A/B.
Efeitos
uma escrita. Aceito com o teste RODANDO, ao contrário das variantes: apertar o critério não reatribui visitante nem invalida amostra — só muda quando o job pode concluir.
DELETE/v1/paginas/{id}/roteamentosem schema na spec
DELETE /v1/dominios/{id}.
Onde é usada
tela de domínios.
Efeitos
uma escrita. Apagar TIRA a autorização de TLS, e é o único caminho para isso — de propósito: um domínio verificado que pudesse ser "desverificado" por outro caminho sairia do ar sem ninguém entender por quê.
uma leitura. Página SEM experimento responde 200 com o corpo vazio de variantes, e não 404: "esta página não tem teste" é uma resposta sobre o mundo, e a tela precisa dela para mostrar o botão de criar. 404 mandaria a tela tratar o caso normal como erro.
Exemplo
curl -X GET 'https://api.pages.asender.net/v1/paginas/id_AQUI/roteamento'
curl -X GET 'https://api.pages.asender.net/v1/paginas/id_AQUI/variantes'
POST/v1/paginas/{id}/variantessem schema na spec
POST /v1/paginas/{id}/variantes.
Onde é usada
botão "nova variante" da tela de A/B.
Efeitos
uma ou duas escritas. # O experimento nasce aqui, e em rascunho Exigir uma chamada de "criar experimento" antes da primeira variante daria ao operador um objeto vazio que não mede nada e que ele teria de lembrar de apagar. A primeira variante cria o teste.
abre e transmite o arquivo. # Sem sessão, e por isso com cuidado extra O id é o único parâmetro, e ele é resolvido no BANCO antes de tocar o disco: o caminho do arquivo nunca é montado com o que veio da URL. Path traversal não tem por onde entrar — e o armazenamento recusa id fora do formato de qualquer jeito, como segunda linha. # X-Content-Type-Options: nosniff é obrigatório aqui O tipo é o DETECTADO no upload, e a allowlist só tem imagem. Sem nosniff, o navegador pode reinterpretar o conteúdo e executar como outra coisa — e a allowlist de tipos perderia o sentido no último metro.
Exemplo
curl -X GET 'https://api.pages.asender.net/v1/public/midias/id_AQUI'
readyz
GET/readyzsem schema na spec
503 `starting` antes do fim do boot; depois disso, CONSULTA as sondas e responde 200 `ready` ou 503 `degraded` com o nome de cada dependência.
Onde é usada
rota pública /readyz.
Efeitos
escreve JSON na resposta; pode tocar a dependência (com cache de 1s). # Por que ele não é mais um trinco de boot A readiness era MarkReady() uma vez e pronto — memória do boot, não estado atual. Num incidente real de 2026-08-27 o serviço voltou com a credencial errada do Postgres, falhou ~30 vezes por minuto ao drenar o outbox, e /healthz e /readyz responderam 200 o tempo todo: o gate de saúde do deploy passou e quem descobriu foi o pentest, depois. Readiness que só lembra do boot não enxerga a dependência que caiu DEPOIS dele. /healthz continua sem tocar em nada, e isso é de propósito: liveness que consulta o banco derruba o processo quando o banco oscila. Quem tem de dizer "não me mande tráfego" é o readiness. O que sai na resposta é o NOME da dependência e "indisponível" — nunca a mensagem do driver, que carrega usuário, host e base. Rota pública não conta topologia; o erro inteiro vai para o log.
Exemplo
curl -X GET 'https://api.pages.asender.net/readyz'
redirects
GET/v1/redirectssem schema na spec
GET /v1/dominios.
Onde é usada
tela de domínios.
Efeitos
uma leitura.
Exemplo
curl -X GET 'https://api.pages.asender.net/v1/redirects'
POST/v1/redirectssem schema na spec
POST /v1/dominios.
Onde é usada
tela de domínios.
Efeitos
uma escrita. # O domínio nasce NÃO verificado E é isso que impede o pior desfecho do produto: sem a prova de DNS, qualquer conta declararia banco.com.br e passaria a receber certificado TLS para esse nome — servindo uma página de captura no domínio de outra pessoa, com cadeado verde.
uma escrita. Apagar TIRA a autorização de TLS, e é o único caminho para isso — de propósito: um domínio verificado que pudesse ser "desverificado" por outro caminho sairia do ar sem ninguém entender por quê.
uma leitura. Página SEM experimento responde 200 com o corpo vazio de variantes, e não 404: "esta página não tem teste" é uma resposta sobre o mundo, e a tela precisa dela para mostrar o botão de criar. 404 mandaria a tela tratar o caso normal como erro.
Exemplo
curl -X GET 'https://api.pages.asender.net/v1/redirects/id_AQUI'
uma leitura. Devolve o CONTADOR, e não uma lista de eventos: a pergunta é sempre "quantos", nunca "quais". Uma linha por clique transformaria o caminho mais quente do redirect num INSERT por requisição.
Exemplo
curl -X GET 'https://api.pages.asender.net/v1/redirects/id_AQUI/cliques'
Publicação e analytics
Publica as páginas e recebe os eventos delas. Sistema próprio, endereço próprio.
https://api.sites.asender.net · 40 operações
{slug}
GET/{slug}sem schema na spec
resolve host+caminho na página publicada e devolve o HTML renderizado.
Onde é usada
rota curinga do runtime, a de maior tráfego do sistema.
Efeitos
consulta a fonte; escreve a resposta. 404 quando não há página: o host pode apontar para nós sem ter página naquele caminho, e devolver erro de servidor nesse caso confundiria monitoramento com tráfego normal de quem digitou a URL errada.
Exemplo
curl -X GET 'https://api.sites.asender.net/slug_AQUI'
POST/{slug}sem schema na spec
resolve a página pela mesma rota que a serviu, monta a submissão com `Modo = pagina` e chama o MESMO pipeline dos outros dois modos.
Onde é usada
POST no caminho da própria página.
Efeitos
os do pipeline (banco, outbox). O tenant e o form vêm da PÁGINA resolvida no servidor, nunca do corpo: o form da página é HTML público, e qualquer um pode reenviá-lo com outro form_id. Resolver pelo host+caminho é o que impede postar no formulário de outro tenant.
curl -X GET 'https://api.sites.asender.net/v1/analytics/ab'
GET/v1/analytics/breakdownsem schema na spec
GET /v1/analytics/breakdown?dimensao=utm_source&…
Onde é usada
tela de origens.
Efeitos
uma leitura.
Exemplo
curl -X GET 'https://api.sites.asender.net/v1/analytics/breakdown'
GET/v1/analytics/eventssem schema na spec
GET /v1/analytics/events?pagina=&limite=.
Onde é usada
tela de depuração da instrumentação.
Efeitos
uma leitura.
Exemplo
curl -X GET 'https://api.sites.asender.net/v1/analytics/events'
GET/v1/analytics/heatmapsem schema na spec
GET /v1/analytics/heatmap?pagina=&lado=.
Onde é usada
tela de mapa de calor.
Efeitos
uma leitura. # As posições são PORCENTAGEM, e não pixel Pixel absoluto é da tela de quem clicou: o mesmo botão sai em x=320 no celular e x=980 no monitor. A célula em porcentagem compara telas diferentes — que é a única pergunta que um mapa de calor responde.
Exemplo
curl -X GET 'https://api.sites.asender.net/v1/analytics/heatmap'
GET/v1/analytics/livesem schema na spec
GET /v1/analytics/live — sessões e eventos da janela curta.
Onde é usada
cabeçalho da tela ao vivo, no primeiro carregamento.
Efeitos
uma leitura. Existe além do stream porque quem abre a tela precisa ver ALGO antes do primeiro evento chegar. Sem isso, uma conta com movimento baixo mostraria tela vazia por minutos e pareceria quebrada.
Exemplo
curl -X GET 'https://api.sites.asender.net/v1/analytics/live'
GET/v1/analytics/live/streamsem schema na spec
GET /v1/analytics/live/stream — Server-Sent Events.
Onde é usada
tela ao vivo.
Efeitos
mantém uma conexão aberta e uma inscrição no hub. # SSE, e não WebSocket O fluxo é de mão única (servidor → tela) e o cliente é um navegador. SSE atravessa proxy comum, reconecta sozinho e cabe em quatro linhas de JS; WebSocket traria um protocolo inteiro para carregar dado que só desce.
Exemplo
curl -X GET 'https://api.sites.asender.net/v1/analytics/live/stream'
GET/v1/analytics/overviewsem schema na spec
GET /v1/analytics/overview?pagina=&de=&ate=.
Onde é usada
tela inicial de analytics.
Efeitos
duas leituras. Funil e atribuição juntos porque a tela mostra os dois lado a lado, e duas requisições poderiam ler janelas diferentes — o total da atribuição não fecharia com o topo do funil, e o número "errado" seria o certo.
Exemplo
curl -X GET 'https://api.sites.asender.net/v1/analytics/overview'
GET/v1/analytics/session/{id}sem schema na spec
GET /v1/analytics/session/{id}.
Onde é usada
tela de sessão — o que aquela visita fez, em ordem.
Efeitos
uma leitura. Sessão de outra conta responde lista VAZIA, e não 404: o id de sessão é opaco e gerado no browser, e distinguir "não existe" de "não é sua" transformaria a rota num oráculo de existência de sessão alheia.
Exemplo
curl -X GET 'https://api.sites.asender.net/v1/analytics/session/id_AQUI'
GET/v1/analytics/traffic-qualitysem schema na spec
GET /v1/analytics/traffic-quality.
Onde é usada
tela de qualidade — "esse tráfego pago é gente?".
Efeitos
uma leitura.
Exemplo
curl -X GET 'https://api.sites.asender.net/v1/analytics/traffic-quality'
collect
POST/collectsem schema na spec
POST /collect — abre a credencial, normaliza cada evento e publica.
Onde é usada
rota pública, a de maior volume do sistema depois da própria página.
Efeitos
publica no broker; NUNCA escreve no banco. # Responde 204 quase sempre, e isso é deliberado Credencial inválida é 403 (o cliente precisa saber que aquele HTML está velho). Fora isso — evento inválido, tipo desconhecido, lote parcialmente ruim — a resposta é 204 e o que dava para aproveitar foi publicado. O pixel roda no browser de terceiros: transformar um evento malformado em erro visível não conserta nada e enche o console de quem só quer ver a página.
valida a ORIGEM contra os domínios verificados do tenant, ecoa os cabeçalhos de CORS e chama o mesmo pipeline.
Onde é usada
rota pública, chamada de outro domínio pelo navegador.
Efeitos
os do pipeline. Aqui não há API key: quem chama é o navegador do visitante, e uma key no snippet seria segredo no cliente (§13.4). A defesa é a allowlist de origem, verificada NO SERVIDOR — o header Origin é posto pelo navegador e não pode ser forjado por script da própria página.
POST/v1/forms/{formID}/submissionssem schema na spec
autentica por API key (resolvida pelo middleware), lê o JSON e chama o pipeline.
Onde é usada
rota autenticada do runtime.
Efeitos
os do pipeline (banco, outbox). O tenant vem do CONTEXTO, posto pelo middleware que validou a API key — nunca do corpo (§37: confiar em tenant_id do cliente). É isso que faz a key de um tenant não alcançar o form de outro.
responde 200 enquanto o processo está vivo. Não toca dependência: um liveness que consulta o banco derruba o pod quando o banco oscila.
Onde é usada
rota pública /healthz.
Efeitos
escreve JSON na resposta.
Exemplo
curl -X GET 'https://api.sites.asender.net/healthz'
leads
POST/v1/leadssem schema na spec
autentica por API key (resolvida pelo middleware), lê o JSON e chama o pipeline.
Onde é usada
rota autenticada do runtime.
Efeitos
os do pipeline (banco, outbox). O tenant vem do CONTEXTO, posto pelo middleware que validou a API key — nunca do corpo (§37: confiar em tenant_id do cliente). É isso que faz a key de um tenant não alcançar o form de outro.
toda página publicada, uma vez por visita (depois é cache).
Efeitos
escreve o script. Cache de 1 hora com ETag: o script muda com o deploy, e revalidar de hora em hora custa um 304. immutable seria errado — o conteúdo muda no mesmo caminho.
Exemplo
curl -X GET 'https://api.sites.asender.net/pixel.js'
pixels
GET/v1/pixelssem schema na spec
GET /v1/pixels.
Onde é usada
tela de pixels.
Efeitos
uma leitura.
Exemplo
curl -X GET 'https://api.sites.asender.net/v1/pixels'
curl -X GET 'https://api.sites.asender.net/v1/pixels/id_AQUI'
PATCH/v1/pixels/{id}sem schema na spec
PATCH /v1/pixels/{id}.
Onde é usada
tela de detalhe.
Efeitos
uma escrita. PATCH parcial de verdade: campo ausente não é tocado. Sem isso, editar só o nome DESATIVARIA o pixel — e a coleta pararia sem ninguém ter pedido.
resolve host+caminho na página publicada e devolve o HTML renderizado.
Onde é usada
rota curinga do runtime, a de maior tráfego do sistema.
Efeitos
consulta a fonte; escreve a resposta. 404 quando não há página: o host pode apontar para nós sem ter página naquele caminho, e devolver erro de servidor nesse caso confundiria monitoramento com tráfego normal de quem digitou a URL errada.
Exemplo
curl -X GET 'https://api.sites.asender.net/r/slug_AQUI'
raiz
GET/sem schema na spec
resolve host+caminho na página publicada e devolve o HTML renderizado.
Onde é usada
rota curinga do runtime, a de maior tráfego do sistema.
Efeitos
consulta a fonte; escreve a resposta. 404 quando não há página: o host pode apontar para nós sem ter página naquele caminho, e devolver erro de servidor nesse caso confundiria monitoramento com tráfego normal de quem digitou a URL errada.
Exemplo
curl -X GET 'https://api.sites.asender.net/'
POST/sem schema na spec
resolve a página pela mesma rota que a serviu, monta a submissão com `Modo = pagina` e chama o MESMO pipeline dos outros dois modos.
Onde é usada
POST no caminho da própria página.
Efeitos
os do pipeline (banco, outbox). O tenant e o form vêm da PÁGINA resolvida no servidor, nunca do corpo: o form da página é HTML público, e qualquer um pode reenviá-lo com outro form_id. Resolver pelo host+caminho é o que impede postar no formulário de outro tenant.
503 `starting` antes do fim do boot; depois disso, CONSULTA as sondas e responde 200 `ready` ou 503 `degraded` com o nome de cada dependência.
Onde é usada
rota pública /readyz.
Efeitos
escreve JSON na resposta; pode tocar a dependência (com cache de 1s). # Por que ele não é mais um trinco de boot A readiness era MarkReady() uma vez e pronto — memória do boot, não estado atual. Num incidente real de 2026-08-27 o serviço voltou com a credencial errada do Postgres, falhou ~30 vezes por minuto ao drenar o outbox, e /healthz e /readyz responderam 200 o tempo todo: o gate de saúde do deploy passou e quem descobriu foi o pentest, depois. Readiness que só lembra do boot não enxerga a dependência que caiu DEPOIS dele. /healthz continua sem tocar em nada, e isso é de propósito: liveness que consulta o banco derruba o processo quando o banco oscila. Quem tem de dizer "não me mande tráfego" é o readiness. O que sai na resposta é o NOME da dependência e "indisponível" — nunca a mensagem do driver, que carrega usuário, host e base. Rota pública não conta topologia; o erro inteiro vai para o log.
Exemplo
curl -X GET 'https://api.sites.asender.net/readyz'
replay
POST/replaysem schema na spec
POST /replay — abre a credencial, saneia e publica.
Onde é usada
rota pública, chamada pelo pixel a cada poucos segundos de uma sessão gravada.
Efeitos
publica no broker; NUNCA escreve no banco. Responde 204 quase sempre, como o /collect: credencial inválida é 403 (o HTML está velho), e o resto é aproveitado. O pixel roda no browser de terceiros, e transformar um lote parcialmente ruim em erro visível não conserta nada.
curl -X GET 'https://api.sites.asender.net/v1/replay/sessions'
GET/v1/replay/sessions/{id}/eventssem schema na spec
GET /v1/replay/sessions/{id}/events.
Onde é usada
player da tela de replay.
Efeitos
uma leitura. Sessão de outra conta responde lista VAZIA, e não 404: o id é opaco e gerado no browser, e distinguir "não existe" de "não é sua" transformaria a rota num oráculo de existência de sessão alheia.
Exemplo
curl -X GET 'https://api.sites.asender.net/v1/replay/sessions/id_AQUI/events'
robots.txt
GET/robots.txtsem schema na spec
libera a indexação e aponta o sitemap do MESMO host.
Onde é usada
rota pública.
Efeitos
nenhum. # Por que liberar, e não bloquear A página existe para ser achada: é landing page de campanha, e quem a publica quer tráfego. Um Disallow: / como padrão transformaria uma decisão de produto ("esta página não deve ser indexada") em comportamento da plataforma — e o cliente descobriria meses depois, sem entender por que o anúncio orgânico nunca apareceu. O sitemap é apontado com o HOST da requisição, e não com um domínio configurado: cada cliente tem o dele, e um endereço fixo aqui mandaria o buscador de todo mundo para o sitemap de um só.
Exemplo
curl -X GET 'https://api.sites.asender.net/robots.txt'
sitemap.xml
GET/sitemap.xmlsem schema na spec
lista as páginas PUBLICADAS daquele host.
Onde é usada
rota pública.
Efeitos
uma leitura no asender_pages. # Host sem página responde XML VAZIO, e não 404 Um 404 no sitemap faz o buscador registrar erro e tentar de novo; um documento vazio diz "não há nada para indexar aqui", que é a verdade. E o host que ainda não tem página publicada é o caso mais comum logo depois de alguém cadastrar um domínio. # Rascunho não entra A consulta do asender_pages filtra por versão publicada. Listar rascunho entregaria ao buscador o endereço de uma página que o cliente ainda não quis mostrar — e o buscador não esquece.
Exemplo
curl -X GET 'https://api.sites.asender.net/sitemap.xml'
version
GET/versionsem schema na spec
nome do serviço, versão e o commit que gerou o binário.
Onde é usada
rota pública, usada por ops e por quem investiga incidente.
Efeitos
escreve JSON na resposta. # Por que ela é PÚBLICA, e o que ela não conta A primeira pergunta de todo incidente é "que versão está no ar?" — e ela tem de ser respondível sem credencial, porque quem investiga muitas vezes ainda não tem uma. O que sai é o que já está no docker inspect de quem tem acesso ao host: nome, versão e commit. Nunca configuração, nunca dependência, nunca endereço interno — isso é topologia, e topologia é do console de plataforma. A versão vem de LDFLAGS no build, e o default é dev: um binário sem carimbo diz dev em vez de mentir uma versão que ninguém emitiu.
Exemplo
curl -X GET 'https://api.sites.asender.net/version'