Contrato OpenAPI: /openapi.json. As APIs usam JSON e datas UTC. Rotas de integração usam Authorization: Bearer <API_KEY>; páginas e gestão de conta/tenant usam cookie de sessão, com CSRF nas escritas.
Contas de pessoas usam entrada web em /login/; o console global em /admin/ exige papel de administrador. A chave de API continua exclusiva do discador legado e de integrações server-to-server; ela não cria uma conta de pessoa.
Widget React: autorize origens com PUT /api/v1/dialer/origins, gere grants de 90 segundos no backend com POST /api/v1/dialer/sessions e use o pacote @seulobato/voice-dialer descrito em sdk/react/README.md. O SDK de integração TypeScript está em sdk/typescript/README.md (@seulobato/voice-sdk). Os dois pacotes têm configuração local de build/empacotamento, mas ainda não foram publicados no npm nem validados com um CRM real. Os grants são de uso único e limitados ao tenant, à origem HTTPS autorizada e a uma sessão conectada; nunca envie uma API key ao browser.
GET /healthz — liveness público.GET /readyz — readiness com bearer de operação.GET, POST /api/v1/sessions e GET /api/v1/sessions/{id} — sessões WhatsApp; a leitura retorna somente ID e estado.GET /api/v1/sessions/{id}/qr.png — QR temporário, exige escopo sessions:write e sessão do tenant autenticado; POST /api/v1/sessions/{id}/connect e POST /api/v1/sessions/{id}/disconnect — controle autenticado (disconnect requer {"confirm":true}).GET, POST /api/v1/calls e GET /api/v1/calls/{id} — chamadas.POST /api/v1/calls/{id}/answer|reject|hangup — controles; POST /api/v1/calls/{id}/mute define o mudo local do operador.POST /api/v1/calls/{id}/media-token — token WSS de uso único e curta duração.GET /ws/calls/{id}/media — mídia PCM S16LE no subprotocolo WebSocket.POST /api/v1/events-token e GET /ws/events — fluxo autenticado de eventos do tenant com cursor after retomável.GET /api/v1/recordings, GET /api/v1/recordings/{id} — listagem e metadados; GET /api/v1/recordings/{id}/download — download autenticado.POST /api/v1/recordings/{id}/links e GET /r/{id} — link HMAC curto, quando configurado.GET, POST /api/v1/webhooks, DELETE /api/v1/webhooks/{id} — configuração de destinos e eventos (requer chave de criptografia).GET /api/v1/webhooks/deliveries, POST /api/v1/webhooks/deliveries/{id}/retry — consulta e retry do outbox./healthz responde publicamente e /readyz exige bearer operacional (sem credencial retorna 404). As rotas públicas são servidas. O smoke visual público passou em desktop e celular; isso não comprova login administrativo autenticado, MFA, onboarding por e-mail, cobrança, CRM ou chamada VoIP após o release. SMTP, Mercado Pago e planos comerciais não estão configurados. Consulte ACCEPTANCE-REPORT.md.GET / — landing pública HTML; GET /api/v1/public/plans — catálogo ativo e público de preço fixo mensal/anual. Planos internos, usados em atribuições manuais, não são listados nem podem iniciar checkout.GET /login/ — entrada de contas pessoais; GET /admin/ — console global de plataforma.GET /app/ — console do tenant com sessão autenticada. Sem sessão redireciona a /login/; conta sem tenant recebe 403.GET /invite/ — página pública para aceitar convite. O token pode chegar no fragmento URL, mas a página o remove do endereço e envia apenas no corpo JSON da API./login/ oferece recuperação e confirmação de e-mail. Tokens são de uso único e o banco guarda somente hashes: 30 minutos para redefinição de senha e 24 horas para confirmação de endereço. A página remove #reset_token=… ou #verify_token=… do endereço antes de enviar o token no corpo JSON. As rotas POST /auth/email/verify/request e POST /auth/email/verify usam resposta neutra, rate limit e proteção de origem; a confirmação revoga sessões existentes para exigir novo login. O login não cria sessão para conta com endereço não confirmado. A migration preserva timestamps/sessões legadas; convite manual não confirma e-mail. O bootstrap único cmd/admin-bootstrap marca como verificado o administrador inicial provisionado pelo operador e exige troca da senha inicial; contas comuns continuam exigindo confirmação por e-mail.
O sender SMTP é opcional e vendor-neutral. Configure VOICE_SMTP_HOST, VOICE_SMTP_FROM e, se necessário, VOICE_SMTP_PORT, VOICE_SMTP_TLS_MODE=starttls|implicit, VOICE_SMTP_USERNAME, VOICE_SMTP_PASSWORD, VOICE_SMTP_FROM_NAME e VOICE_SMTP_TIMEOUT. Para recuperação e confirmação, configure também VOICE_EMAIL_ENCRYPTION_KEY com base64 de 32 bytes aleatórios; a chave estável cifra destinatário e URL nas outboxes. O worker aplica até cinco tentativas e limpa o payload quando a mensagem é enviada, termina em falha ou expira. STARTTLS ou TLS implícito com validação de certificado é obrigatório. Sem SMTP, os endpoints de entrega respondem indisponível e não emitem token. Credenciais e chaves não devem ser registradas em logs nem versionadas.
Estas rotas usam a sessão web da própria pessoa. Escritas exigem origem permitida e proteção CSRF. As organizações exibidas vêm das memberships ativas e de tenants em estado active ou trialing; o servidor revalida a membership e o tenant a cada requisição.
GET /auth/sessions — lista sessões persistentes no contexto de tenant atual: id,user_agent?,created_at,last_used_at,expires_at,current.DELETE /auth/sessions/{id} — revoga uma sessão da própria conta e tenant. Se for a sessão atual, remove o cookie.GET /auth/organizations — lista id,name,role,current para as organizações acessíveis. Administrador global recebe lista vazia.POST /auth/organization — recebe {"tenant_id":"..."}, valida membership e tenant ativo ou em trial, salva a seleção na sessão persistente e grava auditoria. A seleção sobrevive a restart; não aceita organização fora da membership. Administrador global não seleciona tenant por esta rota.As rotas abaixo exigem cookie de conta com papel tenant_owner ou tenant_admin. O tenant é obtido da sessão autenticada; informar outro tenant_id não muda a autoridade. Escritas exigem CSRF. O último proprietário ativo não pode ser rebaixado nem desativado.
GET /api/v1/tenant/members — retorna {"data":[{id,user_id,email,display_name,role,status,created_at,updated_at}]}.PATCH /api/v1/tenant/members/{id} — recebe {"role":"tenant_admin|supervisor|operator"}, {"status":"active|inactive"} ou ambos; retorna {id,user_id,role,status}. DELETE na mesma rota desativa a membership e responde 204.GET /api/v1/tenant/invitations — lista convites e estado pending|accepted|revoked|expired.POST /api/v1/tenant/invitations — recebe {email,display_name?,role,expires_in_hours?}; papel permitido é tenant_admin, supervisor ou operator. Validade padrão 72 horas, máximo 168.email_delivery:"sent" sem duplicar o token; isso não confirma chegada à caixa postal. Sem configuração ou se o SMTP rejeitar, retorna not_configured ou failed e mostra invitation_token uma vez para compartilhamento manual. O banco guarda apenas o hash do token.POST /api/v1/tenant/invitations/accept recebe {"token":"...","display_name":"...","password":"..."}. O token é obrigatório no corpo JSON e nunca deve ir em query string nem path. Para conta existente, entre com o mesmo e-mail convidado; para conta nova, informe nome e senha de 8 a 72 bytes se o convite não trouxer o nome. O token é de uso único.DELETE /api/v1/tenant/invitations/{id} cancela convite. Criação e aceite exigem assinatura vigente e assento disponível: sem assinatura respondem 402; ao atingir o limite de usuários/assentos respondem 429.GET /api/v1/tenant/audit exige sessão web ativa de tenant_owner ou tenant_admin. O backend deriva o tenant do principal autenticado; não há parâmetro para escolher organização. A resposta lista eventos recentes das origens tenant, billing e account, sem metadata_json. O parâmetro opcional limit aceita de 1 a 200 e usa 50 por padrão. A resposta tem o formato {data:[{id,source,created_at,actor_id,action,resource_type,resource_id?,request_id?}],limit}; papel sem permissão responde 403 e falha de leitura responde 503.
A página /app/#audit oferece a visão Auditoria com estados de carregamento, lista vazia e erro/retentativa. Ela mostra data, origem, ator, ação, recurso e request ID. A leitura só retorna eventos da organização atual e o painel não apresenta metadados internos.
GET /api/v1/tenant/notifications lista notificações do usuário autenticado na organização selecionada e inclui unread_count; GET /api/v1/tenant/notifications/unread-count retorna somente a contagem; POST /api/v1/tenant/notifications/{id}/read marca como lida e é idempotente. Todas exigem sessão de pessoa, membership ativa e tenant ativo ou em avaliação. A escrita também exige CSRF. A central em /app/#notifications atende todos os papéis ativos e mantém cada leitura por usuário.
A migration 0022 cria produtores idempotentes para chamadas encerradas/falhas, gravações prontas, transições reais do WhatsApp para desconectado e convites aceitos. Títulos e textos são constantes genéricas; não incluem número, segredo, token, payload, URL de mídia ou erro externo. Ainda não há produtor in-app para pagamento/fatura, falha de webhook, limite de uso, convite apenas enviado/recebido, verificação de e-mail, MFA/segurança ou WhatsApp conectado. Esses eventos precisam de contratos e origens persistidas antes de serem exibidos.
POST /api/v1/tenant/data-exports requer sessão web de tenant_owner ou tenant_admin e o cabeçalho Idempotency-Key (8–128 caracteres). O tenant vem da sessão; o hash da chave é usado para devolver o mesmo job em reenvios. Há no máximo uma exportação em andamento por tenant. Consulte GET /api/v1/tenant/data-exports/{id} até o estado ready e baixe em GET /api/v1/tenant/data-exports/{id}/download. Cada etapa revalida o papel e o tenant. O pacote ZIP contém export.json e, se o diretório local estiver disponível, gravações WAV ready/partial que passem validação de caminho, tamanho, hash e formato. Se um arquivo local esperado estiver ausente ou inválido, o job falha; quando o root inteiro não está disponível, o JSON explica que o áudio foi omitido. Não exporta senhas, hashes de credenciais, tokens, URLs/segredos de webhook, payloads, caminhos locais ou metadata detalhada de auditoria.
O arquivo é gravado em diretório privado local, servido diretamente apenas pela rota autenticada, sem link público/assinado, e removido após 24 horas. O worker remove arquivos expirados e órfãos e mantém os registros expirados/falhos por até 90 dias. O limite padrão é 2 GiB de conteúdo descompactado e pode ser ajustado com VOICE_TENANT_EXPORT_MAX_BYTES entre 20 MiB e 10 GiB. VOICE_TENANT_EXPORT_DIR deve estar fora do armazenamento de sessões e gravações; o padrão é /var/lib/voz-seulobato/exports. MariaDB é lido em uma transação e os WAVs são copiados durante o job; banco e gravações não formam um snapshot atômico.
Gerenciamento requer sessão web de proprietário ou administrador do tenant e proteção CSRF. O segredo nunca aparece na listagem e é mostrado uma única vez na criação/rotação; salve-o ao recebê-lo.
GET /api/v1/api-keys — lista id,label,scopes,created_at,expires_at,revoked_at,last_used_at,status, sem segredo.POST /api/v1/api-keys — recebe {label,scopes,expires_at?}; escopos aceitos: sessions:read, sessions:write, calls:read, calls:write, recordings:read, webhooks:read e webhooks:write. Resposta contém api_key e shown_once:true.POST /api/v1/api-keys/{id}/rotate — corpo pode ser omitido ou fornecer novos label, scopes e expires_at. Revoga a chave anterior e cria a substituta transacionalmente; a resposta mostra o novo segredo uma única vez.DELETE /api/v1/api-keys/{id} — revoga a chave e responde 204.GET /api/v1/billing/plans, /subscription, /entitlements, /usage, /summary e /invoices. Todas exigem sessão de pessoa com membership ativa tenant_owner no tenant active ou trialing. API keys, tenant_admin, supervisor e operador são negados, inclusive com scopes legados. O tenant vem da sessão, nunca de tenant_id enviado pelo cliente.platform_admin): GET,POST /api/v1/platform/plans; PATCH /api/v1/platform/plans/{id}; POST /api/v1/platform/plans/{id}/versions; GET,POST /api/v1/platform/subscriptions; PATCH /api/v1/platform/subscriptions/{id}.id,slug,name,description,active,created_at,updated_at,versions. Versões têm ciclo month|year, preço fixo em unidade monetária mínima, código ISO 4217 e limites para users,sessions,concurrent_calls,call_minutes,storage_bytes,retention_days,webhooks,api_requests. Limite null significa ilimitado e 0 significa nenhum. Métricas de uso orientam quotas e relatórios, nunca geram cobrança por unidade ou excedente.VOICE_PAYMENT_PROVIDER=mercadopago, VOICE_MERCADOPAGO_MODE=sandbox|production, access token, segredo do webhook e URL pública HTTPS. Sem configuração completa, checkout e reconciliação retornam indisponível. production pode iniciar cobranças reais; confirme manualmente no painel que token e segredo correspondem ao modo escolhido, pois a aplicação não detecta o ambiente pelo token. Nenhuma credencial ou teste de pagamento real está comprovado por esta página.POST /api/v1/billing/checkout recebe {"plan_version_id":"..."} e Idempotency-Key UUID; cria checkout hospedado de preço fixo em BRL para um plano/ciclo ativo. GET /api/v1/billing/portal retorna estado local da assinatura, checkout pendente e até 50 faturas; não oferece portal hospedado para editar cartão. POST /api/v1/billing/subscription/cancel solicita cancelamento imediato e só encerra o estado local após confirmação do provedor. Os três endpoints exigem sessão web de tenant_owner ativa e CSRF nas escritas.GET /api/v1/billing/invoices retorna metadados reconciliados, sem URL/PDF. POST /api/v1/payments/mercadopago/webhook é público, mas verifica x-signature, x-request-id, timestamp e live_mode antes de buscar a assinatura/pagamento diretamente no provedor e reconciliar os dados locais. O webhook deve receber live_mode=false em sandbox e true em production, conforme o modo configurado.past_due, sem estender o período; não há grace period automático. Estado de assinatura past_due bloqueia entitlements. O estado de ciclo de vida do tenant também tem past_due, que bloqueia acesso e revoga sessões; a reconciliação de uma assinatura não muda automaticamente o estado administrativo do tenant.api_requests é reservado atomicamente antes do handler quando há assinatura ativa; chamadas autenticadas que chegam ao roteamento contam mesmo em resposta 4xx/5xx. Falhas de autenticação, bloqueios de quota e leituras /api/v1/billing/* são excluídos. storage_bytes é aplicado na admissão e finalização de gravações. retention_days usa a assinatura atual: null mantém gravações sem prazo, 0 bloqueia gravações novas e purga gravações finalizadas, e N purga faixas finalizadas após N dias desde o maior recordings.created_at; calls.ended_at apenas comprova que a chamada é terminal. O worker mantém bytes contados enquanto linhas estão purging, remove apenas arquivos e metadados de gravações, registra auditoria idempotente e preserva chamadas/eventos. Falhas deixam a purge pendente para retry; sem assinatura vigente, nenhuma purge nova começa.As rotas de ciclo de vida estão implantadas na produção para platform_admin, com cookie de conta e CSRF. A implementação registra alterações e transições rejeitadas em auditoria append-only. As migrations correspondentes foram aplicadas; o aceite do fluxo administrativo autenticado e da ativação MFA ainda está pendente.
PATCH /api/v1/platform/tenants/{id}/status aceita pending|trialing|active|past_due|suspended|cancelled|deleted; PATCH /api/v1/platform/users/{id}/status aceita somente active|suspended. Ambas retornam {id,status,changed,revoked_sessions,request_id}.pending→trialing|active|suspended|cancelled|deleted; trialing→active|past_due|suspended|cancelled; active→past_due|suspended|cancelled; past_due→active|suspended|cancelled; suspended→active|past_due|cancelled|deleted; cancelled→active|deleted. deleted é terminal; transição inválida retorna 409 TENANT_LIFECYCLE_TRANSITION_INVALID. Reenviar o estado atual é um no-op com changed:false.active e trialing operam o produto; demais estados bloqueiam acesso e revogam sessões, tokens e grants do widget. A suspensão do usuário revoga todas as sessões da conta; reativar não restaura sessões. Suspender o último platform_admin ativo responde 409 LAST_PLATFORM_ADMIN.deleted é soft delete, não remoção física de tenant ou de seus dados. A exclusão definitiva/retensão precisa de fluxo próprio.No checkout local, MFA é opcional para todos os papéis. Quando ativado, cada login exige TOTP ou código de recuperação; o usuário pode desativar MFA confirmando a senha e um código válido. Sem fator ativo, login usa e-mail e senha. O release de produção 92daf32 ainda exigia MFA para platform_admin; a mudança só terá efeito no domínio depois de uma publicação validada.
GET /api/v1/platform/audit é exclusiva de platform_admin e agrega eventos das origens platform, billing, tenant e account, do mais recente para o mais antigo. Parâmetros opcionais: limit (padrão 50, entre 1 e 200) e tenant_id (até 64 caracteres). A resposta contém {data:[{id,source,created_at,actor_id,tenant_id?,action,resource_type,resource_id?,request_id?,metadata_json}],limit}; filtros inválidos respondem 400 e falha de leitura responde 503.
O painel e a rota de auditoria estão implantados em /admin/, com estados de carregamento, vazio e erro, além de filtros por organização/limite. Mostra data, origem, ator, organização, ação, recurso e request ID; omite metadata_json. A tela pública foi revisada sem autenticação; a listagem administrativa e a auditoria exigem sessão e ainda aguardam aceite browser autenticado.
curl -H 'Authorization: Bearer SUA_API_KEY' https://voz.seulobato.com/api/v1/sessions