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:

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íliaDomínio de produçãoApp clienteAutenticação
Admin (/api/admin/*)admin.conectamove.techapp/admin-webJWT Bearer, usuário super_admin
Portal da Tenant (/api/portal/*)portal.conectamove.techapp/tenant-webJWT Bearer, "tenant owner" (dono da operadora)
Consulta Pública (/api/public/*)consulta.conectamove.techapp/public-webnenhuma (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.tech hospeda apenas esta documentação.

Convenções gerais



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):

campotipoobrigatóriovalidação
usernamestringsim1–100 caracteres
passwordstringsim1–512 caracteres

Response200 OK (AdminTokenResponse):

campotipo
access_tokenstring (JWT)
token_typestring, sempre "bearer"
expires_inint (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.

Response200 OK (AdminMeResponse):

campotipo
idUUID
usernamestring
emailstring \null
rolestring
is_activebool
last_login_atdatetime \null
created_atdatetime

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:

nometipoobrigatóriodefaultdescrição
qstringnãobusca por name ou slug (ILIKE, 1–150 chars)
operational_statusenum (implementation\active\suspended\blocked\closed)nãofiltro exato
financial_statusenum (not_applicable\current\overdue)nãofiltro exato
limitintnão501–100
offsetintnão0≥0

Response200 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):

campotipoobrigatóriovalidação
namestringsim1–150 chars, trim, não pode ficar vazio após trim
slugstringsim1–100 chars, regex ^[a-z0-9]+(?:-[a-z0-9]+)*$, normalizado para minúsculo/trim antes de validar
operational_statusenumnãodefault "implementation"
financial_statusenumnãodefault "not_applicable"

Response201 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).

Response200 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).

Response200 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):

campotipoobrigatóriovalidação
enabledboolsim
required_for_operationboolsimse true, enabled também precisa ser true (validação a nível de schema, espelha o CHECK constraint do banco)

Response200 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).

Response200 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):

campotipovalidação
namestring \null1–150 chars, trim
slugstring \null1–100 chars, regex ^[a-z0-9]+(?:-[a-z0-9]+)*$, normalizado
operational_statusenum \nullimplementation\active\suspended\blocked\closed
financial_statusenum \nullpresente no schema, mas nunca deveria ser setado manualmente por regra de negócio — ver nota abaixo

Response200 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).

Response200 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.

Response200 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):

campotipoobrigatóriovalidação
reason_codeenum manual_admin\security\technical\contract\othersim
notesstring \nullnãoaté 4000 chars

Response201 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) }. Response200 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.

Response200 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):

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 timezone422 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_versionderruba 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):

Response: 201, SerproChannelResponsestatus="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:

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):

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:


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