Autenticação e segurança
Autenticação
Chamadas REST usam Bearer token no header. A chave secreta define o ambiente: sk_test roda em sandbox, sk_live em produção. Nunca exponha a secret no front: ela é só server-side.
# A chave secreta NUNCA sai do seu servidor. Aqui ela vem do ambiente, nunca do arquivo:
# export UNIFOKAL_SECRET_KEY="sk_live_..." (use sk_test_... para sandbox)
#
# O ambiente (sandbox ou producao) e definido pela CHAVE, nao por um campo do corpo.
curl -sS -i -X POST https://api.unifokal.com/v1/verification-sessions \
-H "Authorization: Bearer $UNIFOKAL_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"flow_id": "flow_01J8ZQ2R6K7T8V9W0X1Y2Z3A4B",
"reference_id": "usr_8842"
}'
# NAO existe campo "email" nem campo "phone": o contato do titular NUNCA entra aqui. Quem pergunta
# o e-mail e o telefone a pessoa, e dispara o codigo em seguida, e o widget. Mandar um dos dois
# devolve 422 email_not_accepted / phone_not_accepted (recusamos com nome, nunca ignoramos em
# silencio). Um flow com email_otp cria sessao com este MESMO corpo.
# O QUE ESPERAR DE VOLTA (201). Guarde o "id": e ele, e so ele, que vai para o navegador.
# A resposta vem comentada para o bloco inteiro poder ser colado no terminal sem erro de sintaxe.
#
# HTTP/1.1 201 Created
# {
# "id": "vs_01J8ZQ4S8M2N3P4Q5R6S7T8U9V",
# "flow_id": "flow_01J8ZQ2R6K7T8V9W0X1Y2Z3A4B",
# "environment": "production",
# "reference_id": "usr_8842",
# "status": "requires_input",
# "expires_at": "2026-08-25T18:15:00.000Z",
# "created_at": "2026-08-25T18:00:00.000Z",
# "modules": ["cpf_ocr", "face", "liveness"],
# "livemode": true,
# "expires_in": 900
# }
# "expires_in" e o prazo em SEGUNDOS: 900 = 15 minutos. Depois disso o widget nao sobe
# mais com esse id, e voce cria outra sessao.
# "modules" sao os modulos do flow, na ordem. Voce nao declara modulos no widget.
#
# A criacao NAO cobra nada: nenhum centavo sai da sua conta aqui. A cobranca acontece quando a
# verificacao TERMINA, e o saldo atualizado chega no seu webhook do desfecho.
#
# IDEMPOTENCIA: nao existe header. O "reference_id" (obrigatorio) E a chave, do nosso lado. Repetir
# a chamada com o MESMO reference_id, enquanto a sessao estiver viva, devolve a MESMA sessao, com o
# header "Idempotent-Replay: true": um retry do seu backend nao cria (nem cobra) uma segunda. Quando
# a sessao expira (900 s), o mesmo reference_id cria uma sessao NOVA, que e o que o
# titular precisa para tentar de novo.Todo erro tem a mesma forma: { "error": "<código>", "message": "<texto>" }. Trate pelo error, que é contrato estável dentro de /v1. O message é para humano e pode mudar sem aviso. A lista completa dos que você encontra no primeiro dia está logo abaixo.
# O envelope de erro e SEMPRE o mesmo, em toda a API:
# { "error": "<codigo estavel>", "message": "<texto legivel>" }
# Trate pelo "error". O "message" e para humano e pode mudar; o codigo nao muda dentro de /v1.
# Reproduza um erro AGORA, com a sua chave de sandbox, e confira o formato com os proprios olhos:
curl -sS -i -X POST https://api.unifokal.com/v1/verification-sessions \
-H "Authorization: Bearer $UNIFOKAL_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"flow_id": "flow_esse_nao_existe"}'
# O QUE ESPERAR DE VOLTA: 404 com o envelope acima. Os outros que voce vai encontrar estao listados
# abaixo, comentados, para este bloco inteiro poder ser colado no terminal sem erro de sintaxe.
# 401 -> chave ausente, mal embalada, inexistente ou REVOGADA. A mensagem diz qual dos casos:
# "Missing API key" (nao mandou nada), "API key not recognized" (mandou a chave crua no
# Authorization, sem o "Bearer "), "API key not found" ou "This API key was revoked".
# { "error": "invalid_api_key", "message": "API key not found: check the key and the environment it was created in." }
# 400 -> campo faltando ou malformado. E o erro mais comum do primeiro dia. A mensagem lista os
# campos, os valores aceitos e os limites; ela NUNCA repete o valor que voce mandou.
# { "error": "validation_error", "message": "\"flow_id\" any.required" }
# Caso especial que vale conhecer: POST sem o header Content-Type: application/json nao chega
# a ter corpo nenhum, e a resposta diz isso na primeira frase.
# 403 -> a organizacao esta suspensa: TODA chamada sk_ para de funcionar de uma vez.
# { "error": "organization_suspended", "message": "Organization suspended: every API call is blocked until it is reactivated. Sign in to the dashboard to see the reason, or contact support." }
# 403 -> o prefixo da chave e o registro dela discordam de ambiente (chave guardada na variavel
# errada do deploy e o caso classico).
# { "error": "test_key_used_in_production", "message": "API key prefix says production but the key belongs to sandbox: use the key of the environment you want to call." }
# 403 -> a conta ainda nao confirmou o e-mail. E o tropeco mais comum do primeiro dia:
# a chave e valida, o flow existe, e mesmo assim nenhuma sessao e criada.
# { "error": "email_not_verified", "message": "Confirm your email address before using this resource" }
# 403 -> voce mandou a credencial do PAINEL (dsk_) nesta rota. Aqui so entra sk_.
# { "error": "credential_type_not_allowed", "message": "This route does not accept dashboard credentials" }
# 404 -> flow_id inexistente NESTE ambiente. Chave de teste nao enxerga flow de producao,
# e vice-versa: o mesmo id nao existe dos dois lados. E o erro do curl acima.
# { "error": "flow_not_found", "message": "Flow not found in sandbox: check the flow id, and remember the environment is the API key you used (a flow lives in one environment only, and the same id does not exist on the other side)." }
# 422 -> o flow existe, mas nao esta live
# { "error": "flow_not_live", "message": "Flow is not live" }
# 422 -> voce mandou "email" (ou "phone") no corpo. Contato do titular nao entra na criacao:
# quem pergunta a pessoa e dispara o codigo e o widget. Recusado na entrada, custo zero.
# { "error": "email_not_accepted", "message": "Email is always collected from the end user in the widget: remove email from the request." }
# { "error": "phone_not_accepted", "message": "Phone is always collected from the end user in the widget: remove phone from the request." }
# 422 -> voce mandou "phone". Ele nao entra aqui: o telefone e sempre digitado pelo titular
# no widget. Recusamos com nome em vez de ignorar, senao voce acharia que travou o
# destino do SMS quando nao travou.
# { "error": "phone_not_accepted", "message": "Phone is always collected from the end user in the widget: remove phone from the request." }
# 402 -> o saldo nao cobre o maximo que a verificacao pode custar. A mensagem diz quanto falta.
# Recarregue, ou ligue a recarga automatica no painel.
# { "error": "insufficient_credit", "message": "Insufficient credit balance to start a production verification: balance is 0 cents and 68 more cents are needed to cover the maximum this verification can cost. Add credit in the dashboard (Cobranca), or use a sandbox key (sk_test_), which never charges." }
# 429 -> teto MENSAL de sandbox, ou teto de chamadas por minuto da conta.
# Os dois sao 429 com codigos diferentes: nao trate 429 como um caso so.
# { "error": "sandbox_limit_reached", "message": "Sandbox monthly limit reached." }
# { "error": "rate_limited", "message": "Rate limit exceeded." }
# 409 / 422 -> idempotencia
# { "error": "idempotency_conflict", "message": "A request with this reference_id is already in progress." }
# { "error": "idempotency_key_reuse", "message": "reference_id reused with a different request body." }
# 422 -> politica por sessao para um modulo que o flow NAO tem. Recusamos com nome em vez de
# ignorar: politica aceita-e-ignorada faria voce acreditar que desligou algo que segue ligado.
# { "error": "policy_module_not_in_flow", "message": "Flow has no pep_sancoes module; remove policy.allow_pep from the request." }
# 400 -> chave DESCONHECIDA dentro de "policy" ou de "monitoring". Os dois blocos sao whitelist
# fechada: recusamos com nome em vez de descartar o campo em silencio, porque um 201 com a
# chave engolida faria voce acreditar que a politica valeu. O nome da chave recusada vem na
# "message". Valem os mesmos blocos tambem no link hospedado.
# { "error": "unknown_policy_key", "message": "\"policy.allowBettingBan\" object.unknown (unknown_policy_key)" }
# { "error": "unknown_monitoring_key", "message": "\"monitoring.cancel\" object.unknown (unknown_monitoring_key)" }
# 422 -> o flow informado so recebe alertas do monitoramento transacional e nao aceita sessao de
# verificacao. Mande os eventos com o bloco "transaction", ou crie a sessao no seu flow
# de verificacao. Vale igual na emissao do link hospedado.
# { "error": "session_not_supported", "message": "the flow's module transacao only receives alerts from transaction monitoring and does not accept a verification session." }
# 422 -> "policy.ubo_max_paid_nodes" acima do teto do proprio flow. O teto da sessao so APERTA o do
# flow (gasto so sobe por decisao registrada no flow, nunca por um campo de request).
# { "error": "policy_ubo_cap_above_flow", "message": "policy.ubo_max_paid_nodes (8) exceeds the flow limit (3); the session policy can only lower it." }
# Nenhum destes cria sessao e nenhum e cobrado. Repetir a chamada so resolve 429 (espere) e 402
# (recarregue). Nos outros, a correcao e de configuracao: repetir devolve o mesmo erro.flow_id de produção não existe para uma chave de teste, e o erro que volta é 404 flow_not_found, não um 403 de permissão. Se um flow que você acabou de criar "não existe", confira o ambiente da chave antes de qualquer outra coisa (veja Sandbox).Segurança do fluxo
O front recebe do seu backend apenas o sessionId (vs_), escopado a UMA única sessão (uso curto, com TTL). Ele é a credencial do widget: nenhum segredo vai ao navegador. Não é uma chave global: só habilita a captura daquela verificação, então expor por engano não compromete outras sessões nem a sua conta. A sk_ (secret), usada no backend para criar a sessão, é a única credencial sensível e nunca sai do servidor. A captura e o processamento acontecem no UNIFOKAL; o resultado oficial só trafega no webhook assinado para o seu backend.
Encontrou uma vulnerabilidade? A política de divulgação responsável diz como relatar, o escopo e o que esperar, e o arquivo /.well-known/security.txt traz os mesmos canais em formato legível por máquina. Os documentos de segurança, privacidade e contrato ficam reunidos na central de confiança.
Acesso da equipe por SSO (SAML 2.0)
A sua equipe pode entrar no painel pelo provedor de identidade da própria empresa, em vez de e-mail e senha por pessoa. Falamos SAML 2.0 no perfil Web Browser SSO, que é o caminho documentado por Okta, Microsoft Entra ID e Google Workspace para aplicações de terceiros. Está incluso: não é add-on, não tem mensalidade e não consome crédito.
Quem liga é o dono da conta, em Segurança da conta no painel. A tela mostra os dois valores que você cola no seu provedor (o Entity ID e a URL de retorno, que o Entra chama de Reply URL) e recebe de volta o identificador, a URL de login e o certificado de assinatura que o seu provedor gera.
Como fica o segundo fator: quem entra pelo seu provedor não precisa também do nosso código de verificação em duas etapas, porque o segundo fator é o que a sua empresa já exige no próprio provedor. A exigência de duas etapas aqui continua valendo para quem entra por e-mail e senha.
Ao exigir SSO, a equipe deixa de entrar por senha. O dono da conta continua conseguindo entrar por senha de propósito: é a saída de emergência para o dia em que o certificado do provedor expirar ou a configuração sair errada, situação em que ninguém mais consegue entrar. Pelo mesmo motivo, você pode cadastrar mais de um certificado ao mesmo tempo e trocar sem janela de queda: cadastre o novo antes de virar a chave no seu provedor.
Provisionamento automático (SCIM 2.0). Além de entrar pelo seu provedor, a sua equipe pode ser gerida por ele: Okta e Microsoft Entra ID criam, atualizam e desativam contas daqui pelo protocolo SCIM 2.0 (RFC 7643 e 7644). O ganho de segurança é o desligamento: quem for desativado no seu diretório perde o acesso aqui na hora, com as sessões abertas derrubadas no mesmo instante, sem depender de alguém lembrar de mexer no painel.
Para ligar: na tela de Segurança da conta, com a conexão de SSO ativa, o dono da conta gera o token de provisionamento e copia o endereço do SCIM. No Okta, os dois entram em Provisioning como SCIM connector base URL e API token (autenticação HTTP Header); no Entra, como Tenant URL e Secret Token do provisionamento do aplicativo. O token é exibido uma única vez, como as chaves de API: perdeu, rotacione. Rotacionar invalida o anterior no mesmo instante, e revogar interrompe a sincronização sem mexer em quem já foi provisionado.
O recorte, dito com todas as letras: o identificador da pessoa (userName) é o e-mail e não se renomeia por SCIM; quem nasce pelo provisionamento entra com o papel padrão da conexão, e o papel de quem já existe nunca é reescrito; grupos não são provisionados (papel por grupo continua sendo do mapeamento do SAML); o titular da conta não é gerenciável por SCIM, de propósito, porque é ele quem mantém a saída de emergência; remover alguém pelo provedor desativa a conta aqui, sem apagar o histórico de quem fez o quê; e uma remoção feita por uma pessoa no painel não é desfeita pelo provedor.
O que ainda não existe, para você não planejar em cima do que não temos: logout único (SLO) e login iniciado pelo provedor (o acesso começa sempre pela nossa tela de entrada).
Pronto para integrar? A chave de sandbox sai no painel, logo depois do cadastro. Criar conta grátis