# API REST

<https://unifokal.com/docs/api-rest>

## API REST (4 endpoints públicos)

A superfície server-to-server é **enxuta de propósito**: com a `sk_` você **cria a sessão** (ou **emite um link hospedado**, se não for montar o widget) e cuida da **confiabilidade do webhook**. Todo o resto (flows, verificações, blocklist, destinos de webhook, billing) se gerencia pela [plataforma](https://unifokal.com/login).

**POST** `/v1/verification-sessions`

```
POST /v1/verification-sessions
Authorization: Bearer sk_live_…        // a chave define o ambiente (sk_test = sandbox)
Content-Type: application/json
// SEM header de idempotência: o "reference_id" do corpo é a chave, do nosso lado.

// CORPO. Contato do titular NÃO entra aqui: o widget pergunta e dispara o código.
{
  "flow_id": "flow_01J8…",             // obrigatório
  "reference_id": "user_123",          // OBRIGATÓRIO: o id do usuário NO SEU sistema. Volta igual no
                                       // webhook, e é a CHAVE DE IDEMPOTÊNCIA da criação, comparada
                                       // BYTE A BYTE: "User_42" e "user_42" criam DUAS sessões.
                                       // Id OPACO e estável por pessoa. No HISTÓRICO DE TRANSAÇÕES e
                                       // na LISTA DE BLOQUEIO, maiúscula e minúscula são o MESMO
                                       // titular (User_42 = user_42); mande sempre a mesma grafia
  "policy": { "allow_pep": false },    // opcional: política por sessão; chave desconhecida é 400 unknown_policy_key
  "return_url": "meuapp://kyc/pronto", // opcional: para onde o widget manda o titular no fim. Serve
                                       // ao aplicativo nativo, que abre a verificação no navegador
                                       // do sistema. https ou o esquema do seu app; a volta NÃO
                                       // carrega resultado (quem conta o desfecho é o webhook)
  "origin_tag": "google-ads:black-friday", // opcional: o rótulo SEU da origem da jornada (campanha,
                                       // canal, tela). Volta exato no 201 e no webhook; até 64
                                       // caracteres, sem espaço nem arroba. Nunca dado pessoal

  // OPCIONAIS DE TRANSAÇÃO. Só em flow que contenha um módulo consumidor de transação;
  // sem consumidor, 422 transaction_not_supported. Os dois são EXCLUSIVOS entre si:
  // mandar os dois juntos é 422 transaction_conflict. Detalhes logo abaixo do corpo.
  "transaction": {                     // opcional: UM evento, a forma do Gate Transacional
    "type": "payment",                 // OBRIGATÓRIO: deposit | withdraw | transfer | payment | bet |
                                       // settlement | reversal | bet_profit | bet_loss
    "external_id": "pay_9f2c",         // OBRIGATÓRIO: é a idempotência durável do evento
    "amount_cents": 250000,
    "currency": "BRL",
    "occurred_at": "2026-09-11T14:02:00.000Z"
  },
  "transactions": [ /* … */ ],         // opcional: o LOTE. Proibido em flow com o gate síncrono
                                       // (422 batch_not_supported_for_gate): lá use o singular

  // OPCIONAL DE MONITORAMENTO CONTÍNUO: override POR SESSÃO do monitoring_enabled do flow.
  "monitoring": { "enabled": true }    // ausente ou null herda o flow; chave desconhecida aqui é 400 unknown_monitoring_key
}
// Não existe campo "phone": o telefone é sempre digitado pelo titular no widget.
// Mandar "phone" aqui devolve 422 phone_not_accepted (recusamos com nome, nunca ignoramos).

→ 201  // a resposta INTEIRA, não só três campos
{
  "id": "vs_01J8…",                    // a credencial do widget. É o único valor que vai ao navegador
  "flow_id": "flow_01J8…",
  "environment": "production",
  "reference_id": "user_123",
  "status": "requires_input",
  "expires_at": "2026-08-25T18:15:00.000Z",
  "created_at": "2026-08-25T18:00:00.000Z",
  "modules": ["cpf_ocr", "face", "liveness"],   // os módulos do flow, na ordem
  "livemode": true,
  "expires_in": 900                    // SEGUNDOS de vida da sessão: 900 = 15 minutos
}
// "origin_tag" entra nesta resposta só quando você o mandou no corpo, com o valor exato.
// "decision" entra nesta resposta só quando o flow tem o módulo conta: o veredito do evento
//   (allow | step_up | deny), o risk_score de 0 a 100 em faixas de 20, os códigos dos motivos
//   (lista aberta: trate o que não conhecer como informativo), o verification_id e o step_up.
// "policy" entra nesta resposta só quando o flow tem pep_sancoes ou impedidos_apostar: a política
//   efetiva com que a sessão nasceu e a origem de cada valor (flow, session ou session_inherited).
// Criar NÃO cobra: o dinheiro sai quando a verificação TERMINA, e o saldo vem no webhook.

// ERROS (envelope { "error", "message" } em todos):
//   401 invalid_api_key · 403 email_not_verified · 403 credential_type_not_allowed
//   404 flow_not_found  · 422 flow_not_live      · 402 insufficient_credit
//   422 email_not_accepted · 422 phone_not_accepted (contato do titular não entra aqui)
//   422 transaction_not_supported (flow sem módulo consumidor de transação)
//   422 session_not_supported (flow que só recebe alertas do monitoramento transacional)
//   422 origin_tag_not_supported (origin_tag junto do bloco de transação: ali não há jornada a rotular)
//   422 transaction_conflict (transaction e transactions juntos) · 422 batch_too_large
//   422 batch_not_supported_for_gate (lote em flow com gate síncrono)
//   422 transaction_required (flow com o Gate Transacional e corpo SEM o bloco; vale também no link hospedado)
//   Os demais códigos da ingestão de transação estão na página do Gate Transacional, um por regra:
//   external_id_required · amount_too_large · currency_not_supported · event_too_old · reference_id_charset
//   pii_shaped_value · settlement_status_invalid · unknown_settles_reference · already_settled
//   pending_lifecycle_not_enabled · 429 daily_ingest_cap_reached (teto diário por organização)
//   429 sandbox_limit_reached (teto mensal) · 429 rate_limited (teto por minuto)
//   429 spend_cap_reached (orçamento diário configurado no painel; Retry-After até 00:00 UTC)
//   409 idempotency_conflict (mesmo reference_id em voo) · 422 idempotency_key_reuse (mesmo reference_id, corpo outro)
// Nenhum deles cria sessão, e nenhum é cobrado. O exemplo com cada corpo está em Autenticação.
```

! **Você nunca envia o contato do titular.** Nem `email`, nem `phone`: mandar qualquer um dos dois falha com `422 email_not_accepted` ou `422 phone_not_accepted`, e **nenhuma sessão é criada nem cobrada**. Recusamos com nome em vez de aceitar e ignorar, para você não seguir acreditando que travou o destino do código. Quem pergunta o e-mail e o telefone à pessoa, e dispara o código em seguida, é o widget. Um flow com `email_otp` cria sessão com o mesmo corpo de qualquer outro. Guardamos o endereço **cifrado**: a resposta e o webhook levam só derivados (domínio e máscara), nunca o endereço completo.

**`origin_tag` é o rótulo da origem da jornada, e quem escolhe o valor é você**. Ele responde de qual campanha, canal ou tela a pessoa veio, sem tabela paralela do seu lado: o valor volta **exato** no `201` e no `data.origin_tag` do webhook da verificação, e a sessão renovada pelo widget herda o mesmo rótulo. São de 1 a 64 caracteres entre letras, dígitos, ponto, sublinhado, dois-pontos e hífen, começando por letra ou dígito. E-mail e URL não cabem, de propósito: **nunca coloque dado pessoal aqui**. A caixa é preservada, e ferramentas de análise tratam `Google` e `google` como valores diferentes, então prefira minúsculas. Como o campo entra no corpo, a idempotência vale para ele: repetir o `reference_id` com outro `origin_tag` é `422 idempotency_key_reuse`. Sem o campo, nenhuma resposta muda. Junto do bloco de transação ele é recusado com `422 origin_tag_not_supported`, porque a ingestão não tem jornada a rotular.

**`transaction` e `transactions` são a ingestão do Gate Transacional**, e só existem para flow que contenha um **módulo consumidor de transação**. Flow sem consumidor responde `422 transaction_not_supported`, e os dois campos são **exclusivos entre si**: mandar os dois no mesmo corpo é `422 transaction_conflict`.

No singular, `transaction` é **um** evento e `external_id` é **obrigatório**: ele é a idempotência durável do evento, e é o que faz o seu retry não virar uma segunda cobrança. A resposta `201` traz a forma de ingestão, com o id `ing_` e os contadores, e num flow com **gate síncrono** ela traz também o **veredito do gate**, que é o ponto do módulo: você tem um pagamento parado esperando, e a resposta vem na mesma chamada. **Ela vem sempre**, inclusive no seu retry do mesmo `external_id`, que recebe de volta o mesmo veredito da primeira vez e não é cobrado de novo. E no **sandbox** o gate decide também, de forma determinística, para você exercitar os três ramos antes de ir para produção. Os detalhes estão na página do [Gate Transacional](https://unifokal.com/docs/modulos/transacao).

! **Num flow que contenha o Gate Transacional, o lote é proibido: `transactions` responde `422 batch_not_supported_for_gate`**. A razão é de produto e não de implementação: um gate síncrono decide **um** pagamento, e um lote não teria veredito nenhum para devolver. Nesse flow use `transaction`, no singular. Leia isto antes de montar a integração: é a diferença entre um laço que funciona e um lote que nunca passa.

Fora do flow com gate, `transactions` é o **lote**, com a mesma condição de consumidor. Ele aceita de **1** até o teto por chamada, e acima disso responde `422 batch_too_large` **com o teto no corpo do erro**, para você não precisar descobrir o número por tentativa. O dedupe é por `external_id`: reenviar o mesmo lote não duplica evento nem cobra de novo.

**`monitoring` é `{ enabled: boolean }`, o override por sessão do Monitoramento Contínuo.** Ausente ou `null`, a sessão **herda** o `monitoring_enabled` do flow. `false` significa "esta sessão não inscreve" e é **sempre aceito**, mesmo com o módulo desligado, porque é um pedido que o produto honra em qualquer estado. `true` inscreve o titular aprovado. Chave desconhecida dentro do bloco é **400** `unknown_monitoring_key`, nunca ignorada: toggle de compliance aceito e ignorado faria você acreditar que monitora enquanto ninguém monitora.

O `422 monitoring_unavailable` **deixou de acontecer em 11 de setembro de 2026**, quando a venda do módulo abriu. Ele só volta se o módulo for desligado de novo, e nesse caso vale de novo a mesma regra de sempre: `enabled: false` continua passando, e só `true` é recusado.

**GET** `/v1/webhook-events`

```
GET /v1/webhook-events?page=1&limit=20
Authorization: Bearer sk_live_…

→ 200 { "data": [ { "event_type": "completed",
                    "verification_id": "ver_…", "reference_id": "user_123",
                    "target_url": "sua.api", "status": "error",
                    "status_code": null, "attempts": 3,
                    "failed_at": "2026-07-28T21:14:09.312Z" } ],
        "page": 1, "totalPages": 1, "totalItems": 1 }
// UMA entrada por verificação+evento (idempotente): reemissões não duplicam;
// attempts acumula o total de falhas. Ciclo: até 3 tentativas ("sending") →
// 2xx marca "success"; esgotou sem 2xx (4xx/5xx ou sem resposta) marca "error"
// e entra NESTA lista. Entregue com sucesso (original OU replay) sai da lista.
// failed_at = quando a última tentativa falhou (a data do erro).
// target_url = o HOST do destino desta entrega, que é exatamente onde o replay vai tentar de
// novo. O endereço completo do seu webhook nunca volta em resposta de leitura: ele aparece só
// para quem administra os endpoints, no painel. Trocou o webhook do flow depois disso? Este
// campo continua mostrando o destino antigo, porque é dele que a entrega falhada está falando.
// limit: máximo 20 por página (pagine com page=2, 3…)
// resource_id: filtro OPCIONAL. Sem ele, a listagem responde exatamente como sempre respondeu.
//
// Nem todo evento pertence a uma verificação. A revogação de um dispositivo, por exemplo, nasce
// de um ato sobre o aparelho e não de um fluxo de verificação: nela o verification_id vem null e
// o resource_id carrega a identificação do recurso. Use esse valor para filtrar aqui e para
// resgatar no replay.
```

**POST** `/v1/webhook-events/replay`

```
POST /v1/webhook-events/replay
Authorization: Bearer sk_live_…

{ "verification_ids": ["ver_aaa…"], "resource_ids": ["pxd_xxx:1"] }
// os dois campos são opcionais e pelo menos um é obrigatório;
// o máximo de 20 vale para a SOMA dos dois

→ 200 { "results": [
          { "verification_id": "ver_aaa…", "resent": true,  "http_status": 200 },
          { "resource_id": "pxd_xxx:1",    "resent": false, "http_status": null,
            "error": "nothing_to_replay" } ],
        "resent": 1, "failed": 1 }
// cada item volta pelo eixo em que foi pedido: verification_id ou resource_id, nunca os dois.
// cada item reenvia os webhooks com erro daquela verificação: o corpo EXATO salvo,
// com assinatura nova, no MESMO destino daquela entrega (o target_url que a listagem
// mostra). Reenviou com sucesso? A verificação sai da listagem de erros.
// Erro de um item não derruba os demais. Rate limit: 30 chamadas/min por conta.
//
// error possíveis por item:
//   nothing_to_replay   nenhuma entrega com erro nessa verificação (nada a fazer)
//   target_unavailable  o destino daquela entrega não existe mais (endpoint apagado
//                       ou desativado). Reaponte o flow e reemita pelo painel: isso
//                       cria a entrega do endereço NOVO. O replay nunca redireciona
//                       para outro endereço em nome de uma entrega que prometeu ir
//                       para o antigo.
//   payload_unavailable o corpo daquela entrega já foi expurgado pelo prazo de retenção
```

i Seu sistema ficou fora do ar? Liste com `GET /v1/webhook-events` e redispare pelos `verification_ids`: você recebe exatamente o mesmo corpo que teria recebido, sem nenhuma mudança. Os eventos que não pertencem a uma verificação, como a revogação de um dispositivo, são resgatados pelo `resource_id` que veio no próprio evento.

## Link de verificação hospedado

Nem toda integração quer montar o widget. Quando o titular não está no seu site (cobrança por e-mail, onboarding por WhatsApp, atendimento no balcão, QR num contrato), você emite um **link hospedado**: a página é nossa, o fluxo é o mesmo do widget e você só entrega a `url`.

Três diferenças importam antes de escolher. O **link vive horas** e a **sessão só nasce no resgate** (e aí vive os mesmos 900 segundos de sempre), então um link não vira sessão vencida na caixa de entrada de ninguém. O **token** volta **em claro uma única vez**, porque guardamos só o hash: não existe reexibição, e perder o token é emitir outro. E **emitir link não cobra nada**: quem cobra é a verificação que nascer do resgate, com o preço do flow que você escolheu.

**POST** `/v1/verification-links`

```
POST /v1/verification-links
Authorization: Bearer sk_live_…        // a chave define o ambiente (sk_test = sandbox)
Content-Type: application/json

{
  "flow_id": "flow_01J8…",             // obrigatório, e o flow precisa estar live NESTE ambiente
  "reference_id": "user_123",          // obrigatório: o seu id do titular; volta igual no webhook
  "expires_in": 86400,                 // opcional: validade do LINK em segundos (mínimo 60)
  "policy": { "ubo_max_paid_nodes": 3 }  // opcional: mesma política da criação de sessão
}
// Não existem campos "email" nem "phone" aqui, pelo mesmo motivo da criação de sessão: a própria
// página hospedada pergunta o contato ao titular (422 email_not_accepted / phone_not_accepted).

→ 201
{
  "id": "vl_01J8…",                    // o id do link (não é segredo)
  "url": "https://…/v/#vlt_…",         // ENTREGUE ISTO ao titular (o token vai no FRAGMENTO,
                                       // que o navegador nao envia ao servidor: nao cai em log)
  "token": "vlt_…",                    // em claro UMA vez: não há como reexibir
  "expires_in": 86400,                 // segundos de vida do LINK (a sessão só nasce no resgate)
  "environment": "production", "livemode": true,
  "flow_id": "flow_01J8…", "reference_id": "user_123",
  "contact_masked": "pe***@exemplo.com",   // só quando o flow verifica e-mail; o endereço fica cifrado
  "status": "pending", "expires_at": "…", "opened_at": null,
  "consumed_at": null, "session_id": null, "revoked_at": null,
  "created_via": "api", "created_by_member_id": null, "created_at": "…"
}

// ERROS (mesmo envelope { "error", "message" }):
//   401 invalid_api_key · 403 email_not_verified · 403 credential_type_not_allowed
//   404 flow_not_found  · 422 flow_not_live
//   422 email_not_accepted · 422 phone_not_accepted · 429 rate_limited (60 por minuto)
//   422 expires_in_too_long (acima do teto da casa: recusamos com nome, nunca cortamos em silêncio)
//   422 environment_mismatch ("environment" diferente do da chave: o ambiente É a chave)
//   422 policy_module_not_in_flow · 422 policy_ubo_cap_above_flow
//   422 session_not_supported (flow que só recebe alertas do monitoramento transacional)
// Emitir link não cobra nada, e link não resgatado expira sozinho.
```

i **Sem idempotência nesta rota, de propósito.** Selar a resposta a guardaria no banco, e a resposta aqui carrega o segredo do link. Repetir a chamada só cria outro link, que expira sozinho e não custa nada: o risco de guardar o segredo é maior que o de emitir um link a mais.

## Confirmação fora de banda

Um pedido chegou por chamada, por vídeo ou por mensagem: liberar um pagamento, trocar o telefone de uma conta, assinar uma contratação. Quem pede diz ser o seu cliente, e o rosto e a voz na tela parecem os dele. A **confirmação fora de banda** tira a decisão dessa conversa: você manda um link pontual pelo canal que já tinha cadastrado para aquela pessoa, e só ela confirma, com a passkey dela ou com a reautenticação facial com prova de vida.

! **Em breve.** A confirmação usa os módulos [Aprovação de ato com passkey](https://unifokal.com/docs/modulos/passkey#modulo-passkey) e [Reautenticação facial](https://unifokal.com/docs/modulos/face-reauth#modulo-face-reauth), que estão com a venda pausada. O que está descrito abaixo é o contrato que a API já emite, para você planejar a integração.

**Quando usar.** No financeiro, antes de liberar um pagamento pedido fora do fluxo normal. Na central de atendimento, antes de trocar e-mail, telefone ou senha de quem ligou. Na contratação remota, antes de aceitar a assinatura de quem apareceu só na chamada. Em todos, a regra é a mesma: quem inicia é você, pelo canal que você já tinha, com um pedido que só existe naquele momento.

**Como mandar pelo painel.** No detalhe de uma verificação da pessoa, ou na linha dela em Sessões, use **Confirmar identidade agora**. Escolha o flow que confirma (só aparecem os flows ativos com passkey ou com reautenticação facial), escreva o resumo do pedido, com até 140 caracteres, e crie. A tela mostra o link, o QR, o botão de copiar, o prazo de 10 minutos e o estado ao vivo: aguardando, aberto, em andamento, confirmado, não confirmado, recusado pelo titular, expirado ou revogado.

**Como mandar pela API.** É a mesma rota do [link hospedado](https://unifokal.com/docs/api-rest#link-hospedado), com `purpose` igual a `confirmation` e o bloco `act`, que descreve o pedido. O `act.external_id` é obrigatório pela API: é o id do pedido no seu sistema, e volta no webhook para você casar a resposta com o pedido.

```
POST /v1/verification-links
Authorization: Bearer sk_live_…
Content-Type: application/json

{
  "flow_id": "flow_01J8…",             // flow ativo com passkey ou face_reauth, neste ambiente
  "reference_id": "user_123",          // a pessoa que vai confirmar (a mesma do cadastro)
  "purpose": "confirmation",
  "act": {
    "kind": "pix_transfer",            // obrigatório, até 40 caracteres
    "summary": "Transferência de 12.000,00 reais para Fornecedor X",  // obrigatório, até 140
    "amount_cents": 1200000,           // opcional, inteiro em centavos
    "currency": "BRL",                 // opcional, só junto do valor
    "counterparty": "Fornecedor X",    // opcional, até 80 caracteres
    "external_id": "pedido_789"        // obrigatório pela API: o id do pedido no seu sistema
  },
  "expires_in": 600                    // opcional: padrão e máximo de 600 segundos
}

→ 201
{
  "id": "vl_01J8…",
  "purpose": "confirmation",
  "url": "https://…#t=vlt_…",          // ENVIE ISTO pelo canal cadastrado da pessoa
  "token": "vlt_…",                    // em claro uma vez só, como em todo link
  "expires_in": 600,
  "act_kind": "pix_transfer",
  "act_digest": "9f2c…",               // 64 hex: o MESMO digest que chega no webhook
  "reference_id": "user_123", "status": "pending", "expires_at": "…",
  "environment": "production", "livemode": true, "created_via": "api", "…": "…"
}

// ERROS próprios da confirmação (o resto é o vocabulário do link hospedado):
//   422 act_required (faltou o bloco act com kind e summary)
//   422 act_external_id_required (pela API, o external_id do pedido é obrigatório)
//   422 act_not_accepted (act só vale com purpose confirmation)
//   422 act_hostile_char · act_kind_invalid · act_summary_invalid
//   422 act_counterparty_invalid · act_amount_invalid · act_currency_invalid
//   400 unknown_act_key (campo que o bloco act não conhece)
//   422 expires_in_too_long_for_confirmation (acima de 600 segundos)
//   422 confirmation_requires_human_proof (o flow não tem passkey nem face_reauth)
//   422 no_factor_enrolled (a pessoa ainda não tem o fator que o flow pede)
//   429 confirmation_rate_limited (cinco por pessoa por hora; lê o Retry-After)
//   409 confirmation_unavailable (a página de confirmação está fora agora; tente de novo)
```

**Envie pelo canal que você já tinha cadastrado para esta pessoa. Nunca pelo chat da chamada ou da conversa em que o pedido chegou.** Pode ser e-mail, mensagem ou SMS para o contato do cadastro. A pessoa abre o link, vê a sua marca, o resumo do pedido e o prazo, e escolhe entre confirmar com o fator dela ou dizer **Não reconheço este pedido**.

**O que volta.** Quando a pessoa confirma, o seu webhook recebe `verification.completed` com `data.purpose` igual a `confirmation` e o bloco `data.act` com `kind`, `digest` e `external_id`. O `digest` é o mesmo `act_digest` da criação do link: um digest só do link ao webhook. O `check_details` diz qual fator confirmou. Com passkey, o bloco do módulo `passkey` traz `user_verified` e a evidência conferível da assinatura. Com o rosto, o bloco do módulo `face_reauth` traz a semelhança à matrícula feita no cadastro: é semelhança, e não se lê como autenticação.

```
// verification.completed de uma confirmação (trecho de data)
{
  "status": "approved",
  "reference_id": "user_123",
  "purpose": "confirmation",
  "act": { "kind": "pix_transfer", "digest": "9f2c…", "external_id": "pedido_789" },
  "check_details": [ { "module": "passkey", "passed": true, "outcome": "approved",
                       "data": { "passkey_id": "spk_01J9ZC6Q8R3M2V7K4T5N1B0XHD", "bound_by": "cadastro",
                                 "assurance": "identidade", "backup_eligible": false, "backup_state": false,
                                 "user_verified": true, "act_digest": "9f2c…", "act_kind": "pix_transfer",
                                 "evidence": { "format": "unifokal/passkey-assertion@1" },
                                 "model_version": "passkey-v1" } } ]
}
```

Se a pessoa disser que não reconhece o pedido, o webhook recebe `act.rejected`, a sessão fecha e não há cobrança. Trate como sinal para parar o pedido e falar com ela pelo canal cadastrado, e não como prova de fraude: quem recebeu o link encaminhado também consegue recusar. Se a confirmação não fechar, o `verification.completed` chega reprovado ou em revisão, e a revisão de uma confirmação nunca vira aprovação pelo painel: a decisão do pedido é sua, pelo seu canal. Se o prazo acabar sem resposta, nenhum webhook de verificação é enviado, e o estado aparece no painel como expirado.

**Os limites.** O link de confirmação vale no máximo 10 minutos, e a sessão que nasce dele vale o que resta desse prazo, sem renovação: quem perdeu o prazo pede um novo link. Existe no máximo um pedido aberto por pessoa, e o novo substitui o anterior, que é revogado. São no máximo cinco pedidos por pessoa por hora. O resumo aparece para a pessoa exatamente como você escreveu, então caractere invisível, de controle ou de direção do texto é recusado. No sandbox, a cerimônia da passkey é simulada na própria página, e o bloco do módulo traz `simulated`.

**O que ela não é.** O link é transporte, não fator: quem o abre ainda precisa da passkey ou do rosto da pessoa. Por isso e-mail nunca é fator de autenticação aqui, e um clique num link nunca confirma nada sozinho, como a norma pública NIST SP 800-63B-4, seção 3.1.3.1, exige. E a confirmação não é detecção de deepfake na chamada: ela não olha a chamada, ela tira a decisão de dentro dela.

## Spec OpenAPI e tipos

Tudo o que esta página promete existe também como **OpenAPI 3.1**: um documento **gerado do código do backend** e comparado com ele em teste a cada mudança, cobrindo as 4 rotas públicas, a rota de capacidades, o corpo assinado do webhook e o código estável de cada erro, por status. Spec escrita à mão envelhece e mente; esta não tem como.

SPEC · SITE (URL estável)

https://unifokal.com/docs/openapi.json

SPEC · API (mesmo documento)

https://api.unifokal.com/v1/openapi.json

Esta página declara o spec no `<head>` (`rel="describedby"` e `rel="service-desc"`), então ferramentas o encontram sozinhas. Para gerar tipos TypeScript do contrato, uma linha basta:

**curl** · Gerar os tipos do contrato · no terminal

```sh
# O contrato inteiro, legivel por maquina: OpenAPI 3.1 GERADA do codigo do backend e comparada
# com ele em teste a cada mudanca (spec que mente e pior que nao ter spec).
curl -sS https://unifokal.com/docs/openapi.json -o unifokal-openapi.json

# Tipos TypeScript do contrato em uma linha, sem SDK para instalar nem manter:
npx openapi-typescript@7 unifokal-openapi.json -o unifokal-api.d.ts

# O QUE ESPERAR DE VOLTA: o arquivo unifokal-api.d.ts com os tipos de request, resposta e erro das
# rotas publicas e do corpo do webhook. No seu codigo:
#   import type { paths, webhooks } from "./unifokal-api";
# A API serve o MESMO documento em https://api.unifokal.com/v1/openapi.json (com ETag: revalidar custa um 304),
# e o catalogo vivo de modulos e precos esta em GET https://api.unifokal.com/v1/capabilities.
```

**GET** `/v1/capabilities`

A **descoberta viva** da API: módulos, preços em centavos, estado `available`/`coming_soon` e o grafo `requires`, lidos do banco (a mesma fonte da vitrine de preços, nunca uma segunda lista). A credencial é **opcional**: anônimo recebe o catálogo geral com o preço-base público; com a sua `sk_` no `Authorization`, a **mesma URL** devolve o contrato efetivo da sua organização (preço negociado, ambiente e `livemode` da chave).

```
curl -sS https://api.unifokal.com/v1/capabilities

→ 200 { "api_version": "v1",
        "spec_url": "https://api.unifokal.com/v1/openapi.json",
        "docs_url": "https://unifokal.com/docs",
        "llms_url": "https://unifokal.com/llms.txt",
        "webhook_schema_version": 1,
        "context": { "authenticated": false, "environment": null,
                     "livemode": null, "pricing": "list" },
        "modules": [ { "module": "cpf_receita", "group": "validation",
                       "status": "available", "unit_cents": …,
                       "is_addon": false,
                       "requires": ["cpf_ocr", "face", "liveness"] }, … ] }
// unit_cents = preço em centavos lido do banco (o MESMO da vitrine de preços);
// o preço do flow é a soma dos módulos ligados. coming_soon = ainda não vendível.
// Com -H "Authorization: Bearer $UNIFOKAL_SECRET_KEY": o MESMO formato, com
// "context": { "authenticated": true, "pricing": "contract" } e o preço efetivo
// da SUA organização (contrato negociado aparece aqui, não o de tabela).
```

## Coleção importável

A API inteira numa coleção pronta para o seu cliente HTTP, no formato aberto **Postman Collection v2.1**, que o Postman, o Insomnia e o Bruno importam. Ela é gerada da mesma especificação OpenAPI a cada publicação, então acompanha a API: traz as rotas públicas da chave secreta com o corpo mínimo e a autenticação já montados, em pastas na ordem de uso e com o link para a seção de cada uma nesta documentação.

**A coleção não carrega chave nem script.** A autenticação usa a variável `UNIFOKAL_SECRET_KEY`, que você define no ambiente do seu cliente HTTP (ou no cofre dele), nunca dentro da coleção. Nenhum código roda ao importar.

1. Baixe o arquivo ou importe pelo endereço [https://unifokal.com/docs/unifokal.postman_collection.json](https://unifokal.com/docs/unifokal.postman_collection.json).
2. Crie um ambiente com `UNIFOKAL_SECRET_KEY` (a sua chave de sandbox, `sk_test`) e `UNIFOKAL_FLOW_ID` (um flow do mesmo ambiente).
3. Rode. O catálogo público responde sem chave nenhuma, e as outras chamadas usam a chave do ambiente.

[Baixar a coleção](https://unifokal.com/docs/unifokal.postman_collection.json)

## Para agentes de IA

Integrando com um agente (Claude, Cursor, Copilot ou o seu)? Aponte-o para [https://unifokal.com/llms.txt](https://unifokal.com/llms.txt): é o briefing, no formato **llmstxt.org**, do que um agente **não infere do spec**: retry seguro e idempotência, a semântica dos códigos de erro, o webhook como fonte da verdade, o sandbox determinístico e o que nunca fazer com a chave secreta. A versão expandida, com os 10 pontos por extenso, está em [https://unifokal.com/llms-full.txt](https://unifokal.com/llms-full.txt).

O contrato executável fica no [spec OpenAPI 3.1](https://unifokal.com/docs/api-rest#openapi) e a descoberta viva de módulos e preços em [`GET /v1/capabilities`](https://unifokal.com/docs/api-rest#get-capabilities). Os três arquivos são gerados do código e comparados com ele em teste: o que o seu agente lê é o que a API faz.

Para dar ao seu assistente a documentação inteira de um produto de uma vez, use os [pacotes de contexto](https://unifokal.com/docs/pacotes-para-ia#pacotes-para-ia): cada um é esta documentação em Markdown, recortada por produto, com o tamanho medido. E toda página daqui tem a própria versão em Markdown, na mesma URL com `.md` no fim.

O ponto de partida por máquina é o catálogo de API em [`/.well-known/api-catalog`](https://unifokal.com/.well-known/api-catalog) (RFC 9727): de lá saem o spec e esta documentação, em uma leitura. E o nosso [`robots.txt`](https://unifokal.com/robots.txt) nomeia os principais rastreadores de resposta, um a um, para dizer com todas as letras o que já valia: esta superfície é aberta para leitura por máquina. A área autenticada e as rotas de API seguem fora, para todo mundo igual.
