Documentação das APIs — Conecta Move
Documento gerado em 2026-09-20 a partir do código-fonte real do backend (app/backend/src/conecta_move). Cobre as 3 famílias de rotas voltadas a produto: Admin, Portal da Tenant e Consulta Pública. Total: 72 endpoints.
Visão geral do sistema
O Conecta Move opera o programa federal Move Brasil (crédito/benefício a motoristas de aplicativo) para múltiplas operadoras ("Tenants"), integrando duas plataformas externas:
- Machine — plataforma onde vivem os motoristas/corridas das operadoras (consultada para verificar elegibilidade: status do motorista, continuidade cadastral, número de corridas).
- SERPRO — canal oficial do governo para troca de arquivos (
autoscp): recebe solicitações de CPF a verificar e devolve o resultado de elegibilidade.
Um único backend (FastAPI/Python, app/backend) expõe 4 famílias de API, cada uma servida por um domínio/app diferente em produção:
| Família | Domínio de produção | App cliente | Autenticação |
|---|---|---|---|
Admin (/api/admin/*) | admin.conectamove.tech | app/admin-web | JWT Bearer, usuário super_admin |
Portal da Tenant (/api/portal/*) | portal.conectamove.tech | app/tenant-web | JWT Bearer, "tenant owner" (dono da operadora) |
Consulta Pública (/api/public/*) | consulta.conectamove.tech | app/public-web | nenhuma (uso por motoristas) |
Esta referência cobre as três famílias de API voltadas a produto acima. Mecanismos internos de infraestrutura não são documentados publicamente.
Nota sobre este domínio (api.conectamove.tech): esta página/documento descrevem a API, mas não são a própria API. As chamadas reais continuam acontecendo pelos domínios da tabela acima (cada app já embute a URL certa) —api.conectamove.techhospeda apenas esta documentação.
Convenções gerais
- Formato: todas as requisições e respostas são JSON (
Content-Type: application/json), exceto upload de pacote AutoSCP (multipart/form-data) e exportação de CSV (text/csv). - Autenticação: quando aplicável,
Authorization: Bearer <token>. Token ausente/inválido/expirado →401 Unauthorized. Usuário autenticado mas sem permissão →403 Forbidden. Cada família de API (Admin / Portal) tem seu próprio JWT — um token de admin não funciona no portal e vice-versa. - Erros: respostas de erro seguem o padrão FastAPI,
{"detail": "mensagem"}(ou lista de erros de validação Pydantic em422). - IDs: todos os identificadores de recurso são UUID v4.
- Datas: ISO 8601 com timezone (
datetime) ouYYYY-MM-DD(date), sempre UTC quando não especificado. - Paginação: listas grandes usam
limit/offsetcomo query params, com resposta no formato{items: [...], total, limit, offset}. - CPF: nunca trafega nem é armazenado em texto plano fora do momento estritamente necessário — é cifrado em repouso (Fernet) com um índice HMAC para busca; nas respostas do Portal, sempre mascarado (
*.*.*89-01); na Consulta Pública, nunca é ecoado de volta.
1. API Admin (/api/admin/*)
Consumida por app/admin-web, servida em produção via admin.conectamove.tech. Todos os endpoints abaixo exigem Authorization: Bearer <jwt> de um AdminUser com role=super_admin, exceto o login.
1.1 Autenticação, Tenants, Bloqueios Operacionais e Observabilidade
Escopo desta parte: 14 endpoints em 4 routers — api/admin/auth.py (2), api/admin/tenants.py (7), api/admin/operation_control.py (4), api/admin/observability.py (1).
Autenticação padrão: todos os endpoints exceto POST /api/admin/auth/login exigem Authorization: Bearer <jwt> validado por require_super_admin (api/dependencies.py) — decodifica um JWT HS256, carrega o AdminUser, e rejeita se is_active=false, se token_version do usuário não bater com o claim ver do token, ou se role != "super_admin". Resposta em caso de falha: 401 Unauthorized com header WWW-Authenticate: Bearer.
POST/api/admin/auth/login
O que faz: autentica um administrador por usuário/senha e emite um token JWT de acesso.
Autenticação: nenhuma (endpoint público de login).
Path params: nenhum.
Query params: nenhum.
Request body (AdminLoginRequest):
| campo | tipo | obrigatório | validação |
|---|---|---|---|
username | string | sim | 1–100 caracteres |
password | string | sim | 1–512 caracteres |
Response — 200 OK (AdminTokenResponse):
| campo | tipo |
|---|---|
access_token | string (JWT) |
token_type | string, sempre "bearer" |
expires_in | int (segundos até expirar) |
Erros possíveis: 401 Unauthorized ("Usuario ou senha invalidos") se usuário não existe ou senha não confere (hash argon2, pwdlib).
Regras de negócio: em login bem-sucedido, admin_users.last_login_at é atualizado. O token embute token_version do usuário no momento da emissão — revogar todos os tokens de um admin é feito incrementando token_version no banco (fora deste endpoint), o que invalida instantaneamente qualquer token emitido antes.
Exemplo:
POST /api/admin/auth/login
Content-Type: application/json
{"username": "admin", "password": "minhasenha123"}
{"access_token": "eyJhbGciOi...", "token_type": "bearer", "expires_in": 3600}
GET/api/admin/auth/me
O que faz: retorna os dados do administrador autenticado (para a UI confirmar sessão/identidade).
Autenticação: Bearer JWT super_admin.
Path/Query params: nenhum.
Request body: nenhum.
Response — 200 OK (AdminMeResponse):
| campo | tipo | |
|---|---|---|
id | UUID | |
username | string | |
email | string \ | null |
role | string | |
is_active | bool | |
last_login_at | datetime \ | null |
created_at | datetime |
Erros possíveis: 401 Unauthorized se token ausente/inválido/expirado/revogado.
Regras de negócio: nenhuma além da autenticação.
Exemplo:
GET /api/admin/auth/me
Authorization: Bearer eyJhbGciOi...
{"id":"b1a2...","username":"admin","email":"admin@conectamove.tech","role":"super_admin","is_active":true,"last_login_at":"2026-09-20T22:00:00Z","created_at":"2026-01-10T12:00:00Z"}
GET/api/admin/tenants
O que faz: lista/busca tenants (operadoras) com filtros e paginação.
Autenticação: Bearer JWT super_admin.
Path params: nenhum.
Query params:
| nome | tipo | obrigatório | default | descrição | ||||
|---|---|---|---|---|---|---|---|---|
q | string | não | — | busca por name ou slug (ILIKE, 1–150 chars) | ||||
operational_status | enum (implementation\ | active\ | suspended\ | blocked\ | closed) | não | — | filtro exato |
financial_status | enum (not_applicable\ | current\ | overdue) | não | — | filtro exato | ||
limit | int | não | 50 | 1–100 | ||||
offset | int | não | 0 | ≥0 |
Response — 200 OK (TenantListResponse): { items: TenantResponse[], total: int, limit: int, offset: int }, ordenado por created_at desc, id. TenantResponse: id, name, slug, operational_status, financial_status, created_at, updated_at.
Erros possíveis: nenhum além de 401/422 (parâmetros inválidos).
Regras de negócio: financial_status é sempre derivado automaticamente pelo sistema — nunca setado manualmente (ver PATCH /{tenant_id}).
Exemplo:
GET /api/admin/tenants?q=53&operational_status=active&limit=10
{"items":[{"id":"...","name":"53 Driver","slug":"53-driver","operational_status":"active","financial_status":"current","created_at":"...","updated_at":"..."}],"total":1,"limit":10,"offset":0}
POST/api/admin/tenants
O que faz: cria uma nova tenant e já provisiona suas 4 capabilities padrão (desabilitadas).
Autenticação: Bearer JWT super_admin.
Request body (TenantCreateRequest):
| campo | tipo | obrigatório | validação |
|---|---|---|---|
name | string | sim | 1–150 chars, trim, não pode ficar vazio após trim |
slug | string | sim | 1–100 chars, regex ^[a-z0-9]+(?:-[a-z0-9]+)*$, normalizado para minúsculo/trim antes de validar |
operational_status | enum | não | default "implementation" |
financial_status | enum | não | default "not_applicable" |
Response — 201 Created (TenantResponse).
Erros possíveis: 409 Conflict ("Slug de Tenant ja utilizado") se já existe tenant com o mesmo slug; 409 Conflict ("Tenant ja existente") em race condition de integridade no commit.
Regras de negócio: ao criar, o sistema insere automaticamente 4 linhas em tenant_capabilities (machine, move_brasil, serpro, finance), todas com enabled=false e required_for_operation=false — a tenant recém-criada nunca está pronta para operar até essas capabilities serem configuradas.
Exemplo:
POST /api/admin/tenants
{"name": "53 Driver", "slug": "53-driver"}
{"id":"9f1c...","name":"53 Driver","slug":"53-driver","operational_status":"implementation","financial_status":"not_applicable","created_at":"...","updated_at":"..."}
GET/api/admin/tenants/{tenant_id}
O que faz: retorna os dados de uma tenant específica.
Path params: tenant_id (UUID).
Response — 200 OK (TenantResponse).
Erros possíveis: 404 Not Found ("Tenant nao encontrada").
GET/api/admin/tenants/{tenant_id}/capabilities
O que faz: lista as 4 capabilities (feature flags) da tenant e seu estado atual.
Path params: tenant_id (UUID).
Response — 200 OK (TenantCapabilitiesResponse): { items: [{code, enabled, required_for_operation}, ...] } — apenas as capabilities já existentes no banco para essa tenant, na ordem fixa machine, move_brasil, serpro, finance.
Erros possíveis: 404 Not Found se a tenant não existe.
PATCH/api/admin/tenants/{tenant_id}/capabilities/{code}
O que faz: liga/desliga uma capability da tenant e define se ela é obrigatória para operação.
Path params: tenant_id (UUID); code (enum machine\|move_brasil\|serpro\|finance).
Request body (TenantCapabilityUpdateRequest):
| campo | tipo | obrigatório | validação |
|---|---|---|---|
enabled | bool | sim | — |
required_for_operation | bool | sim | se true, enabled também precisa ser true (validação a nível de schema, espelha o CHECK constraint do banco) |
Response — 200 OK (TenantCapabilityResponse): {code, enabled, required_for_operation}.
Erros possíveis: 404 Not Found (tenant ou capability inexistente); 422 se required_for_operation=true com enabled=false; 409 Conflict em falha de integridade no commit.
Regras de negócio: capabilities habilitadas/obrigatórias determinam quais checagens de prontidão (readiness) se aplicam e bloqueiam a operação da tenant — ver GET /{tenant_id}/readiness e o gate de produção documentado no CLAUDE.md.
Exemplo:
PATCH /api/admin/tenants/9f1c.../capabilities/serpro
{"enabled": true, "required_for_operation": true}
GET/api/admin/tenants/{tenant_id}/readiness
O que faz: calcula (em tempo real, não é uma tabela) se a tenant está pronta para operar, checando cada capability habilitada.
Path params: tenant_id (UUID).
Response — 200 OK (TenantReadinessResponse):
{ ready: bool, problems: string[], checks: [{key, label, applicable, required_for_operation, ready, detail}, ...] }
Erros possíveis: 404 Not Found.
Regras de negócio: implementado em services/tenant_readiness.inspect_tenant_readiness. Para cada capability habilitada verifica suporte real: machine exige integração active com ≥1 conta ativa e teste de conexão validado; move_brasil exige TenantProgram active+credentialing_status=approved; serpro exige ≥1 SerproChannel active; finance exige perfil de cobrança habilitado e configurado. Se nenhuma capability operacional estiver habilitada, a tenant nunca está pronta.
PATCH/api/admin/tenants/{tenant_id}
O que faz: atualiza dados cadastrais e/ou o status operacional da tenant.
Path params: tenant_id (UUID).
Request body (TenantUpdateRequest, todos os campos opcionais — só os enviados são alterados, via exclude_unset):
| campo | tipo | validação | |||||
|---|---|---|---|---|---|---|---|
name | string \ | null | 1–150 chars, trim | ||||
slug | string \ | null | 1–100 chars, regex ^[a-z0-9]+(?:-[a-z0-9]+)*$, normalizado | ||||
operational_status | enum \ | null | implementation\ | active\ | suspended\ | blocked\ | closed |
financial_status | enum \ | null | presente no schema, mas nunca deveria ser setado manualmente por regra de negócio — ver nota abaixo |
Response — 200 OK (TenantResponse).
Erros possíveis: 404 Not Found; 409 Conflict ("Tenant ainda não está pronta para operação: ...") se tentar mudar operational_status para active e a readiness falhar (lista os problemas); 409 Conflict ("Slug de Tenant ja utilizado") se o novo slug colidir com outra tenant; 409 Conflict genérico em falha de integridade.
Regras de negócio: mudar para operational_status=active dispara a checagem de readiness completa — só é aceito se ready=true. Nota de inconsistência do código: embora o CLAUDE.md documente que financial_status é sempre derivado automaticamente (nunca setado manualmente), o schema TenantUpdateRequest aceita esse campo e o endpoint o aplica sem restrição — na prática, o valor tende a ser sobrescrito no próximo ciclo de sincronização financeira (finance_operation_sync), mas o endpoint não impede o set manual.
Exemplo:
PATCH /api/admin/tenants/9f1c.../
{"operational_status": "active"}
GET/api/admin/tenants/{tenant_id}/operation-state
O que faz: retorna o estado operacional consolidado de uma tenant (status de execução + bloqueios ativos) — resumo do gate de produção sem forçar reprocessamento.
Path params: tenant_id (UUID).
Response — 200 OK (TenantOperationStateResponse):
{
tenant_id, tenant_operational_status,
execution_status: "not_started"|"not_ready"|"running"|"paused"|"suspended"|"blocked"|"closed",
can_process: bool,
active_blocks_count: int,
active_blocks: TenantOperationBlockResponse[]
}
Erros possíveis: 404 Not Found.
Regras de negócio: execution_status/can_process vêm de services/tenant_operation_control.py, refletindo a mesma lógica do gate de produção (tenant_production_gate).
GET/api/admin/tenants/{tenant_id}/operation-blocks
O que faz: lista os bloqueios operacionais (histórico ou só ativos) de uma tenant.
Path params: tenant_id (UUID).
Query params: active_only (bool, default false) — filtra resolved_at IS NULL.
Response — 200 OK (TenantOperationBlockListResponse): {items: TenantOperationBlockResponse[], total: int}, ordenado por created_at desc, id desc. TenantOperationBlockResponse: id, tenant_id, reason_code, source, source_reference_type, source_reference_id, notes, resolved_at, resolution_notes, created_at, updated_at.
Erros possíveis: 404 Not Found.
POST/api/admin/tenants/{tenant_id}/operation-blocks
O que faz: cria um bloqueio operacional manual sobre a tenant (pausa a operação por decisão do admin).
Path params: tenant_id (UUID).
Request body (TenantOperationBlockCreateRequest):
| campo | tipo | obrigatório | validação | ||||
|---|---|---|---|---|---|---|---|
reason_code | enum manual_admin\ | security\ | technical\ | contract\ | other | sim | — |
notes | string \ | null | não | até 4000 chars |
Response — 201 Created (TenantOperationBlockResponse), sempre com source="admin".
Erros possíveis: 404 Not Found; 409 Conflict ("A operacao so pode ser pausada quando a Tenant estiver liberada para operacao") se operational_status != "active"; 409 Conflict ("Tenant encerrada nao recebe novos bloqueios operacionais") se operational_status == "closed".
Regras de negócio: só é possível criar bloqueio manual sobre uma tenant active. reason_code=finance_overdue não é criável por este endpoint — esse é reservado ao processo automático de sincronização financeira.
POST/api/admin/operation-blocks/{block_id}/resolve
O que faz: resolve (encerra) um bloqueio operacional manual.
Path params: block_id (UUID).
Request body (TenantOperationBlockResolveRequest): { resolution_notes: string | null (até 4000 chars) }. Response — 200 OK (TenantOperationBlockResponse), com resolved_at preenchido.
Erros possíveis: 404 Not Found; 409 Conflict ("Bloqueio financeiro deve ser resolvido pela regularizacao financeira da Tenant") se source == "finance" — blocks financeiros só se fecham sozinhos via finance_operation_sync; 409 Conflict ("Bloqueio operacional ja esta resolvido") se já tinha resolved_at.
GET/api/admin/observability/overview
O que faz: dashboard consolidado — visão geral de tenants, processamento, issues, canais SERPRO, runner e gates, usado pela tela inicial do admin.
Path/Query params: nenhum.
Response — 200 OK (OperationalObservabilityOverviewResponse):
{
tenants: { total, by_operational_status: {status: count}, serpro_flow_enabled, serpro_flow_allowed, serpro_flow_blocked },
processing: { total, by_status: {status: count}, waiting_publication },
issues: { open_total, open_by_severity: {severity: count} },
serpro: { channels_total, channels_by_status: {status: count}, deliveries_waiting_txok },
runner: { status, cycle_started_at, cycle_finished_at, last_success_at, last_error_at, last_error_code, last_error_message, heartbeat_at },
tenant_gates: [{tenant_id, tenant_name, tenant_slug, operational_status, execution_status, processing_allowed, active_block_count, problems[]}, ...],
recent_batches: [{batch_id, tenant_name, tenant_slug, source_filename, status, error_message, created_at, updated_at}, ...] (até 20, mais recentes primeiro)
}
Erros possíveis: nenhum além de 401.
Regras de negócio: waiting_publication conta lotes em response_generated (decididos, aguardando publicação manual ou automática). deliveries_waiting_txok conta entregas já liberadas na outbox mas sem confirmação de transmissão SERPRO (txok) ainda. runner reflete a linha única de heartbeat (serpro_runner_runtime, runner_code="serpro") — se não existe (nunca rodou), retorna status="not_started" com todos os timestamps null. tenant_gates avalia o gate de produção completo (machine+move_brasil+serpro) para cada tenant com a capability serpro habilitada, sem sincronizar financeiro nessa chamada (synchronize_finance=False) para não gerar efeitos colaterais numa tela de leitura.
1.2 Integração Machine, Financeiro, Pendências e Proprietário da Tenant
Nota de anomalia encontrada no código (não corrigida, só reportada): em api/admin/machine.py o endpoint PATCH /api/admin/machine/integrations/{integration_id} está definido duas vezes, de forma idêntica, e em schemas/admin_machine.py a classe MachineIntegrationUpdateRequest também está duplicada. Não afeta o comportamento (FastAPI usa a última definição), mas é código morto/duplicado.
Todos os endpoints abaixo exigem Authorization: Bearer <JWT> válido, usuário ativo, token_version batendo com a claim ver, e role == super_admin (senão 401 se token inválido/inativo, 403 se autenticado mas sem role super_admin).
api/admin/machine.py — prefixo /api/admin
GET/api/admin/tenants/{tenant_id}/integrations
O que faz: lista todas as integrações de plataforma (hoje só machine) que uma Tenant possui, com status e settings.
Autenticação: Bearer JWT super_admin.
Path params: tenant_id (UUID).
Response 200: list[TenantIntegrationResponse] — cada item: id, platform_id, platform_code, platform_name, external_app_id: str|null, status: str, settings: dict, created_at, updated_at.
Erros: 404 se tenant não existe.
Exemplo:
[{"id":"b1e...","platform_id":"a2c...","platform_code":"machine","platform_name":"Machine","external_app_id":null,"status":"active","settings":{},"created_at":"2026-01-10T12:00:00Z","updated_at":"2026-01-10T12:00:00Z"}]
GET/api/admin/tenants/{tenant_id}/machine/accounts
O que faz: lista as "centrais" (contas nomeadas dentro da integração Machine) de uma Tenant, com status de credenciais configuradas.
Path params: tenant_id (UUID).
Response 200: MachineAccountListResponse = {items: MachineAccountResponse[], total: int}. Se a Tenant não tem integração Machine ainda, retorna items: [], total: 0 (não dá 404).
MachineAccountResponse: id, tenant_integration_id, name, code, external_id: str|null, status: "active"|"inactive", settings: dict, created_at, updated_at, credentials: {username_configured, password_configured, api_key_configured}.
Erros: 404 se tenant não existe.
POST/api/admin/tenants/{tenant_id}/machine/accounts
O que faz: cria uma nova central Machine para a Tenant. Cria a integração TenantIntegration automaticamente se ainda não existir (status inicial inactive).
Path params: tenant_id (UUID).
Request body (MachineAccountCreateRequest):
name: str(1–150 chars, trim, não pode ficar vazio)code: str(1–100 chars, regex^[a-z0-9]+(?:-[a-z0-9]+)*$, normalizado para lowercase/trim antes de validar)external_id: str|null(≤150 chars)settings: dict(default{})
Response 201: MachineAccountResponse (ver acima). Central nasce sempre com status: "inactive".
Erros: 404 tenant não existe; 409 código já usado por outra central da mesma tenant (checagem prévia + IntegrityError como rede de segurança).
Regra de negócio: toda central nova nasce inativa — precisa ser ativada explicitamente depois de configurar credenciais e passar no teste de conexão.
GET/api/admin/machine/accounts/{account_id}
O que faz: detalhe de uma central Machine específica.
Path params: account_id (UUID).
Response 200: MachineAccountResponse. Erros: 404 central não encontrada.
PATCH/api/admin/machine/accounts/{account_id}
O que faz: atualiza campos de uma central (nome, código, id externo, status, settings) — só os campos enviados são alterados (exclude_unset).
Request body (MachineAccountUpdateRequest, todos opcionais): name (1–150), code (1–100, mesmo regex), external_id (≤150), status: "active"|"inactive", settings: dict.
Response 200: MachineAccountResponse.
Erros: 404 central não encontrada; 409 ao tentar mudar status para active sem: (a) as 3 credenciais completas (username, password, api_key) — ou (b) settings.last_connection_test_success != true; 409 conflito de integridade (ex.: código duplicado).
Regra de negócio: ativação de central é gated por credenciais completas + teste de conexão validado (mesma regra que a readiness de Tenant verifica).
PUT/api/admin/machine/accounts/{account_id}/credentials
O que faz: define/atualiza as credenciais (username/password/api_key) de uma central, cifradas em repouso via save_credential (Fernet).
Request body (MachineCredentialsUpdateRequest, todos opcionais mas ao menos 1 obrigatório): username (1–500), password (1–500), api_key (1–1000). Campos null/omitidos são ignorados (não apaga credencial existente).
Response 200: MachineCredentialStatus = {username_configured, password_configured, api_key_configured}.
Erros: 404 central não encontrada; 422 se nenhum campo foi enviado.
Regra de negócio: alterar qualquer credencial invalida automaticamente o último teste de conexão (last_connection_test_success volta pra false, remove last_connection_test_at) — força um novo teste antes de poder ativar a central.
GET/api/admin/machine/accounts/{account_id}/credentials
O que faz: retorna só o status (quais credenciais estão configuradas), nunca os valores em si.
Response 200: MachineCredentialStatus. Erros: 404.
POST/api/admin/machine/accounts/{account_id}/test-connection
O que faz: testa a conexão real com a plataforma Machine usando as credenciais salvas, buscando um CPF de teste — sem persistir o CPF, só valida se autentica.
Request body (MachineConnectionTestRequest): test_cpf: str (1–30 chars, normalizado via normalize_cpf). Response 200 (MachineConnectionTestResponse): success: true, driver_found: bool|null, account_status: str, tested_at: datetime, message: str.
Erros: 404 central não encontrada; 409 credenciais incompletas; 422 CPF de teste inválido; 502 falha de autenticação na Machine (MachineAuthenticationError) ou erro de requisição (MachineRequestError); 503 Machine temporariamente indisponível (MachineTemporaryError).
Regra de negócio: todo resultado (sucesso ou falha) atualiza settings.last_connection_test_success/_at/_error da central — inclusive em erro, para auditoria.
POST/api/admin/tenants/{tenant_id}/machine/discovery-test
O que faz: testa a "descoberta" de um motorista por CPF em todas as centrais ativas da Tenant (mesma lógica usada no pipeline real de elegibilidade) — útil pra debug de "por que esse CPF dá conflito/erro".
Path params: tenant_id.
Request body: test_cpf: str (1–30).
Response 200 (MachineDiscoveryTestResponse): outcome: "found"|"not_found"|"conflict", accounts_checked: int, accounts_found: int, selected_account_id/name: UUID|str|null, checks: [] (uma entrada por central testada, com account_id, account_name, processing_status, found, platform_status, status_changed_at, error_code, error_message).
Erros: 404 tenant; 422 CPF inválido; 409 tenant sem nenhuma central Machine ativa; 502 falha operacional numa central durante a descoberta (MachineDiscoveryOperationalError, indica qual central falhou).
Regra de negócio: outcome: "conflict" = motorista achado em mais de uma central simultaneamente — o sistema nunca escolhe uma sozinho nesse caso.
PATCH /api/admin/machine/integrations/{integration_id} (definido 2x no código, idêntico)
O que faz: ativa/desativa a integração Machine inteira da Tenant (não uma central específica).
Path params: integration_id (UUID).
Request body: {status: "active"|"inactive"}.
Response 200: TenantIntegrationResponse.
Erros: 404 integração Machine não encontrada; 409 ao ativar: se a Platform "machine" globalmente está inactive, se não há nenhuma central active, ou se alguma central ativa tem credenciais incompletas/teste de conexão não validado (lista os nomes das centrais inválidas na mensagem).
POST/api/admin/tenants/{tenant_id}/machine/eligibility-simulation
O que faz: roda a simulação completa da regra de elegibilidade Move Brasil pra um CPF+data hipotéticos, sem criar Request/ProcessingBatch real — é uma ferramenta de diagnóstico pro admin entender por que um motorista seria aprovado/reprovado.
Path params: tenant_id.
Request body (MachineEligibilitySimulationRequest): test_cpf: str (1–30), data_solicitacao: datetime (obrigatoriamente com timezone — 422 se naive). Response 200 (MachineEligibilitySimulationResponse, schema grande): answer: "sim"|"nao"|"bloqueado", eligible: bool, reason_code/message, selected_account_id/name, driver_status, status_changed_at, window_start/end, rule_version, minimo_corridas, janela_meses, continuidade_meses, corridas_desde_status_atual, status_aprovados: list[str], status_corrida_contabilizada, status_ok, continuity_ok, rides_consulted, valid_rides, rides_checked, pages_checked, minimum_reached, accounts_checked, accounts_found.
Erros: 404 tenant; 422 CPF inválido ou data sem timezone; 502 falha operacional na simulação.
api/admin/finance.py — prefixo /api/admin
Toda rota chama sync_finance_operation_state (recalcula financial_status, abre/fecha tenant_operation_blocks de fatura vencida) antes de responder — os dados de financeiro nunca ficam "desatualizados" na resposta.
GET/api/admin/tenants/{tenant_id}/billing-profile
O que faz: retorna o perfil de cobrança da Tenant (mensalidade, dia de vencimento, etc.), recalculando o financeiro antes.
Response 200 (TenantBillingProfileResponse): exists: bool (perfil nunca criado ainda = false, resto null), id, tenant_id, billing_enabled, monthly_amount: decimal|null, currency (default "BRL"), due_day: int|null, billing_start_date: date|null, grace_days: int, notes, financial_status (derivado da Tenant), created_at, updated_at.
Erros: 404 tenant.
PUT/api/admin/tenants/{tenant_id}/billing-profile
O que faz: cria ou substitui integralmente o perfil de cobrança (upsert).
Request body (TenantBillingProfileUpdateRequest): billing_enabled: bool (obrigatório); monthly_amount: decimal|null (≥0, até 12 dígitos/2 casas); currency: str (exatamente 3 letras maiúsculas, regex ^[A-Z]{3}$, default "BRL"); due_day: int|null (1–28); billing_start_date: date|null; grace_days: int (0–90, default 0); notes: str|null (≤4000).
Response 200: TenantBillingProfileResponse.
Erros: 404 tenant; 409 se billing_enabled=true e faltar monthly_amount (>0), due_day ou billing_start_date — cobrança ativa exige os 3.
GET/api/admin/tenants/{tenant_id}/invoices
O que faz: lista todas as faturas da Tenant, mais recentes primeiro, recalculando o financeiro antes.
Response 200: TenantInvoiceListResponse = {items: TenantInvoiceResponse[], total: int}. Cada item: id, tenant_id, reference_month: date, amount_due: decimal, due_date: date, status: "pending"|"paid"|"overdue"|"cancelled", amount_paid: decimal, paid_at: datetime|null, notes, created_at, updated_at.
Erros: 404 tenant.
POST/api/admin/tenants/{tenant_id}/invoices
O que faz: cria uma fatura manual para uma competência. Pode informar valor/vencimento explícitos, ou deixar em branco pra puxar do perfil de cobrança recorrente (se habilitado).
Request body (TenantInvoiceCreateRequest): reference_month: date (deve ser dia 1 do mês, senão erro de validação Pydantic); amount_due: decimal|null (≥0); due_date: date|null; notes: str|null (≤4000).
Response 201: TenantInvoiceResponse, status inicial sempre "pending", amount_paid inicial 0.00.
Erros: 404 tenant; 409 se amount_due omitido e perfil não tem cobrança habilitada/mensalidade definida; 409 mesma lógica pra due_date/due_day; 409 já existe fatura para essa competência (única por tenant+mês).
PATCH/api/admin/invoices/{invoice_id}
O que faz: edita campos de uma fatura existente (parcial).
Path params: invoice_id.
Request body (TenantInvoiceUpdateRequest, todos opcionais): amount_due: decimal|null (≥0); due_date: date|null; status: "pending"|"overdue"|"cancelled"|null (não aceita "paid" aqui — só via endpoint próprio); notes: str|null (≤4000).
Response 200: TenantInvoiceResponse.
Erros: 404 fatura; 409 se a fatura já está "paid" e tentam mudar amount_due, due_date ou status — fatura paga é imutável nesses campos.
POST/api/admin/invoices/{invoice_id}/mark-paid
O que faz: marca uma fatura como paga.
Request body (TenantInvoiceMarkPaidRequest): amount_paid: decimal|null (≥0; se omitido, usa amount_due da fatura); paid_at: datetime|null (se omitido, usa agora); notes: str|null (≤4000).
Response 200: TenantInvoiceResponse com status: "paid".
Erros: 404 fatura; 409 fatura já "cancelled" não pode ser paga; 409 amount_paid informado é menor que amount_due (não aceita pagamento parcial).
DELETE/api/admin/invoices/{invoice_id}
O que faz: exclui uma fatura permanentemente (hard delete) e recalcula o financeiro da Tenant em seguida.
Response 200: TenantInvoiceResponse (o snapshot da fatura antes de apagar).
Erros: 404 fatura.
Atenção: não há soft-delete/estorno aqui — é remoção definitiva do registro.
api/admin/processing_issues.py — prefixo /api/admin/processing-issues
GET/api/admin/processing-issues
O que faz: lista pendências operacionais (arquivos/lotes/requests com problema) com filtros, para a fila de trabalho do admin.
Query params (todos opcionais): scope: "file"|"request"|"batch"; status: "open"|"resolved"|"dismissed"; severity: "warning"|"error"|"critical"; tenant_id: UUID; limit: int (1–100, default 50); offset: int (≥0, default 0). Response 200 (ProcessingIssueListResponse): {items: ProcessingIssueResponse[], total, limit, offset}. Cada item: id, serpro_channel_id, batch_id|null, request_id|null, tenant_id|null, tenant_name|null, scope, issue_type: str, status, severity, code: str|null, message, source_filename|null, artifact_path|null, attempt_count: int, first_seen_at, last_seen_at, resolved_at|null, resolution_notes|null, details: dict.
GET/api/admin/processing-issues/{issue_id}
O que faz: detalhe de uma pendência específica.
Response 200: ProcessingIssueResponse. Erros: 404.
POST/api/admin/processing-issues/{issue_id}/release-reprocessing
O que faz: libera manualmente uma Request travada em error/blocked de volta para pending, para o próximo ciclo do runner reprocessar.
Request body: {resolution_notes: str|null}.
Response 200 (ProcessingIssueReleaseResponse): issue_id, request_id, issue_status, request_status, message: "Solicitacao liberada para reprocessamento".
Erros: 404 (issue some entre o release e o reload — raro); 409 (ProcessingIssueError — ex.: issue não está num estado liberável).
api/admin/tenant_owners.py — prefixo /api/admin/tenants
Gerencia a conta de usuário que o dono da Tenant usa para logar no portal (app/tenant-web, autenticação separada do admin). Relação 1:1 por tenant — sem endpoint de delete: desativação é feita via is_active.
GET/api/admin/tenants/{tenant_id}/owner
O que faz: retorna o proprietário cadastrado da Tenant, ou null se ainda não existe (não é erro).
Response 200: AdminTenantOwnerResponse | null — {id, tenant_id, tenant_name, username, email|null, is_active, last_login_at|null, created_at, updated_at}.
Erros: 404 tenant não existe.
POST/api/admin/tenants/{tenant_id}/owner
O que faz: cria o proprietário do portal para a Tenant (só pode existir 1 por tenant).
Request body (AdminTenantOwnerCreateRequest): username: str (1–100, normalizado/validado por normalize_tenant_owner_username); email: str|null (≤255, normalizado lowercase); password: str (8–500 chars — só o tamanho é validado aqui, hash via argon2).
Response 201: AdminTenantOwnerResponse, is_active: true por padrão.
Erros: 404 tenant; 409 tenant já tem um proprietário; 422 username inválido; 409 username/e-mail já usado por outro proprietário (constraint de unicidade).
PATCH/api/admin/tenants/{tenant_id}/owner
O que faz: atualiza username/email/status ativo do proprietário (parcial).
Request body (AdminTenantOwnerUpdateRequest, todos opcionais): username, email, is_active: bool|null.
Response 200: AdminTenantOwnerResponse.
Erros: 404 tenant ou proprietário não existe; 422 username inválido; 409 conflito de unicidade.
Regra de negócio: mudar is_active (pra qualquer direção, inclusive reativando) incrementa token_version — derruba todas as sessões ativas do owner no portal, mesmo ao reativar.
POST/api/admin/tenants/{tenant_id}/owner/reset-password
O que faz: admin força uma nova senha para o proprietário (fora do fluxo de "esqueci minha senha").
Request body: {password: str} (8–500).
Response 200: AdminTenantOwnerResponse.
Erros: 404.
Regra de negócio: também incrementa token_version — invalida qualquer sessão anterior do owner imediatamente.
1.3 Programas Nacionais, Canais SERPRO e Regras de Elegibilidade
api/admin/program_serpro.py — 21 endpoints, prefixo /api/admin
O APIRouter inteiro tem dependencies=[Depends(require_super_admin)] no nível do router — todos os 21 exigem Bearer JWT de super_admin.
GET/api/admin/programs
O que faz: lista o catálogo de programas nacionais disponíveis (hoje só move_brasil, semeado via seeds/001_move_brasil.sql).
Response: 200, list[ProgramResponse] — id (UUID), code, name, status ("active"|"inactive"), settings (dict), created_at/updated_at.
Erros: nenhum específico.
Exemplo:
[{"id":"...","code":"move_brasil","name":"Move Brasil","status":"active","settings":{},"created_at":"...","updated_at":"..."}]
GET/api/admin/tenants/{tenant_id}/programs
O que faz: lista as participações (adesões) de uma tenant em programas nacionais.
Path params: tenant_id (UUID).
Response: 200, list[TenantProgramResponse] — id, tenant_id, program_id, program_code, program_name, status (onboarding|active|suspended|closed), credentialing_status (not_started|waiting_client|submitted|under_review|approved|rejected), automatic_response_enabled (bool), settings, approved_at/activated_at (datetime|null), timestamps.
Erros: 404 se tenant não existe.
POST/api/admin/tenants/{tenant_id}/programs/{program_code}
O que faz: matricula uma tenant em um programa nacional (cria a "participação").
Path params: tenant_id (UUID), program_code (str, ex. move_brasil).
Request body (TenantProgramCreateRequest): settings: dict (opcional, default {}).
Response: 201, TenantProgramResponse — criado com status="onboarding", credentialing_status="not_started", automatic_response_enabled=False.
Erros: 404 tenant ou programa inexistente; 409 programa não está active; 409 tenant já participa deste programa.
PATCH/api/admin/tenant-programs/{tenant_program_id}
O que faz: atualiza status/credenciamento/config de uma participação — move o credenciamento pelo funil (not_started → ... → approved) e ativa a participação.
Path params: tenant_program_id (UUID).
Request body (TenantProgramUpdateRequest, parcial): status, credentialing_status, automatic_response_enabled (bool), settings (dict).
Response: 200, TenantProgramResponse.
Erros: 404; 409 se ligar automatic_response_enabled=True sem status=active+credentialing_status=approved; 409 se status=active sem credentialing_status=approved; 409 se ligar automatic_response_enabled=True sem nenhum SerproChannel active.
Regras de negócio: seta approved_at na transição para approved; seta activated_at na transição para active. Resposta automática do SERPRO exige: participação ativa + credenciamento aprovado + ≥1 canal SERPRO active.
GET/api/admin/tenant-programs/{tenant_program_id}/serpro-channels
O que faz: lista os canais SERPRO de uma participação em programa.
Response: 200, list[SerproChannelResponse] (schema completo no endpoint de detalhe abaixo).
Erros: 404.
POST/api/admin/tenant-programs/{tenant_program_id}/serpro-channels
O que faz: cria um novo canal SERPRO para a participação — ponto de partida do fluxo de integração SERPRO.
Request body (SerproChannelCreateRequest):
code: str, 1-100 chars, regex^[a-z0-9]+(?:-[a-z0-9]+)*$name: str, 1-150 charsserver_host: str|null, máx 255server_port: int|null, 1-65535mailbox_identifier: str|null, máx 150authorized_outbound_ips: list[str], default[]autoscp_base_path: str|null, máx 500expected_client_name: str|null, 1-150 chars, regex^[A-Za-z0-9_]+$settings: dict, default{}
Response: 201, SerproChannelResponse — status="pending_configuration", transport="autoscp" (fixo).
Erros: 404 participação inexistente; 409 código de canal já usado nesta participação.
GET/api/admin/serpro-channels/{channel_id}
O que faz: detalhe de um canal SERPRO específico.
Response: 200, SerproChannelResponse: id, tenant_program_id, code, name, status (pending_configuration|configured|testing|active|suspended|error|closed), transport, server_host, server_port, mailbox_identifier, authorized_outbound_ips, autoscp_base_path, expected_client_name, provisioning_status (not_provisioned|package_received|queued|provisioning|testing|ready|error|rolling_back), last_connectivity_check_at, last_connectivity_success_at, last_upload_at, last_download_at, last_txok_at, last_error_code, last_error_message, settings, configured_at, activated_at, created_at, updated_at.
Erros: 404.
PATCH/api/admin/serpro-channels/{channel_id}
O que faz: atualiza configuração do canal e controla transições de status — inclusive ativação (exige prontidão validada) e suspensão (desliga a comunicação real no host).
Request body (SerproChannelUpdateRequest, parcial): name, status, server_host, server_port, mailbox_identifier, authorized_outbound_ips, autoscp_base_path, expected_client_name, settings.
Response: 200, SerproChannelResponse.
Erros: 409 se status→active mas participação não está active+approved; 409 se status→active sem autoscp_base_path; 409 se status→active e a checagem de readiness reprovar; 503 se status→suspended e a chamada ao host control falhar.
Regras de negócio / efeitos colaterais:
- Ao mudar para
suspended(comprovisioning_status="ready"eautoscp_base_pathsetado): chama o host control client (socket Unix pro daemon root na VPS) para parar a unit systemd do canal. Se o socket falhar, a mudança é bloqueada com503. - Ao mudar para
configured/testing/activepela primeira vez: setaconfigured_at. - Ao mudar para
activevindo de outro status: setaactivated_at.
GET/api/admin/serpro-channels/{channel_id}/credentials
O que faz: retorna só se as credenciais SFG do canal estão configuradas (nunca o valor).
Response: 200, SerproChannelCredentialStatusResponse: username (bool), password (bool), package_password (bool), complete (bool).
Erros: 404.
PATCH/api/admin/serpro-channels/{channel_id}/credentials
O que faz: define/atualiza usuário, senha e senha do pacote pra autenticar no servidor SFG do SERPRO. Armazenadas cifradas.
Request body (SerproChannelCredentialsUpdateRequest, parcial): username (1-500), password (1-1000), package_password (1-1000).
Response: 200, mesmo shape do GET.
Erros: 404.
GET/api/admin/serpro-channels/{channel_id}/readiness
O que faz: checagem de prontidão operacional do canal (sem side effects) — usada pela UI e internamente antes de ativação/comunicação.
Response: 200, SerproChannelReadinessResponse: ready (bool), tenant_program_active, credentialing_approved, base_path_configured, base_path_exists, directories ({temp,inbox,outbox,enviados: bool}), scripts ({mget.sh,mput.sh,loop.sh: bool}), executable_scripts (mesmos scripts, bit de execução), problems (list[str]).
Erros: 404.
Regras de negócio: ready=True exige TODOS: participação active + credenciamento approved + autoscp_base_path existente no disco + os 4 diretórios existirem + os 3 scripts existirem e serem executáveis.
GET/api/admin/serpro-channels/{channel_id}/packages
O que faz: lista todos os pacotes AutoSCP (.tar.gz) já enviados/adquiridos para o canal, mais recentes primeiro.
Response: 200, list[SerproChannelPackageResponse]: id, serpro_channel_id, original_filename, sha256, size_bytes, status (uploaded|validated|invalid|installed|superseded), validated_at, timestamps.
Erros: 404.
POST/api/admin/serpro-channels/{channel_id}/packages
O que faz: upload manual de um pacote AutoSCP (.tar.gz) fornecido pelo SERPRO, pra usar no provisionamento do canal.
Request: multipart/form-data, campo file (obrigatório), deve terminar em .tar.gz.
Response: 201 (novo pacote) ou 200 (pacote com mesmo sha256 já existia — idempotente), SerproChannelPackageResponse.
Erros: 422 nome não termina em .tar.gz; 413 excede settings.serpro_package_max_bytes (default 50 MB, configurável 1-250 MB); 422 arquivo vazio; 422 estrutura interna do .tar.gz não bate com o esperado do AutoSCP.
Regras de negócio: SHA256 calculado em streaming com fsync; idempotência por (serpro_channel_id, sha256); arquivo armazenado cifrado em disco; se provisioning_status != "ready", seta para "package_received".
POST/api/admin/serpro-channels/{channel_id}/acquire-package
O que faz: alternativa ao upload manual — baixa o pacote AutoSCP diretamente do servidor SFG do SERPRO via SSH/SFTP, usando as credenciais salvas.
Request body: nenhum.
Response: 201 (pacote novo) ou 200 (já existente), SerproChannelPackageResponse.
Erros: 409 se username/senha SFG não configurados; 409 se a busca/validação falhar (conexão SSH, host key inesperada, estrutura inválida).
Regras de negócio: conecta em channel.server_host (default transfer.spo.serpro.gov.br):channel.server_port (default 14879). Pinning de host key: primeira conexão grava sfg_host_key_sha256; conexões seguintes validam contra esse valor (proteção MITM). Mesma idempotência por sha256 do upload manual.
GET/api/admin/serpro-channels/{channel_id}/provisioning-runs
O que faz: histórico de tentativas de provisionamento privilegiado do canal (robô que cria usuário Linux + systemd unit), mais recente (generation maior) primeiro.
Response: 200, list[SerproProvisioningRunResponse]: id, serpro_channel_id, package_id, generation (int), status (queued|running|succeeded|failed|rolling_back|rolled_back), phase (str|null), started_at, finished_at, error_code, error_message, rollback_status (not_required|pending|running|succeeded|failed), rollback_error_message, details (dict), timestamps.
Erros: 404.
GET/api/admin/serpro-channels/{channel_id}/communication
O que faz: consulta se a unit systemd do canal (loop.sh do AutoSCP) está rodando, perguntando ao daemon root da VPS via socket Unix.
Response: 200, SerproCommunicationControlResponse: available (bool), channel_id, unit (str|null), active_state (str), enabled_state, running (bool).
Erros: 503 se a chamada ao socket falhar (daemon fora do ar).
Regras de negócio: se provisioning_status != "ready" ou sem autoscp_base_path, retorna direto available=False sem chamar o host.
POST/api/admin/serpro-channels/{channel_id}/communication/start
O que faz: liga a comunicação SERPRO do canal — inicia a unit systemd dedicada (systemctl start conecta-move-serpro-<channel_id>.service) via o daemon de controle.
Response: 200, SerproCommunicationControlResponse.
Erros: 409 se channel.status != "active"; 409 se a checagem de readiness reprovar; 503 se o socket falhar.
POST/api/admin/serpro-channels/{channel_id}/communication/stop
O que faz: desliga a comunicação SERPRO do canal.
Response: 200, SerproCommunicationControlResponse.
Erros: 409 se canal nunca foi provisionado; 503 se o socket falhar.
POST/api/admin/serpro-channels/{channel_id}/provisioning-runs
O que faz: enfileira um novo provisionamento privilegiado do canal (cria usuário Linux isolado + instala AutoSCP + cria systemd unit) e acorda o robô provisionador na hora via socket, em vez de esperar o ciclo de 2 min.
Request body: nenhum.
Response: 201 (novo run) ou 200 (idempotente), SerproProvisioningRunResponse.
Erros: 409 credentialing_status != "approved"; 409 expected_client_name do canal vazio; 409 sem pacote validated/installed; 503 run enfileirado mas "kick" falhou (continua queued, pego no próximo ciclo automático).
Regras de negócio (a mais importante do arquivo):
- Hash determinístico (
desired_config_hash) de{channel_id, package_id, package_sha256, expected_client_name}. Se já existe runsucceededcom esse hash e canal jáready, devolve o existente (200), sem novo trabalho. - Se já existe run
queued|running|rolling_back, devolve esse run (nunca dois provisionamentos concorrentes do mesmo canal). - Senão cria run novo (
generation = último+1,idempotency_key = sha256(channel_id:config_hash:generation),status="queued"), setachannel.provisioning_status = "queued", disparakick_provisioner()(systemctl reset-failed+systemctl start --no-block conecta-move-serpro-provisioner.servicena VPS).
GET/api/admin/eligibility-rules/move-brasil
O que faz: retorna a versão atualmente ativa das regras de elegibilidade do Move Brasil.
Response: 200, MoveBrasilEligibilityRulesResponse: version (int), active (bool), effective_from (datetime|null), minimo_corridas (int), continuidade_meses (int), status_aprovados (list[str]), status_corrida_contabilizada (str), cpf_nao_encontrado ("sim"|"nao"), conflito_multiplas_contas (str), corridas_desde_status_atual (bool).
Erros: 404 sem regra ativa; 500 JSON de regras corrompido.
POST/api/admin/eligibility-rules/move-brasil/versions
O que faz: cria uma nova versão das regras de elegibilidade — nunca edita a existente (auditoria/imutabilidade), cria a próxima e desativa as anteriores.
Request body (MoveBrasilEligibilityRulesCreateRequest): minimo_corridas (int, 1-100000), continuidade_meses (int, 1-120), status_aprovados (list[str], 1-20 itens), status_corrida_contabilizada (str, 1-30), cpf_nao_encontrado ("sim"|"nao", default "nao").
Response: 201 nova versão, ou 200 com a versão base inalterada se nada mudou (comparação estrutural completa).
Erros: 404 nenhuma regra Move Brasil existe ainda; 422 status_aprovados vazio após normalização, ou status_corrida_contabilizada vazio; 500 regra base inválida.
Regras de negócio:
- Trava todas as linhas existentes com
SELECT ... FOR UPDATEantes de decidir a próxima versão. - Normaliza
status_aprovados/status_corrida_contabilizadapraUPPER()+ trim + dedup. - Parte da regra ativa mais recente como base, sobrescreve só os campos do payload — preserva
data_referencia,campo_data_corrida,conflito_multiplas_contas(fixo"avaliar_separadamente"),continuidade_cadastral_exigida=True,corridas_desde_status_atual=True. - Se resultado final for idêntico à base, não cria versão nova.
- Ao criar, desativa todas as versões anteriores, ativa só a nova com
effective_from=now(). - Não afeta processamentos já em andamento:
ProcessingBatchtrava a regra no momento da ingestão (FK RESTRICT).
2. API Portal da Tenant e Consulta Pública
Portal API (api/portal/*, prefixo /api/portal) — autenticação: JWT próprio do "tenant owner"
Mecanismo de auth: Bearer JWT HS256, emitido por POST /api/portal/auth/login, decodificado por require_tenant_owner. Claims: sub (owner id), tenant_id, ver (token_version), exp. Validade: settings.jwt_access_token_minutes minutos. Header sem token, token expirado, ver divergente do token_version atual do dono (revogado por troca de senha ou reativação) ou is_active=false → sempre 401. Todo endpoint (exceto login) escopa os dados automaticamente pelo tenant_id do token — não há como um tenant owner acessar dados de outra tenant.
POST/api/portal/auth/login
O que faz: autentica o proprietário da tenant (usuário/senha) e emite o token de acesso do portal.
Autenticação: nenhuma.
Request body (PortalLoginRequest): username: str (1–100, obrigatório), password: str (1–500, obrigatório). Response 200 (PortalTokenResponse): access_token, token_type: "bearer", expires_in (segundos).
Erros: 401 usuário/senha inválidos.
Regras de negócio: atualiza owner.last_login_at. Senha via argon2.
Exemplo:
// Request
{"username": "operadora53", "password": "SenhaForte123"}
// Response 200
{"access_token": "eyJhbGciOi...", "token_type": "bearer", "expires_in": 3600}
POST/api/portal/auth/change-password
O que faz: o tenant owner troca a própria senha.
Autenticação: Bearer JWT tenant owner.
Request body (PortalChangePasswordRequest): current_password: str (1–500), new_password: str (min 8, max 500). Response 200 (PortalTokenResponse): novo token já com token_version atualizado.
Erros: 400 senha atual inválida. 409 nova senha igual à atual.
Regras de negócio: incrementa owner.token_version, invalidando todos os tokens anteriores; esta chamada já devolve um token novo válido.
GET/api/portal/auth/me
O que faz: retorna os dados do proprietário logado e da sua tenant.
Autenticação: Bearer JWT tenant owner.
Response 200 (PortalMeResponse): owner_id, username, email, tenant_id, tenant_name, tenant_slug, operational_status, financial_status.
Erros: 409 tenant do token não existe mais.
GET/api/portal/overview
O que faz: painel inicial do portal — status da tenant + resumo de cobrança + última fatura.
Autenticação: Bearer JWT tenant owner.
Response 200 (PortalOverviewResponse): tenant_id, tenant_name, tenant_slug, operational_status, financial_status, billing (PortalBillingSummary: enabled, monthly_amount, currency, due_day, billing_start_date, grace_days), latest_invoice (PortalInvoiceResponse | null).
Erros: 409 tenant não encontrada.
GET/api/portal/invoices
O que faz: lista todas as faturas da tenant, mais recente primeiro.
Autenticação: Bearer JWT tenant owner.
Response 200: list[PortalInvoiceResponse] — id, reference_month, amount_due, amount_paid, due_date, status, paid_at, notes.
GET/api/portal/operations/summary
O que faz: contadores agregados das solicitações SERPRO da tenant.
Autenticação: Bearer JWT tenant owner.
Response 200 (PortalOperationalSummary): total_requests, pending_requests, processing_requests, approved_requests, rejected_requests, blocked_requests, error_requests.
GET/api/portal/requests/{request_id}
O que faz: detalhe de uma solicitação específica (CPF mascarado, resultado, motivo, regra aplicada).
Autenticação: Bearer JWT tenant owner.
Path params: request_id: UUID.
Response 200 (PortalRequestDetailResponse): id, batch_id, cpf_masked (formato *.*.*89-01), data_solicitacao, status, processed_at, result (sim/nao/null), decision_status, central_name, rides_count, created_at, updated_at, reason_code, reason_message, continuity_ok, rides_ok, rule_version.
Erros: 404 solicitação não encontrada (ou de outra tenant — mesma resposta, não vaza existência).
GET/api/portal/processing-batches
O que faz: lista paginada dos lotes SERPRO processados da tenant, com contadores por lote.
Autenticação: Bearer JWT tenant owner.
Query params: limit (1–100, default 20), offset (≥0, default 0).
Response 200 (PortalProcessingBatchListResponse): items (id, source_filename, response_filename, status, total_requests, approved_requests, rejected_requests, blocked_requests, error_requests, created_at, updated_at), total, limit, offset.
GET/api/portal/requests
O que faz: lista paginada das solicitações da tenant, ordenadas por data (mais recentes primeiro).
Autenticação: Bearer JWT tenant owner.
Query params: limit (1–100, default 25), offset (≥0, default 0).
Response 200 (PortalRequestListResponse): items (mesmos campos do detalhe, exceto reason_code/reason_message/continuity_ok/rides_ok/rule_version), total, limit, offset.
GET/api/portal/requests/export.csv
O que faz: exporta as solicitações da tenant como CSV para download (Excel-friendly, ; delimitador, BOM UTF-8).
Autenticação: Bearer JWT tenant owner.
Query params: status (opcional), result (opcional, sim/nao/blocked), date_from (date, >= 00:00 UTC), date_to (date, <= 23:59:59 UTC).
Response 200: text/csv; charset=utf-8, Content-Disposition: attachment; filename="consultas-conecta-move.csv". Colunas: data_solicitacao, cpf, status, resultado, situacao_decisao, central, corridas, processado_em (CPF sempre mascarado).
Erros: 404 nenhuma solicitação encontrada para os filtros.
API Pública de Consulta (api/public_lookup.py, prefixo /api/public) — sem autenticação
Usada pelo motorista final para consultar sua própria elegibilidade por CPF, sem login. Nunca expõe dados de tenant/operação além do nome/slug do aplicativo e um status simplificado — não expõe CPF completo, contagem de corridas, conta Machine usada, motivo da decisão nem qualquer dado interno.
Proteção contra abuso: rate limit em memória por IP, 10 requisições por 60 segundos por IP (lido de X-Forwarded-For se presente, senão IP da conexão), aplicado em POST /api/public/lookup. É em memória por processo (não distribuído entre workers) e reseta a cada restart da API.
GET/api/public/applications
O que faz: lista os aplicativos/tenants disponíveis para consulta pública.
Autenticação: nenhuma.
Response 200: list[PublicApplicationResponse] (slug, name). Só tenants com operational_status em implementation/active, programa move_brasil active+approved, e tenant_program.settings.public_lookup_enabled=true (precisa ser ligada explicitamente por tenant).
Exemplo: [{"slug": "53driver", "name": "53 Driver"}]
POST/api/public/lookup
O que faz: consulta o status de elegibilidade Move Brasil de um CPF para um aplicativo específico.
Autenticação: nenhuma. Rate limit: 10/min por IP.
Request body (PublicLookupRequest): tenant_slug: str (1–100), cpf: str (11–20 chars, aceita com/sem máscara, normalizado, precisa resultar em 11 dígitos). Response 200 (PublicLookupResponse): application (slug, name), found: bool, status: em_analise | aprovado | nao_aprovado | atencao_necessaria | nao_encontrado, status_label, updated_at.
Erros: 422 CPF inválido; 404 aplicativo indisponível; 429 muitas consultas, aguardar 1 minuto.
Regras de negócio: busca a solicitação mais recente daquele CPF (via HMAC) nos lotes do programa daquela tenant; se não achar, nao_encontrado; mapeamento status interno→público sempre simplifica pros 5 rótulos acima, nunca expõe sim/nao cru.
Exemplo:
// Request
{"tenant_slug": "53driver", "cpf": "123.456.789-01"}
// Response 200
{"application": {"slug": "53driver", "name": "53 Driver"}, "found": true, "status": "aprovado", "status_label": "Aprovado pelo aplicativo", "updated_at": "2026-09-15T14:22:00Z"}
Observações finais
- Este documento foi gerado por leitura direta do código-fonte (routers + schemas Pydantic + services) em 2026-09-20 — reflete o comportamento real, não a intenção original de design. Qualquer divergência futura entre este documento e o código deve ser resolvida a favor do código.
- Uma anomalia de código foi encontrada durante o levantamento (não corrigida, só reportada): em
api/admin/machine.py, o endpointPATCH /api/admin/machine/integrations/{integration_id}e o schemaMachineIntegrationUpdateRequestestão definidos duas vezes de forma idêntica. Não afeta o comportamento (FastAPI usa a última definição), é código duplicado a limpar. - Para o modelo de domínio completo (máquinas de estado de Tenant, gate de produção, ciclo SERPRO, etc.), ver
CLAUDE.mdna raiz do repositório.