Criar conta grátis

Documentação
Ver em Markdown

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.

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.

!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
iSeu 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.

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.
iSem 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 e Reautenticação facial, 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, 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:

curlGerar os tipos do contratono terminal
# 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.
  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.

Para agentes de IA

Integrando com um agente (Claude, Cursor, Copilot ou o seu)? Aponte-o para 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.

O contrato executável fica no spec OpenAPI 3.1 e a descoberta viva de módulos e preços em GET /v1/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: 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 (RFC 9727): de lá saem o spec e esta documentação, em uma leitura. E o nosso 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.

Pronto para integrar? A chave de sandbox sai no painel, logo depois do cadastro. Criar conta grátis