Guias de Desenvolvimento

Signing Sessions

Guias dos SDKs

Enrollment Biométrico

Cadastre imagens de referência facial para verificação biométrica em transações futuras.

Visão Geral

O enrollment biométrico permite pré-cadastrar a imagem de referência facial de um usuário. Essa imagem é utilizada nas etapas BIOMETRIC_MATCH e DOCUMENT_PHOTO_MATCH para comparar o rosto capturado em tempo real com a referência armazenada.

O enrollment é versionado — cada chamada incrementa a versão, permitindo atualizar a foto de referência sem perder o histórico.

Endpoint

PUT /v1/users/{userExternalId}/enrollment
Content-Type: application/json
Authorization: Bearer {access_token}

Campos da Requisição

Campo Tipo Obrigatório Descrição
image string (base64) Sim Imagem facial em base64 (JPEG). Para DOCUMENT_PHOTO, envie a foto completa do documento.
cpf string Sim CPF do titular (11 dígitos, sem pontuação). Vincula o enrollment à identidade do usuário.
source string Não Origem da imagem. Padrão: ORGANIZATION_PROVIDED.

Fontes de Enrollment

1. ORGANIZATION_PROVIDED (padrão)

Foto vinda dos registros da própria organização — crachá, ficha de RH, folha de pagamento. O titular não estava presente na captura; quem responde pela imagem é a organização. A imagem deve conter exatamente um rosto visível.

BANK_PROVIDED era o nome anterior deste valor, de quando o único cliente era um banco. Continua aceito e é normalizado na gravação: o que volta na resposta e o que vai para a evidência é sempre ORGANIZATION_PROVIDED.

curl -X PUT "$BASE_URL/v1/users/usr_maria_001/enrollment" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "image": "'$(base64 -w0 selfie.jpg)'",
    "cpf": "12345678909",
    "source": "ORGANIZATION_PROVIDED"
  }'

2. FIRST_LIVENESS

Imagem capturada na primeira sessão de liveness do usuário. Útil quando a organização não tem foto prévia e quer usar a primeira verificação facial como referência. Diferente da anterior, aqui o titular estava presente e a captura foi verificada ao vivo.

curl -X PUT "$BASE_URL/v1/users/usr_maria_001/enrollment" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "image": "'$(base64 -w0 liveness-frame.jpg)'",
    "cpf": "12345678909",
    "source": "FIRST_LIVENESS"
  }'

3. DOCUMENT_PHOTO

Foto de documento de identidade (CNH, RG ou passaporte). O sistema extrai automaticamente a face do documento e a utiliza como imagem de enrollment.

Feature flag requerida: Esta fonte requer que a flag documentExtractionEnabled esteja habilitada no tenant. Solicite a ativação ao seu gerente de conta.
curl -X PUT "$BASE_URL/v1/users/usr_maria_001/enrollment" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "image": "'$(base64 -w0 cnh-frente.jpg)'",
    "cpf": "12345678909",
    "source": "DOCUMENT_PHOTO"
  }'

Neste fluxo, o servidor:

  1. Recebe a foto completa do documento
  2. Detecta e extrai a face do documento via DetectFaces
  3. Valida que exatamente um rosto foi encontrado
  4. Armazena a face extraída (crop) como imagem de enrollment
  5. Retorna os campos adicionais documentImageHash e extractionConfidence

Sem foto prévia? Uma sessão de assinatura pode carregar a própria imagem de referência, em referenceImage.content na criação, sem passar por enrollment. Ela vale só para aquela transação e não cria cadastro. Veja Imagem de referência.

Campos da Resposta

Campo Tipo Descrição
userExternalId string Identificador externo do usuário.
enrollmentHash string Hash SHA-256 da imagem de enrollment (face crop quando DOCUMENT_PHOTO).
enrollmentVersion integer Versão do enrollment (incrementada a cada atualização).
enrollmentSource string Origem do enrollment: ORGANIZATION_PROVIDED, FIRST_LIVENESS ou DOCUMENT_PHOTO.
enrolledAt string (ISO 8601) Data/hora do enrollment.
cpf string CPF do titular.
faceConfidence number Confiança da detecção facial (0-100%).
documentImageHash string Hash SHA-256 do documento original. Presente apenas quando enrollmentSource é DOCUMENT_PHOTO.
extractionConfidence number Confiança da extração facial do documento (0-100%). Presente apenas quando enrollmentSource é DOCUMENT_PHOTO.

Exemplo de resposta (ORGANIZATION_PROVIDED)

{
  "userExternalId": "usr_maria_001",
  "enrollmentHash": "a1b2c3d4e5f6...",
  "enrollmentVersion": 1,
  "enrollmentSource": "ORGANIZATION_PROVIDED",
  "enrolledAt": "2026-02-28T10:30:00.000Z",
  "cpf": "12345678909",
  "faceConfidence": 99.8
}

Exemplo de resposta (DOCUMENT_PHOTO)

{
  "userExternalId": "usr_maria_001",
  "enrollmentHash": "f6e5d4c3b2a1...",
  "enrollmentVersion": 2,
  "enrollmentSource": "DOCUMENT_PHOTO",
  "enrolledAt": "2026-02-28T10:35:00.000Z",
  "cpf": "12345678909",
  "faceConfidence": 98.5,
  "documentImageHash": "9a8b7c6d5e4f...",
  "extractionConfidence": 97.2
}

Validação de Qualidade Facial

Todas as imagens passam por validação via DetectFaces para garantir:

Caso a validação falhe, a API retorna 422 Unprocessable Entity com o motivo específico.


Triagem antes de gravar (dryRun)

Enviar dryRun: true avalia a foto e não grava nada: nenhum cadastro é criado, nenhuma imagem vai para o S3, nenhuma cota é consumida. Serve para conferir um lote de fotos antes de comprometer o cadastro.

PUT /v1/users/matricula-4471/enrollment
Content-Type: application/json
Authorization: Bearer {access_token}

{
  "dryRun": true,
  "image": "<base64>",
  "cpf": "12345678900"
}

Funciona igual em produção e em HML — não é recurso de sandbox.

const report = await client.users.enroll('matricula-4471', {
  image: imageBase64,
  cpf: '12345678900',
  dryRun: true,
});

if (report.referenceQuality === 'rejected') {
  // Nada foi gravado: peça outra foto antes de cadastrar.
  console.warn(report.warnings);
}
report = client.users.enroll(
    "matricula-4471",
    image=image_base64,
    cpf="12345678900",
    dry_run=True,
)

if report.reference_quality == "rejected":
    # Nada foi gravado: peça outra foto antes de cadastrar.
    print(report.warnings)
report, _ := client.Users.Enroll(ctx, "matricula-4471", &signdocs.EnrollUserRequest{
    Image:  imageBase64,
    CPF:    "12345678900",
    DryRun: true,
})

if report.ReferenceQuality == "rejected" {
    // Nada foi gravado: peça outra foto antes de cadastrar.
    fmt.Println(report.Warnings)
}
EnrollUserRequest req = new EnrollUserRequest(imageBase64, "12345678900");
req.setDryRun(true);

EnrollUserResponse report = client.users().enroll("matricula-4471", req);

if ("rejected".equals(report.getReferenceQuality())) {
    // Nada foi gravado: peça outra foto antes de cadastrar.
    System.out.println(report.getWarnings());
}
$report = $client->users->enroll('matricula-4471', new EnrollUserRequest(
    image: $imageBase64,
    cpf: '12345678900',
    dryRun: true,
));

if ($report->referenceQuality === 'rejected') {
    // Nada foi gravado: peça outra foto antes de cadastrar.
    print_r($report->warnings);
}
var report = await client.Users.EnrollAsync("matricula-4471", new EnrollUserRequest(
    Image: imageBase64,
    Cpf: "12345678900",
    DryRun: true));

if (report?.ReferenceQuality == "rejected")
{
    // Nada foi gravado: peça outra foto antes de cadastrar.
    Console.WriteLine(string.Join(", ", report.Warnings ?? []));
}

O campo que responde "essa foto serve?"

referenceQuality vem em toda resposta de cadastro — individual, em lote, gravação real e dryRun — com um de três valores:

ValorSignificadoO que fazer
usableServe como referência.Nada.
marginalGrava, mas com ressalvas em warnings.Vale repetir a foto se for barato.
rejectedNão serve.Tirar outra foto antes de cadastrar.

Não confunda com status. Numa linha de lote, status diz o que aconteceu com a gravação (enrolled / failed), que é outra pergunta. Uma foto ruim que gravou bem é status: enrolled com referenceQuality: marginal — a combinação que vale agir, e que um campo só não conseguiria expressar.

Também não confunda com faceConfidence. Esse responde "isto é um rosto?", e satura perto de 100 em foto boa e em foto inútil do mesmo jeito: uma imagem com brilho 15 e nitidez 13 cadastra com 99,99 de confiança e só falha na comparação, meses depois.

{
  "userExternalId": "matricula-4471",
  "status": "enrolled",
  "referenceQuality": "marginal",
  "warnings": ["LOW_BRIGHTNESS"],
  "quality":  { "brightness": 22.4, "sharpness": 61.8 },
  "pose":     { "yaw": -4.1, "pitch": 2.7, "roll": 0.9 },
  "faceCoverage": 0.11,
  "faceConfidence": 99.99,
  "enrollmentVersion": 1,
  "expiresAt": "2026-12-01T00:00:00.000Z"
}

Cadastro em lote

Para onboarding de muitos funcionários de uma vez. Até 25 por requisição, com sucesso parcial: uma linha ruim não derruba as outras.

POST /v1/users/enrollments
Content-Type: application/json
Authorization: Bearer {access_token}

{
  "dryRun": false,
  "enrollments": [
    { "userExternalId": "matricula-4471", "image": "<base64>", "cpf": "12345678900" },
    { "userExternalId": "matricula-4472", "image": "<base64>", "cpf": "98765432100" }
  ]
}

A resposta traz o resumo e uma linha por item, na ordem enviada:

{
  "submitted": 2, "succeeded": 2, "failed": 0, "dryRun": false,
  "usable": 1, "marginal": 1, "rejected": 0,
  "results": [
    { "index": 0, "userExternalId": "matricula-4471", "status": "enrolled",
      "referenceQuality": "usable", "warnings": [], "enrollmentVersion": 1 },
    { "index": 1, "userExternalId": "matricula-4472", "status": "enrolled",
      "referenceQuality": "marginal", "warnings": ["SMALL_FACE"], "enrollmentVersion": 1 }
  ]
}

Três coisas que costumam pegar

Pelos SDKs

const result = await client.users.enrollBatch({
  dryRun: false,
  enrollments: [
    { userExternalId: 'matricula-4471', image: img1, cpf: '12345678900' },
    { userExternalId: 'matricula-4472', image: img2, cpf: '98765432100' },
  ],
});

// Leia results, não o status HTTP: um lote meio reprovado ainda é 200.
for (const row of result.results) {
  if (row.status === 'failed') console.error(row.userExternalId, row.error);
  else if (row.referenceQuality === 'marginal') console.warn(row.userExternalId, row.warnings);
}
result = client.users.enroll_batch(
    enrollments=[
        {"userExternalId": "matricula-4471", "image": img1, "cpf": "12345678900"},
        {"userExternalId": "matricula-4472", "image": img2, "cpf": "98765432100"},
    ],
    dry_run=False,
)

# Leia results, não o status HTTP: um lote meio reprovado ainda é 200.
for row in result.results:
    if row.status == "failed":
        print(row.user_external_id, row.error)
    elif row.reference_quality == "marginal":
        print(row.user_external_id, row.warnings)
result, _ := client.Users.EnrollBatch(ctx, &signdocs.EnrollUsersBatchRequest{
    Enrollments: []signdocs.BatchEnrollmentItem{
        {UserExternalID: "matricula-4471", Image: img1, CPF: "12345678900"},
        {UserExternalID: "matricula-4472", Image: img2, CPF: "98765432100"},
    },
})

// Leia Results, não o status HTTP: um lote meio reprovado ainda é 200.
for _, row := range result.Results {
    if row.Status == "failed" {
        fmt.Println(row.UserExternalID, row.Error)
    }
}
BatchEnrollmentModels.Request req = new BatchEnrollmentModels.Request();
req.setEnrollments(List.of(
    new BatchEnrollmentModels.Item("matricula-4471", img1, "12345678900"),
    new BatchEnrollmentModels.Item("matricula-4472", img2, "98765432100")));
req.setDryRun(false);

BatchEnrollmentModels.Response result = client.users().enrollBatch(req);

// Leia getResults(), não o status HTTP: um lote meio reprovado ainda é 200.
for (BatchEnrollmentModels.Result row : result.getResults()) {
    if ("failed".equals(row.getStatus())) {
        System.out.println(row.getUserExternalId() + ": " + row.getError());
    }
}
$result = $client->users->enrollBatch([
    ['userExternalId' => 'matricula-4471', 'image' => $img1, 'cpf' => '12345678900'],
    ['userExternalId' => 'matricula-4472', 'image' => $img2, 'cpf' => '98765432100'],
], dryRun: false);

// Leia results, não o status HTTP: um lote meio reprovado ainda é 200.
foreach ($result->results as $row) {
    if ($row->status === 'failed') {
        echo $row->userExternalId . ': ' . $row->error . PHP_EOL;
    }
}
var result = await client.Users.EnrollBatchAsync(new EnrollUsersBatchRequest(
    Enrollments: new List<BatchEnrollmentItem>
    {
        new("matricula-4471", img1, "12345678900"),
        new("matricula-4472", img2, "98765432100"),
    },
    DryRun: false));

// Leia Results, não o status HTTP: um lote meio reprovado ainda é 200.
foreach (var row in result?.Results ?? [])
{
    if (row.Status == "failed") Console.WriteLine($"{row.UserExternalId}: {row.Error}");
}

Erros Comuns

Código Tipo Causa Solução
400 Bad Request Campo image ou cpf ausente/inválido Verifique se ambos os campos obrigatórios estão presentes e no formato correto
422 Unprocessable Entity Nenhum rosto detectado na imagem ou mais de um rosto Envie uma imagem com exatamente um rosto visível e bem iluminado
422 Feature Not Enabled source: "DOCUMENT_PHOTO" sem documentExtractionEnabled Solicite a ativação da feature flag ao gerente de conta
401 Unauthorized Token inválido ou expirado Obtenha um novo token via POST /oauth2/token

Boas Práticas