Guias de Desenvolvimento

Política e Conformidade

Signing Sessions

Sessão de Confiança

Modelos de Solução

Guias dos SDKs

KYC Digital de Clientes com Assinatura Eletrônica — Modelo de Solução

Verifique a identidade dos seus clientes com biometria facial e prova de vida via API, gere uma trilha de evidências assinada criptograficamente com certificado ICP-Brasil e, na sequência, colha a assinatura eletrônica do contrato de adesão com validade jurídica fundada na MP 2.200-2/2001 (art. 10, §2º) — tudo em conformidade com a LGPD. O resultado de negócio: onboarding digital que aprova clientes legítimos em minutos, reprova fraudes automaticamente e deixa um dossiê probatório pronto para auditoria e regulador.

Público-alvo: times de produto e engenharia de fintechs, plataformas de crédito, exchanges de criptoativos e marketplaces que precisam de KYC digital (Know Your Customer) no cadastro de clientes ou vendedores.


⚡ Caminho de 15 minutos

  1. Solicite credenciais HML e a ativação de Sessão de Confiança em app.signdocs.com.br/admin/api-clients ou por contato@signdocs.com.br.
  2. Gere um token OAuth2 em POST /oauth2/token (grant client_credentials).
  3. Crie a verificação com POST /v1/trust-sessions (purpose=ACTION_AUTHENTICATION, perfil BIOMETRIC) e receba url + clientSecret.
  4. Abra url?cs=<clientSecret> no navegador e conclua a selfie com prova de vida (em HML, a biometria é simulada — nenhum rosto real é necessário).
  5. Baixe o pacote de evidências assinado (GET /v1/trust-sessions/{id}/evidence → arquivo .p7m): cliente verificado, evidência arquivada.

1. O problema de negócio

Onboarding manual de clientes é o gargalo clássico de qualquer operação financeira digital: o cliente envia foto de documento por e-mail ou formulário, alguém do backoffice compara a selfie com o RG "no olho", e a decisão de aprovar ou reprovar leva horas — às vezes dias. Nesse intervalo, o cliente legítimo desiste e abre conta no concorrente, enquanto o fraudador com um documento roubado e uma foto impressa passa despercebido. Para fintechs e plataformas de crédito, cada dia de fila de KYC é receita perdida; para exchanges e marketplaces, cada fraude que passa é chargeback, conta laranja e risco regulatório.

A verificação de identidade com biometria facial resolve as duas pontas ao mesmo tempo. Uma prova de vida via API (liveness detection) garante que há uma pessoa real na frente da câmera — não uma foto, não um vídeo reproduzido — e o registro fica amarrado ao CPF informado, com carimbo de data/hora do servidor, IP, geolocalização e hash SHA-256, tudo dentro de um pacote de evidências assinado digitalmente. Seu backoffice deixa de "olhar selfie" e passa a receber um webhook: TRANSACTION.COMPLETED aprova o cadastro automaticamente, TRANSACTION.FAILED manda para a fila de revisão ou reprova.

E como o mesmo motor também assina documentos, o passo seguinte do onboarding — o contrato de adesão, os termos de uso da conta — sai na mesma integração: uma sessão de assinatura eletrônica logo após o KYC aprovado, com o evidenceId da verificação encadeado nos metadados do contrato. Onboarding digital de fintech completo, do "quero abrir conta" ao contrato assinado, sem papel e sem fila.

2. Arquitetura do fluxo

Cliente (app/site)        Seu backend               SignDocs API            Página hospedada
      │                        │                          │                        │
      │ 1. "Abrir conta"       │                          │                        │
      │───────────────────────>│                          │                        │
      │                        │ 2. POST /v1/trust-sessions                        │
      │                        │    { action, signer, policy: BIOMETRIC }          │
      │                        │─────────────────────────>│                        │
      │                        │    { url, clientSecret } │                        │
      │ 3. Redirect            │<─────────────────────────│                        │
      │    url?cs=...          │                          │                        │
      │<───────────────────────│                          │                        │
      │ 4. Selfie + prova de vida (liveness)              │                        │
      │───────────────────────────────────────────────────────────────────────────>│
      │                        │                          │  gera Evidence Pack    │
      │                        │ 5. Webhook TRANSACTION.COMPLETED                  │
      │                        │<─────────────────────────│                        │
      │                        │ 6. Aprova cadastro no backoffice                  │
      │                        │ 7. (Opcional) POST /v1/signing-sessions           │
      │                        │    contrato de adesão, CLICK_PLUS_OTP             │
      │                        │─────────────────────────>│                        │
      │                        │ 8. GET .../evidence → arquiva .p7m no dossiê KYC  │

Por que este cenário usa Sessão de Confiança como primitiva central — e não uma sessão de assinatura ou envelope:

Primitiva O que é Papel neste cenário
Sessão de Confiança (POST /v1/trust-sessions) Autentica uma ação (sem PDF) com as mesmas etapas de identidade: OTP, biometria, certificado O KYC em si: não há documento a assinar, há uma identidade a verificar
Sessão de Assinatura (POST /v1/signing-sessions) 1 signatário assina 1 documento PDF Etapa seguinte opcional: o contrato de adesão à conta
Envelope (POST /v1/envelopes) 2+ signatários sobre o mesmo documento Não usado aqui; reserve para contratos com avalista ou sócios (veja Envelopes)

O detalhamento técnico completo da Sessão de Confiança aplicada a KYC (perfis, contexto regulatório BACEN/VASP, cadeia de reverificações) está no guia técnico de KYC e Onboarding. Este guia é o mapa de negócio por cima dele.

3. Pré-requisitos

🟠 HML vs Produção — desenvolva sempre em HML (https://api-hml.signdocs.com.br, com hífen). Credenciais HML não consomem cota do plano e os dados expiram em 7 dias. Em HML self-service a biometria roda em modo simulado — perfeito para testar o fluxo de ponta a ponta sem rosto real.

4. Autenticação OAuth2

Todo o fluxo é servidor-a-servidor: troque client_id + client_secret por um token Bearer em POST /oauth2/token.

curl -s -X POST https://api-hml.signdocs.com.br/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=$SIGNDOCS_CLIENT_ID" \
  -d "client_secret=$SIGNDOCS_CLIENT_SECRET" \
  -d "scope=transactions:read transactions:write steps:write evidence:read webhooks:write"
const resp = await fetch('https://api-hml.signdocs.com.br/oauth2/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: process.env.SIGNDOCS_CLIENT_ID!,
    client_secret: process.env.SIGNDOCS_CLIENT_SECRET!,
    scope: 'transactions:read transactions:write steps:write evidence:read webhooks:write',
  }),
});
const { access_token: jwt } = await resp.json();
import os, requests

resp = requests.post(
    "https://api-hml.signdocs.com.br/oauth2/token",
    data={
        "grant_type": "client_credentials",
        "client_id": os.environ["SIGNDOCS_CLIENT_ID"],
        "client_secret": os.environ["SIGNDOCS_CLIENT_SECRET"],
        "scope": "transactions:read transactions:write steps:write evidence:read webhooks:write",
    },
    timeout=10,
)
jwt = resp.json()["access_token"]

Dica — o token expira; renove-o no seu backend em vez de gerar um por requisição. Veja o guia de autenticação do SDK.

5. Criar a verificação KYC (Sessão de Confiança)

O coração do fluxo: uma Sessão de Confiança com purpose=ACTION_AUTHENTICATION (fixo nessa rota) e perfil BIOMETRIC — selfie com prova de vida. Não há PDF; a "coisa autenticada" é descrita no campo action, que fica gravado no pacote de evidências e é exibido ao cliente na página hospedada.

Use metadata para carregar as chaves do seu sistema (ID da proposta de conta, nível de risco): elas voltam no webhook e são o caminho mais curto para correlacionar o resultado com o cadastro no seu backoffice.

curl -s -X POST https://api-hml.signdocs.com.br/v1/trust-sessions \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: kyc-ACC-2026-58432" \
  -d '{
    "action": {
      "type": "kyc_account_opening",
      "description": "Verificação de identidade para abertura de conta — proposta ACC-2026-58432"
    },
    "policy": { "profile": "BIOMETRIC" },
    "signer": {
      "name": "Maria Souza",
      "cpf": "123.456.789-09",
      "email": "maria.souza@example.com",
      "phone": "+5511999990000",
      "otpChannel": "email"
    },
    "returnUrl": "https://app.example.com/onboarding/kyc-concluido",
    "cancelUrl": "https://app.example.com/onboarding/kyc-cancelado",
    "metadata": {
      "account_application_id": "ACC-2026-58432",
      "risk_tier": "standard"
    },
    "locale": "pt-BR",
    "expiresInMinutes": 60
  }'

Resposta 201 Created:

{
  "sessionId": "01JXYZ...",
  "transactionId": "01JXYZ...",
  "status": "ACTIVE",
  "url": "https://sign-hml.signdocs.com.br/s/01JXYZ...",
  "clientSecret": "ss_secret_eyJhbGciOi...",
  "expiresAt": "2026-07-10T15:00:00Z",
  "createdAt": "2026-07-10T14:00:00Z"
}
import { randomUUID } from 'node:crypto';

async function createKycSession(application: {
  id: string; name: string; cpf: string; email: string; phone: string;
}) {
  const resp = await fetch('https://api-hml.signdocs.com.br/v1/trust-sessions', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.SIGNDOCS_JWT}`,
      'Content-Type': 'application/json',
      'X-Idempotency-Key': `kyc-${application.id}`,
    },
    body: JSON.stringify({
      action: {
        type: 'kyc_account_opening',
        description: `Verificação de identidade para abertura de conta — proposta ${application.id}`,
      },
      policy: { profile: 'BIOMETRIC' },
      signer: {
        name: application.name,
        cpf: application.cpf,
        email: application.email,
        phone: application.phone,
        otpChannel: 'email',
      },
      returnUrl: 'https://app.example.com/onboarding/kyc-concluido',
      cancelUrl: 'https://app.example.com/onboarding/kyc-cancelado',
      metadata: { account_application_id: application.id, risk_tier: 'standard' },
      locale: 'pt-BR',
      expiresInMinutes: 60,
    }),
  });
  if (!resp.ok) throw new Error(`SignDocs error: ${resp.status} ${await resp.text()}`);
  return resp.json() as Promise<{ sessionId: string; url: string; clientSecret: string }>;
}
def create_kyc_session(application):
    resp = requests.post(
        "https://api-hml.signdocs.com.br/v1/trust-sessions",
        headers={
            "Authorization": f"Bearer {os.environ['SIGNDOCS_JWT']}",
            "Content-Type": "application/json",
            "X-Idempotency-Key": f"kyc-{application['id']}",
        },
        json={
            "action": {
                "type": "kyc_account_opening",
                "description": f"Verificação de identidade para abertura de conta — proposta {application['id']}",
            },
            "policy": {"profile": "BIOMETRIC"},
            "signer": {
                "name": application["name"],
                "cpf": application["cpf"],
                "email": application["email"],
                "phone": application["phone"],
                "otpChannel": "email",
            },
            "returnUrl": "https://app.example.com/onboarding/kyc-concluido",
            "cancelUrl": "https://app.example.com/onboarding/kyc-cancelado",
            "metadata": {"account_application_id": application["id"], "risk_tier": "standard"},
            "locale": "pt-BR",
            "expiresInMinutes": 60,
        },
        timeout=10,
    )
    resp.raise_for_status()
    return resp.json()

⚠️ Idempotência — derive a X-Idempotency-Key de uma chave de negócio estável (como o ID da proposta), não de um UUID aleatório por request. Assim um retry após timeout não cria (nem cobra) uma segunda verificação.

O link completo é url + "?cs=" + clientSecret. O clientSecret é de uso único e não é armazenado pela API — guarde-o apenas o tempo necessário para entregar o link.

No onboarding digital o padrão é o redirect direto: o cliente acabou de preencher o cadastro no seu app ou site, então redirecione-o imediatamente para a página hospedada. Ao concluir a selfie ele volta para o seu returnUrl. Alternativas: enviar o link por e-mail, SMS ou WhatsApp (usando a API do WhatsApp Business do próprio cliente ou um fluxo n8n/Make como canal de entrega).

# O link final entregue ao cliente:
echo "${URL}?cs=${CLIENT_SECRET}"
# https://sign-hml.signdocs.com.br/s/01JXYZ...?cs=ss_secret_eyJhbGciOi...
// Em um endpoint Express, logo após criar a sessão:
app.post('/onboarding/:id/start-kyc', async (req, res) => {
  const application = await db.applications.get(req.params.id);
  const { sessionId, url, clientSecret } = await createKycSession(application);
  await db.applications.update(application.id, { kycSessionId: sessionId, kycStatus: 'PENDING' });
  res.redirect(`${url}?cs=${clientSecret}`);
});
# Em uma view Flask, logo após criar a sessão:
@app.post("/onboarding/<application_id>/start-kyc")
def start_kyc(application_id):
    application = db.applications.get(application_id)
    session = create_kyc_session(application)
    db.applications.update(application_id, kyc_session_id=session["sessionId"], kyc_status="PENDING")
    return redirect(f"{session['url']}?cs={session['clientSecret']}")

Na página hospedada o cliente vê a badge "Sessão de Confiança", a descrição da ação ("Verificação de identidade para abertura de conta…") e é conduzido pela selfie com prova de vida. Sem preview de PDF — porque não há PDF.

7. Acompanhar o resultado

Para o fluxo de produção, use webhooks (seção adiante). Para scripts, jobs ou telas de espera, o endpoint de status é leve e direto:

curl -s -X GET https://api-hml.signdocs.com.br/v1/trust-sessions/$SESSION_ID/status \
  -H "Authorization: Bearer $JWT"
{
  "sessionId": "01JXYZ...",
  "transactionId": "01JXYZ...",
  "status": "COMPLETED",
  "completedAt": "2026-07-10T14:12:34Z",
  "evidenceId": "ev_01JXYZ..."
}
const status = await fetch(
  `https://api-hml.signdocs.com.br/v1/trust-sessions/${sessionId}/status`,
  { headers: { Authorization: `Bearer ${process.env.SIGNDOCS_JWT}` } },
).then(r => r.json());
// status.status: ACTIVE | COMPLETED | FAILED | EXPIRED | CANCELLED
status = requests.get(
    f"https://api-hml.signdocs.com.br/v1/trust-sessions/{session_id}/status",
    headers={"Authorization": f"Bearer {os.environ['SIGNDOCS_JWT']}"},
    timeout=10,
).json()
# status["status"]: ACTIVE | COMPLETED | FAILED | EXPIRED | CANCELLED

8. Opcional: contrato de adesão na sequência

KYC aprovado, o passo natural é o contrato de adesão da conta. Aqui a primitiva muda: agora um documento, então use uma sessão de assinatura (POST /v1/signing-sessions) com perfil CLICK_PLUS_OTP — o cliente acabou de provar identidade por biometria; para o contrato, aceite + código OTP é proporcional e rápido.

O detalhe que transforma dois eventos soltos em um dossiê: grave o evidenceId do KYC no metadata do contrato. A evidência do contrato passa a apontar para a evidência da verificação de identidade — cadeia auditável sem replicar dados.

PDF_BASE64=$(base64 -w0 contrato-adesao.pdf)

curl -s -X POST https://api-hml.signdocs.com.br/v1/signing-sessions \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: adesao-ACC-2026-58432" \
  -d '{
    "purpose": "DOCUMENT_SIGNATURE",
    "policy": { "profile": "CLICK_PLUS_OTP" },
    "signer": {
      "name": "Maria Souza",
      "cpf": "123.456.789-09",
      "email": "maria.souza@example.com",
      "otpChannel": "email"
    },
    "document": { "content": "'"$PDF_BASE64"'", "filename": "contrato-adesao.pdf" },
    "metadata": {
      "account_application_id": "ACC-2026-58432",
      "kyc_evidence_id": "ev_01JXYZ..."
    },
    "returnUrl": "https://app.example.com/onboarding/contrato-assinado",
    "locale": "pt-BR",
    "expiresInMinutes": 60
  }'
import { readFileSync } from 'node:fs';

const pdfBase64 = readFileSync('contrato-adesao.pdf').toString('base64');

const session = await client.signingSessions.create({
  purpose: 'DOCUMENT_SIGNATURE',
  policy: { profile: 'CLICK_PLUS_OTP' },
  signer: { name: 'Maria Souza', email: 'maria.souza@example.com', userExternalId: 'user-58432' },
  document: { content: pdfBase64, filename: 'contrato-adesao.pdf' },
  metadata: { account_application_id: 'ACC-2026-58432', kyc_evidence_id: kycEvidenceId },
  returnUrl: 'https://app.example.com/onboarding/contrato-assinado',
  locale: 'pt-BR',
  expiresInMinutes: 60,
});
// Link do contrato: `${session.url}?cs=${session.clientSecret}`
import base64

pdf_base64 = base64.b64encode(open("contrato-adesao.pdf", "rb").read()).decode()

session = client.signing_sessions.create(CreateSigningSessionRequest(
    purpose='DOCUMENT_SIGNATURE',
    policy=Policy(profile='CLICK_PLUS_OTP'),
    signer=Signer(name='Maria Souza', email='maria.souza@example.com', user_external_id='user-58432'),
    document=InlineDocument(content=pdf_base64, filename='contrato-adesao.pdf'),
    metadata={'account_application_id': 'ACC-2026-58432', 'kyc_evidence_id': kyc_evidence_id},
    return_url='https://app.example.com/onboarding/contrato-assinado',
    locale='pt-BR',
    expires_in_minutes=60,
))
# Link do contrato: f"{session.url}?cs={session.client_secret}"

E-mail de convite automático — a API só envia e-mail de convite ao signatário quando a requisição inclui owner.email diferente de signer.email. No fluxo de onboarding você geralmente entrega o link no próprio app (redirect), então isso raramente importa — mas se quiser que a SignDocs envie o e-mail do contrato, inclua o owner.

Fluxos e variações de sessão de assinatura estão em Assinatura Expressa e nas receitas.

9. Trilha de evidências: o pacote .p7m

Cada sessão concluída gera um pacote de evidências: um JSON canônico com todas as etapas (aceite, prova de vida, OTP), CPF e dados do titular, timestamps ISO do servidor, IP, user-agent, geolocalização e hash SHA-256 — embrulhado em um contêiner PKCS#7/CMS (.p7m) assinado com certificado ICP-Brasil A1. É esse arquivo que você arquiva no dossiê KYC do cliente e apresenta a auditoria interna ou regulador.

Baixe o .p7m autenticado:

curl -s -X GET https://api-hml.signdocs.com.br/v1/trust-sessions/$SESSION_ID/evidence \
  -H "Authorization: Bearer $JWT" \
  -o kyc-$SESSION_ID.p7m

E consulte o JSON canônico a qualquer momento pelo endpoint público de verificação (sem autenticação — ideal para responder a um questionamento externo):

curl -s https://api-hml.signdocs.com.br/v1/verify/$EVIDENCE_ID | jq .
// Arquivar o .p7m no seu storage de dossiês
const p7m = await fetch(
  `https://api-hml.signdocs.com.br/v1/trust-sessions/${sessionId}/evidence`,
  { headers: { Authorization: `Bearer ${process.env.SIGNDOCS_JWT}` } },
).then(r => r.arrayBuffer());
await dossierStorage.put(`kyc/${applicationId}/${evidenceId}.p7m`, Buffer.from(p7m));

// Consulta pública do JSON canônico
const evidence = await fetch(
  `https://api-hml.signdocs.com.br/v1/verify/${evidenceId}`,
).then(r => r.json());
# Arquivar o .p7m no seu storage de dossiês
p7m = requests.get(
    f"https://api-hml.signdocs.com.br/v1/trust-sessions/{session_id}/evidence",
    headers={"Authorization": f"Bearer {os.environ['SIGNDOCS_JWT']}"},
    timeout=30,
).content
dossier_storage.put(f"kyc/{application_id}/{evidence_id}.p7m", p7m)

# Consulta pública do JSON canônico
evidence = requests.get(
    f"https://api-hml.signdocs.com.br/v1/verify/{evidence_id}", timeout=10,
).json()

A prova de integridade é composta por: timestamp ISO do servidor + trilha de auditoria completa + hash SHA-256 + assinatura do pacote com certificado ICP-Brasil. Qualquer parte pode validar a assinatura criptográfica de forma independente pelo verificador público (https://verificador.signdocs.com.br/{evidenceId}). A anatomia completa do pacote — campo a campo, e como apresentá-lo a um auditor — está no guia de evidências.

🟠 Retenção — em HML os dados expiram em 7 dias. Em produção, arquive o .p7m no seu próprio storage de dossiês assim que a sessão concluir; é o seu registro de compliance, trate-o como tal.

Biometria facial é dado pessoal sensível na LGPD (Lei 13.709/2018, art. 5º, II), e seu tratamento segue as hipóteses do art. 11. A definição da base legal do seu KYC é uma decisão do seu jurídico/DPO — instituições reguladas frequentemente se apoiam no cumprimento de obrigação legal ou regulatória, mas não assuma: valide com quem responde pelo seu programa de privacidade.

O que a integração já entrega para sustentar essa conformidade:

⚠️ Não improvise doutrina — este guia descreve o que a API registra, não substitui parecer jurídico. Envolva seu DPO na definição de base legal, prazo de retenção do dossiê e resposta a direitos do titular.

11. Upgrade: biometria com verificação SERPRO

O perfil BIOMETRIC faz selfie + prova de vida — suficiente para a maioria dos onboardings. Quando a sua regulação (ou o seu apetite de risco) exigir cross-check do rosto contra bases governamentais (CNH/RG via SERPRO), o mesmo fluxo aceita o upgrade sem mudar a arquitetura: troca-se o perfil da política pela variante com verificação SERPRO (BIOMETRIC_SERPRO, documentada no guia técnico de KYC) e a sessão passa a incluir a etapa de match facial contra a base do governo, descontando a cota monthlySerproIdentity.

É o caminho indicado para instituições sob exigência de KYC reforçado (abertura de conta em SCD/ESC, reverificação periódica de VASP) e para tíquetes de crédito altos. Detalhes de contratação, cotas e comportamento de fallback quando o SERPRO oscila estão no guia do SDK de biometria SERPRO.


Webhooks: aprovação e reprovação automáticas

O ganho operacional do KYC digital se materializa aqui: em vez de alguém conferir uma fila, seu backoffice reage a eventos. Registre um webhook e trate dois eventos:

Evento Significado Ação típica no backoffice
TRANSACTION.COMPLETED Cliente concluiu todas as etapas (prova de vida OK) Aprovar cadastro, liberar conta, disparar contrato de adesão
TRANSACTION.FAILED Falha definitiva em uma etapa (ex: biometria reprovada) Reprovar ou enviar para fila de revisão manual
curl -s -X POST https://api-hml.signdocs.com.br/v1/webhooks \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://backoffice.example.com/webhooks/signdocs",
    "events": ["TRANSACTION.COMPLETED", "TRANSACTION.FAILED"]
  }'

O payload traz sessionId, transactionId, status, evidenceId e o seu metadata — ou seja, o account_application_id volta para você e a correlação é imediata.

app.post('/webhooks/signdocs', async (req, res) => {
  // 1. Valide a assinatura HMAC do header antes de processar (ver ./webhooks.html)
  const { event, data } = req.body;
  const applicationId = data.metadata.account_application_id;

  if (event === 'TRANSACTION.COMPLETED') {
    await backoffice.approveApplication(applicationId, { kycEvidenceId: data.evidenceId });
  } else if (event === 'TRANSACTION.FAILED') {
    await backoffice.sendToManualReview(applicationId, { reason: 'kyc_failed' });
  }
  res.status(200).end();
});
@app.post("/webhooks/signdocs")
def signdocs_webhook():
    # 1. Valide a assinatura HMAC do header antes de processar (ver ./webhooks.html)
    body = request.get_json()
    application_id = body["data"]["metadata"]["account_application_id"]

    if body["event"] == "TRANSACTION.COMPLETED":
        backoffice.approve_application(application_id, kyc_evidence_id=body["data"]["evidenceId"])
    elif body["event"] == "TRANSACTION.FAILED":
        backoffice.send_to_manual_review(application_id, reason="kyc_failed")
    return "", 200

Todos os payloads são assinados com HMAC-SHA256 — valide a assinatura e o timestamp antes de aprovar qualquer coisa. A entrega é at-least-once, então torne o handler idempotente (o evidenceId é uma boa chave de deduplicação). Especificação completa, incluindo validação HMAC passo a passo e os demais eventos (SIGNING_SESSION.COMPLETED para o contrato de adesão, por exemplo), no guia de webhooks.

⚠️ URL pública obrigatória — o registro de webhook com URL localhost ou IP privado é bloqueado na borda (403). Em desenvolvimento, use um túnel (ngrok, cloudflared) ou webhook.site.

Escolhendo o perfil de política

Perfil Etapas Papel no cenário KYC
CLICK_PLUS_OTP Aceite + código OTP (e-mail/SMS) Contrato de adesão pós-KYC. Não substitui verificação de identidade — prova posse do canal, não o rosto
BIOMETRIC Selfie + prova de vida Default recomendado para a Sessão de Confiança de KYC
BIOMETRIC_PLUS_OTP Biometria + OTP Contas de maior risco: prova identidade e posse do canal na mesma sessão
DIGITAL_CERTIFICATE Assinatura com certificado ICP-Brasil Clientes que já possuem e-CPF/e-CNPJ (comum em PJ e contas de alto valor)

Recomendação default: BIOMETRIC na verificação de identidade e CLICK_PLUS_OTP no contrato de adesão. É a combinação com melhor equilíbrio entre robustez probatória e conversão de onboarding — biometria onde a identidade importa, fricção mínima onde ela já foi provada. Suba para BIOMETRIC_PLUS_OTP ou para a variante SERPRO conforme o risco. O mapa completo de perfis e seus efeitos jurídicos está em Perfis de assinatura e níveis legais.

Caminho sem código

Esses caminhos servem bem para pilotos e para o time de operações; o fluxo de produção de uma fintech normalmente acaba na API direta pelos requisitos de idempotência e auditoria.

Erros comuns

403 — feature não habilitada

{ "type": "feature-not-enabled", "status": 403,
  "detail": "trustSessionsEnabled is not active for this tenant." }

Sessão de Confiança é liberada por feature flag no tenant. Solicite a ativação (e as cotas de monthlyTrustSessions/monthlyBiometric) ao seu Customer Success.

400 — documento enviado em trust session

{ "type": "validation-error", "status": 400,
  "detail": "Field 'document' is not accepted when purpose is ACTION_AUTHENTICATION." }

POST /v1/trust-sessions não aceita PDF — é exatamente essa a diferença para a Assinatura Expressa. Para o contrato de adesão, use POST /v1/signing-sessions.

400 — perfil de política inválido

{ "type": "validation-error", "status": 400,
  "detail": "policy.profile must be one of CLICK_ONLY, CLICK_PLUS_OTP, BIOMETRIC, BIOMETRIC_PLUS_OTP, DIGITAL_CERTIFICATE, CUSTOM." }

Valores como DIGITAL_SIGN_A1 são tipos de etapa que aparecem nas respostas, não perfis de política. Envie um dos perfis do enum.

429 — cota esgotada

{ "type": "quota-exceeded", "status": 429,
  "detail": "monthlyBiometric quota exhausted for this period." }

Cada sessão BIOMETRIC desconta uma unidade de monthlyTrustSessions e uma de monthlyBiometric. Consulte os headers RateLimit-Remaining/RateLimit-Reset e dimensione as cotas com o CS antes do go-live. Cotas não são reembolsadas em cancelamento.

401 — token expirado

{ "type": "unauthorized", "status": 401, "detail": "Access token is invalid or expired." }

Tokens client_credentials expiram; renove no backend com cache do token, e confira que está chamando o ambiente certo (api-hml com credenciais HML).

Perguntas frequentes

O que é KYC digital e por que usar biometria facial?

KYC (Know Your Customer) é o processo de verificar que o cliente é quem diz ser antes de abrir conta ou liberar crédito. A biometria facial com prova de vida substitui a conferência manual de documentos: garante que há uma pessoa real e viva na câmera e amarra o registro ao CPF informado, com evidência criptográfica de data, hora e contexto.

A prova de vida impede fraude com foto ou vídeo?

A etapa de liveness detection é projetada para distinguir uma pessoa presente de artefatos como foto impressa, tela de celular ou vídeo reproduzido. Nenhum controle isolado elimina 100% da fraude — por isso o resultado vem acompanhado de IP, geolocalização e trilha completa no pacote de evidências, e por isso existe o upgrade com cross-check SERPRO para os casos de maior risco.

O KYC digital com biometria tem validade jurídica?

O pacote de evidências registra a autenticação com trilha de auditoria, hash SHA-256 e assinatura PKCS#7 com certificado ICP-Brasil — verificável de forma independente por qualquer parte. Para o contrato de adesão assinado na sequência, a validade da assinatura eletrônica se funda na MP 2.200-2/2001, art. 10, §2º. Detalhes por perfil em Perfis de assinatura e níveis legais.

Preciso guardar a selfie do cliente no meu banco de dados?

Não — e pela lógica de minimização da LGPD, é melhor não guardar. Armazene o evidenceId e o arquivo .p7m; eles contêm o registro probatório da verificação. Quando outra operação precisar comprovar o KYC, referencie o evidenceId em vez de replicar dados biométricos.

Posso reaproveitar o KYC para o contrato de adesão?

Sim, e esse é o desenho recomendado: a identidade foi provada por biometria na Sessão de Confiança, então o contrato pode usar CLICK_PLUS_OTP (mais rápido, menos fricção) com o kyc_evidence_id gravado no metadata. As duas evidências ficam encadeadas no dossiê.

Você controla com expiresInMinutes na criação da sessão. Para onboarding com redirect imediato, 30–60 minutos funciona bem; se a sessão expirar (status: EXPIRED), crie uma nova — o link antigo não é reaproveitável.

Próximos passos