Guias de Desenvolvimento

Signing Sessions

Guias dos SDKs

Verificação Biométrica — BIOMETRIC

Verificação de identidade por reconhecimento facial com liveness e comparação biométrica.


O que é

O perfil BIOMETRIC exige que o signatário passe por uma verificação facial. A página hospedada abre a câmera do dispositivo, realiza um teste de liveness (detecção de vida) e compara o rosto capturado com a foto de referência cadastrada (enrollment). Ideal para verificação de identidade em contratos de alto valor.

Signatário completa liveness (câmera) ──> Comparação facial ──> COMPLETED

Requisitos

CampoObrigatórioNotas
nameSimNome completo
cpfSimCPF do signatário (11 dígitos) — usado para comparação biométrica
userExternalIdSimID no seu sistema
emailNãoOpcional

Pré-requisito: O signatário deve ter um enrollment biométrico prévio (PUT /v1/users/{id}/enrollment) com foto de referência. Como alternativa, a foto pode ser enviada na própria sessão — veja Imagem de referência.


Criar a sessão

Fluxo hospedado (recomendado)

A página hospedada executa toda a captura de liveness. Seu backend só cria e espera.

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

const session = await client.signingSessions.create({
  purpose: 'DOCUMENT_SIGNATURE',
  policy: { profile: 'BIOMETRIC' },
  signer: {
    name: 'Carlos Lima',
    cpf: '12345678901',
    userExternalId: 'user-003',
  },
  document: { content: pdfBase64, filename: 'contrato.pdf' },
  returnUrl: 'https://app.example.com/done',
  locale: 'pt-BR',
  expiresInMinutes: 60,
});

console.log('Client Secret:', session.clientSecret);

const result = await client.signingSessions.waitForCompletion(session.sessionId);
console.log('Status:', result.status);       // COMPLETED
console.log('Evidence ID:', result.evidenceId);
session = client.signing_sessions.create(CreateSigningSessionRequest(
    purpose='DOCUMENT_SIGNATURE',
    policy=Policy(profile='BIOMETRIC'),
    signer=Signer(name='Carlos Lima', cpf='12345678901', user_external_id='user-003'),
    document=InlineDocument(content=pdf_base64, filename='contrato.pdf'),
    return_url='https://app.example.com/done',
    locale='pt-BR',
    expires_in_minutes=60,
))

print('Client Secret:', session.client_secret)

result = client.signing_sessions.wait_for_completion(session.session_id)
print('Status:', result.status)       # COMPLETED
print('Evidence ID:', result.evidence_id)
session, err := client.SigningSessions.Create(ctx, &signdocs.CreateSigningSessionRequest{
    Purpose:          signdocs.PurposeDocumentSignature,
    Policy:           signdocs.Policy{Profile: signdocs.PolicyProfileBiometric},
    Signer:           signdocs.Signer{Name: "Carlos Lima", CPF: "12345678901", UserExternalID: "user-003"},
    Document:         &signdocs.DocumentInline{Content: pdfBase64, Filename: "contrato.pdf"},
    ReturnURL:        "https://app.example.com/done",
    Locale:           "pt-BR",
    ExpiresInMinutes: 60,
})
if err != nil { log.Fatal(err) }

fmt.Println("Client Secret:", session.ClientSecret)

result, err := client.SigningSessions.WaitForCompletion(ctx, session.SessionID)
if err != nil { log.Fatal(err) }
fmt.Println("Status:", result.Status)       // COMPLETED
fmt.Println("Evidence ID:", result.EvidenceID)
String pdfBase64 = Base64.getEncoder().encodeToString(Files.readAllBytes(Path.of("contrato.pdf")));

CreateSigningSessionRequest request = new CreateSigningSessionRequest();
request.purpose = "DOCUMENT_SIGNATURE";
request.policy = new Policy("BIOMETRIC");
request.signer = new Signer("Carlos Lima", "user-003");
request.signer.cpf = "12345678901";
request.document = new CreateSigningSessionRequest.InlineDocument(pdfBase64, "contrato.pdf");
request.returnUrl = "https://app.example.com/done";
request.locale = "pt-BR";
request.expiresInMinutes = 60;

SigningSession session = client.signingSessions().create(request);
System.out.println("Client Secret: " + session.clientSecret);

SigningSessionStatusResponse result = client.signingSessions().waitForCompletion(session.sessionId);
System.out.println("Status: " + result.status);       // COMPLETED
System.out.println("Evidence ID: " + result.evidenceId);
$pdfBase64 = base64_encode(file_get_contents('contrato.pdf'));

$session = $client->signingSessions->create(new CreateSigningSessionRequest(
    purpose: 'DOCUMENT_SIGNATURE',
    policy: new Policy(profile: 'BIOMETRIC'),
    signer: new Signer(name: 'Carlos Lima', cpf: '12345678901', userExternalId: 'user-003'),
    document: ['content' => $pdfBase64, 'filename' => 'contrato.pdf'],
    returnUrl: 'https://app.example.com/done',
    locale: 'pt-BR',
    expiresInMinutes: 60,
));

echo "Client Secret: " . $session->clientSecret . "\n";

$result = $client->signingSessions->waitForCompletion($session->sessionId);
echo "Status: " . $result->status . "\n";       // COMPLETED
echo "Evidence ID: " . $result->evidenceId . "\n";
var pdfBase64 = Convert.ToBase64String(await File.ReadAllBytesAsync("contrato.pdf"));

var session = await client.SigningSessions.CreateAsync(new CreateSigningSessionRequest
{
    Purpose = "DOCUMENT_SIGNATURE",
    Policy = new Policy { Profile = "BIOMETRIC" },
    Signer = new Signer { Name = "Carlos Lima", Cpf = "12345678901", UserExternalId = "user-003" },
    Document = new InlineDocument { Content = pdfBase64, Filename = "contrato.pdf" },
    ReturnUrl = "https://app.example.com/done",
    Locale = "pt-BR",
    ExpiresInMinutes = 60,
});

Console.WriteLine($"Client Secret: {session.ClientSecret}");

var result = await client.SigningSessions.WaitForCompletionAsync(session.SessionId);
Console.WriteLine($"Status: {result.Status}");       // COMPLETED
Console.WriteLine($"Evidence ID: {result.EvidenceId}");

Experiência do signatário

A página hospedada abre a câmera, guia o signatário em movimentos de cabeça para o teste de liveness, captura a imagem e realiza a comparação facial automaticamente. Se a comparação for bem-sucedida, a sessão é concluída.


Alternativa programática (headless)

Para integrações server-to-server: inicie o liveness, aguarde o resultado, complete o match.

const session = await client.signingSessions.create({
  purpose: 'DOCUMENT_SIGNATURE',
  policy: { profile: 'BIOMETRIC' },
  signer: { name: 'Carlos Lima', cpf: '12345678901', userExternalId: 'user-003' },
  document: { content: pdfBase64, filename: 'contrato.pdf' },
});

const baseUrl = process.env.SIGNDOCS_BASE_URL;
const headers = {
  'Authorization': `Bearer ${session.clientSecret}`,
  'Content-Type': 'application/json',
};
const advanceUrl = `${baseUrl}/v1/signing-sessions/${session.sessionId}/advance`;

// Passo 1: Iniciar liveness — retorna hostedUrl para o signatário
const startRes = await fetch(advanceUrl, {
  method: 'POST', headers,
  body: JSON.stringify({ action: 'start_liveness' }),
});
const startBody = await startRes.json();
console.log('Hosted URL:', startBody.hostedUrl);
console.log('Liveness Session ID:', startBody.livenessSessionId);

// (Signatário completa a verificação facial na hostedUrl)

// Passo 2: Completar liveness (após o signatário finalizar)
const completeRes = await fetch(advanceUrl, {
  method: 'POST', headers,
  body: JSON.stringify({
    action: 'complete_liveness',
    livenessSessionId: startBody.livenessSessionId,
  }),
});
const completeBody = await completeRes.json();
console.log('Status:', completeBody.status);       // COMPLETED
console.log('Evidence ID:', completeBody.evidenceId);
session = client.signing_sessions.create(CreateSigningSessionRequest(
    purpose='DOCUMENT_SIGNATURE',
    policy=Policy(profile='BIOMETRIC'),
    signer=Signer(name='Carlos Lima', cpf='12345678901', user_external_id='user-003'),
    document=InlineDocument(content=pdf_base64, filename='contrato.pdf'),
))

base_url = os.environ['SIGNDOCS_BASE_URL']
headers = {'Authorization': f'Bearer {session.client_secret}', 'Content-Type': 'application/json'}
advance_url = f'{base_url}/v1/signing-sessions/{session.session_id}/advance'

# Passo 1: Iniciar liveness
start_res = requests.post(advance_url, headers=headers, json={'action': 'start_liveness'})
start_body = start_res.json()
print('Hosted URL:', start_body['hostedUrl'])
liveness_session_id = start_body['livenessSessionId']

# (Signatário completa a verificação facial)

# Passo 2: Completar liveness
complete_res = requests.post(advance_url, headers=headers, json={
    'action': 'complete_liveness',
    'livenessSessionId': liveness_session_id,
})
complete_body = complete_res.json()
print('Status:', complete_body['status'])       # COMPLETED
print('Evidence ID:', complete_body['evidenceId'])
session, _ := client.SigningSessions.Create(ctx, &signdocs.CreateSigningSessionRequest{
    Purpose:  signdocs.PurposeDocumentSignature,
    Policy:   signdocs.Policy{Profile: signdocs.PolicyProfileBiometric},
    Signer:   signdocs.Signer{Name: "Carlos Lima", CPF: "12345678901", UserExternalID: "user-003"},
    Document: &signdocs.DocumentInline{Content: pdfBase64, Filename: "contrato.pdf"},
})

advanceURL := fmt.Sprintf("%s/v1/signing-sessions/%s/advance",
    os.Getenv("SIGNDOCS_BASE_URL"), session.SessionID)

// Passo 1: Iniciar liveness
startBody, _ := json.Marshal(map[string]string{"action": "start_liveness"})
startReq, _ := http.NewRequest("POST", advanceURL, bytes.NewReader(startBody))
startReq.Header.Set("Authorization", "Bearer "+session.ClientSecret)
startReq.Header.Set("Content-Type", "application/json")
startResp, _ := http.DefaultClient.Do(startReq)
var startResult map[string]interface{}
json.NewDecoder(startResp.Body).Decode(&startResult)
livenessSessionID := startResult["livenessSessionId"].(string)

// (Signatário completa a verificação facial)

// Passo 2: Completar liveness
completeBody, _ := json.Marshal(map[string]interface{}{
    "action": "complete_liveness", "livenessSessionId": livenessSessionID,
})
completeReq, _ := http.NewRequest("POST", advanceURL, bytes.NewReader(completeBody))
completeReq.Header.Set("Authorization", "Bearer "+session.ClientSecret)
completeReq.Header.Set("Content-Type", "application/json")
completeResp, _ := http.DefaultClient.Do(completeReq)
var completeResult map[string]interface{}
json.NewDecoder(completeResp.Body).Decode(&completeResult)
fmt.Println("Status:", completeResult["status"])       // COMPLETED
fmt.Println("Evidence ID:", completeResult["evidenceId"])
SigningSession session = client.signingSessions().create(request); // (ver hosted acima)

HttpClient http = HttpClient.newHttpClient();
String advanceUrl = System.getenv("SIGNDOCS_BASE_URL")
    + "/v1/signing-sessions/" + session.sessionId + "/advance";

// Passo 1: Iniciar liveness
HttpRequest startReq = HttpRequest.newBuilder().uri(URI.create(advanceUrl))
    .header("Authorization", "Bearer " + session.clientSecret)
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("{\"action\":\"start_liveness\"}")).build();
String startBody = http.send(startReq, HttpResponse.BodyHandlers.ofString()).body();
// Extrair hostedUrl e livenessSessionId

// (Signatário completa a verificação facial)

// Passo 2: Completar liveness
HttpRequest completeReq = HttpRequest.newBuilder().uri(URI.create(advanceUrl))
    .header("Authorization", "Bearer " + session.clientSecret)
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(
        "{\"action\":\"complete_liveness\",\"livenessSessionId\":\"" + livenessSessionId + "\"}")).build();
String completeBody = http.send(completeReq, HttpResponse.BodyHandlers.ofString()).body();
System.out.println(completeBody); // {"status":"COMPLETED","evidenceId":"..."}
$session = $client->signingSessions->create(/* ... ver hosted acima ... */);

$advanceUrl = getenv('SIGNDOCS_BASE_URL')
    . '/v1/signing-sessions/' . $session->sessionId . '/advance';
$headers = [
    'Authorization: Bearer ' . $session->clientSecret,
    'Content-Type: application/json',
];

// Passo 1: Iniciar liveness
$ch = curl_init($advanceUrl);
curl_setopt_array($ch, [
    CURLOPT_POST => true, CURLOPT_HTTPHEADER => $headers,
    CURLOPT_POSTFIELDS => json_encode(['action' => 'start_liveness']),
    CURLOPT_RETURNTRANSFER => true,
]);
$startBody = json_decode(curl_exec($ch));
$livenessSessionId = $startBody->livenessSessionId;

// (Signatário completa a verificação facial)

// Passo 2: Completar liveness
$ch = curl_init($advanceUrl);
curl_setopt_array($ch, [
    CURLOPT_POST => true, CURLOPT_HTTPHEADER => $headers,
    CURLOPT_POSTFIELDS => json_encode([
        'action' => 'complete_liveness',
        'livenessSessionId' => $livenessSessionId,
    ]),
    CURLOPT_RETURNTRANSFER => true,
]);
$completeBody = json_decode(curl_exec($ch));
echo "Status: " . $completeBody->status . "\n";       // COMPLETED
echo "Evidence ID: " . $completeBody->evidenceId . "\n";
var session = await client.SigningSessions.CreateAsync(/* ... ver hosted acima ... */);

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
    new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", session.ClientSecret);
var advanceUrl = $"{Environment.GetEnvironmentVariable("SIGNDOCS_BASE_URL")}/v1/signing-sessions/{session.SessionId}/advance";

// Passo 1: Iniciar liveness
var startRes = await http.PostAsync(advanceUrl,
    new StringContent("{\"action\":\"start_liveness\"}", System.Text.Encoding.UTF8, "application/json"));
var startJson = System.Text.Json.JsonDocument.Parse(await startRes.Content.ReadAsStringAsync());
var livenessSessionId = startJson.RootElement.GetProperty("livenessSessionId").GetString();

// (Signatário completa a verificação facial)

// Passo 2: Completar liveness
var completeRes = await http.PostAsync(advanceUrl,
    new StringContent($"{{\"action\":\"complete_liveness\",\"livenessSessionId\":\"{livenessSessionId}\"}}",
        System.Text.Encoding.UTF8, "application/json"));
Console.WriteLine(await completeRes.Content.ReadAsStringAsync());
// {"status":"COMPLETED","evidenceId":"..."}

HML: A etapa de liveness pode ser simulada em sandbox. Em produção, requer câmera do dispositivo.


Imagem de referência

A etapa BIOMETRIC_MATCH compara o rosto capturado no liveness com uma imagem de referência. Essa imagem é resolvida nesta ordem:

OrdemOrigemQuando se aplica
1referenceImage.content, enviado na criação da sessãoTem precedência sobre o enrollment sempre que presente
2Enrollment cadastrado para signer.userExternalIdCaso padrão — nada a enviar na sessão
Nenhuma das duasA etapa de comparação falha com 422

Usando o enrollment (padrão)

Cadastre a foto uma vez com PUT /v1/users/{userExternalId}/enrollment e crie as sessões normalmente, sem enviar imagem alguma. O userExternalId usado no enrollment precisa ser o mesmo de signer.userExternalId na sessão. Veja Enrollment Biométrico.

Imagem por transação

Quando não há foto cadastrada — ou quando esta assinatura específica deve usar outra foto — envie a imagem na criação da sessão:

curl -X POST "$BASE_URL/v1/signing-sessions" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "purpose": "DOCUMENT_SIGNATURE",
    "policy": { "profile": "BIOMETRIC" },
    "signer": {
      "name": "Maria Silva",
      "cpf": "12345678909",
      "userExternalId": "usr_maria_001"
    },
    "document": { "content": "<base64 do PDF>", "filename": "contrato.pdf" },
    "referenceImage": { "content": "<base64 da foto JPEG>" }
  }'

Na API direta de transações (POST /v1/transactions), a imagem por transação segue outro caminho: vai no complete da etapa, como referenceImage: { "source": "BASE64_IMAGE", "data": "<base64>" }, e exige a flag perTransactionReferenceEnabled no tenant. Mesmo nome de campo, formato diferente — em sessões de assinatura, use { "content": ... } na criação.

Na evidência, a comparação registra a procedência da imagem em enrollmentRef: o campo source (ENROLLMENT ou TRANSACTION_PROVIDED) diz de onde ela foi lida, e capturedFrom (BANK_PROVIDED, FIRST_LIVENESS, DOCUMENT_PHOTO ou SESSION_CREATE) diz como foi capturada originalmente.


Apertando o limiar numa sessão só

policy.minSimilarity e policy.minLivenessConfidence sobem a régua daquela sessão, sem mexer na configuração do tenant. Aceitam 0 a 100.

curl -X POST "$BASE_URL/v1/signing-sessions" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "purpose": "DOCUMENT_SIGNATURE",
    "policy": {
      "profile": "BIOMETRIC",
      "minSimilarity": 95,
      "minLivenessConfidence": 95
    },
    "signer": { "name": "Maria Silva", "cpf": "12345678909", "userExternalId": "usr_maria_001" },
    "document": { "content": "<base64 do PDF>", "filename": "contrato.pdf" }
  }'

Só aperta, nunca afrouxa. Valor abaixo do mínimo configurado no tenant é recusado com 400 — um documento de maior risco pode exigir mais, e nenhuma requisição pode exigir menos do que o tenant contratou.


Testando a reprovação em sandbox

Em sandbox — todo o ambiente de homologação, e em produção os tenants com mode: SANDBOX — a comparação facial não chama o Rekognition: a similaridade é simulada, com padrão de 99.5%, que aprova. Para exercitar o caminho de reprovação, defina sandboxSimilarity abaixo do limiar do tenant (padrão 80%).

Como a página hospedada é quem chama o advance, o valor entra pela metadata da transação, definida uma vez na criação da sessão:

curl -X POST "$BASE_URL/v1/signing-sessions" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "purpose": "DOCUMENT_SIGNATURE",
    "policy": { "profile": "BIOMETRIC" },
    "signer": {
      "name": "Maria Silva",
      "cpf": "12345678909",
      "userExternalId": "usr_maria_001"
    },
    "document": { "content": "<base64 do PDF>", "filename": "contrato.pdf" },
    "metadata": { "sandboxSimilarity": "40" }
  }'

O signatário percorre a página normalmente e a etapa BIOMETRIC_MATCH reprova, com o mesmo resultado que teria em produção: a etapa vai a FAILED e a resposta do advance traz o passo reprovado.

Em integrações headless, que chamam o advance diretamente, o mesmo valor pode ir no corpo da requisição — e tem precedência sobre a metadata:

curl -X POST "$BASE_URL/v1/signing-sessions/$SESSION_ID/advance" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "complete_liveness",
    "livenessSessionId": "sandbox-liveness-001",
    "sandboxSimilarity": 40
  }'

Ensaiando foto ruim: sandboxBrightness e sandboxSharpness

Similaridade baixa ensaia "não é a mesma pessoa". Não ensaia o erro que mais aparece no chão de fábrica: luz ruim, reflexo no visor, sujeito em movimento. Esses dois campos simulam a qualidade da captura e são o único jeito de produzir DOCUMENT_QUALITY_LOW sob demanda.

# na metadata, uma vez, para o fluxo hospedado
"metadata": { "sandboxBrightness": "15", "sandboxSharpness": "13" }

# ou no corpo do advance/complete, em integração headless
{ "action": "complete_liveness", "livenessSessionId": "sandbox-liveness-001",
  "sandboxBrightness": 15, "sandboxSharpness": 13 }

Aceitam 0 a 100, padrão 80 — que aprova. Mesma precedência do sandboxSimilarity: corpo vence metadata, e captura real vence os dois.


Detalhes da biometria


← Aceite + OTP  |  Próximo: Biometria + OTP →

Voltar para Visão Geral