Guias de Desenvolvimento

Política e Conformidade

Signing Sessions

Sessão de Confiança

Modelos de Solução

Guias dos SDKs

NT65 — Consignado INSS (BIOMETRIC_SERPRO_AUTO_FALLBACK)

A Nota Técnica 65/2023 (INSS/Dataprev) estabelece requisitos mínimos de autenticação biométrica para contratos de empréstimo consignado INSS. Este guia cobre o fluxo completo de conformidade NT65 utilizando o perfil consolidado BIOMETRIC_SERPRO_AUTO_FALLBACK, que resolve verificação SERPRO e fallback por documento em uma única transação.

Pré-requisitos:


Fluxo completo: BIOMETRIC_SERPRO_AUTO_FALLBACK

Passo 1: Criar transação

O perfil BIOMETRIC_SERPRO_AUTO_FALLBACK exige cpf e birthDate no signer. A API injeta automaticamente a etapa PURPOSE_DISCLOSURE como primeira etapa e cria uma etapa DOCUMENT_PHOTO_MATCH em status DORMANT (dormante — ativada automaticamente se o SERPRO não possuir imagem biométrica).

Opcionalmente, inclua fallbackDocument com a foto do documento de identidade. Se o fallback for acionado, essa imagem será usada automaticamente sem necessidade de nova captura.

const tx = await client.transactions.create({
  purpose: 'DOCUMENT_SIGNATURE',
  policy: { profile: 'BIOMETRIC_SERPRO_AUTO_FALLBACK' },
  signer: {
    name: 'Maria Santos',
    userExternalId: 'user-001',
    cpf: '12345678900',
    birthDate: '1985-03-20',
  },
  document: { content: documentBase64, filename: 'consignado.pdf' },
  fallbackDocument: {
    image: cnhPhotoBase64,
    type: 'CNH',
  },
  metadata: { loanType: 'CONSIGNADO_INSS' },
});
// tx.status === 'DOCUMENT_UPLOADED'
tx = client.transactions.create(CreateTransactionRequest(
    purpose='DOCUMENT_SIGNATURE',
    policy=Policy(profile='BIOMETRIC_SERPRO_AUTO_FALLBACK'),
    signer=Signer(
        name='Maria Santos',
        user_external_id='user-001',
        cpf='12345678900',
        birth_date='1985-03-20',
    ),
    document=InlineDocument(content=document_base64, filename='consignado.pdf'),
    fallback_document=FallbackDocument(
        image=cnh_photo_base64,
        type='CNH',
    ),
    metadata={'loanType': 'CONSIGNADO_INSS'},
))
tx, _ := client.Transactions.Create(ctx, &signdocs.CreateTransactionRequest{
    Purpose: signdocs.TransactionPurposeDocumentSignature,
    Policy:  signdocs.Policy{Profile: signdocs.PolicyProfileBiometricSerproAutoFallback},
    Signer: signdocs.Signer{
        Name:           "Maria Santos",
        UserExternalID: "user-001",
        CPF:            "12345678900",
        BirthDate:      "1985-03-20",
    },
    Document: &signdocs.DocumentInline{Content: documentBase64, Filename: "consignado.pdf"},
    FallbackDocument: &signdocs.FallbackDocument{
        Image: cnhPhotoBase64,
        Type:  "CNH",
    },
    Metadata: map[string]string{"loanType": "CONSIGNADO_INSS"},
})
Signer signer = new Signer("Maria Santos", "user-001");
signer.setCpf("12345678900");
signer.setBirthDate("1985-03-20");

CreateTransactionRequest request = new CreateTransactionRequest();
request.setPurpose("DOCUMENT_SIGNATURE");
request.setPolicy(new Policy("BIOMETRIC_SERPRO_AUTO_FALLBACK"));
request.setSigner(signer);
request.setDocument(new InlineDocument(documentBase64, "consignado.pdf"));
request.setFallbackDocument(new FallbackDocument(cnhPhotoBase64, "CNH"));
request.setMetadata(Map.of("loanType", "CONSIGNADO_INSS"));

Transaction tx = client.transactions().create(request);
$tx = $client->transactions->create(new CreateTransactionRequest(
    purpose: 'DOCUMENT_SIGNATURE',
    policy: new Policy(profile: 'BIOMETRIC_SERPRO_AUTO_FALLBACK'),
    signer: new Signer(
        name: 'Maria Santos',
        userExternalId: 'user-001',
        cpf: '12345678900',
        birthDate: '1985-03-20',
    ),
    document: ['content' => $documentBase64, 'filename' => 'consignado.pdf'],
    fallbackDocument: ['image' => $cnhPhotoBase64, 'type' => 'CNH'],
    metadata: ['loanType' => 'CONSIGNADO_INSS'],
));
using SignDocsBrasil.Api;
using SignDocsBrasil.Api.Models;

var tx = await client.Transactions.CreateAsync(new CreateTransactionRequest
{
    Purpose = "DOCUMENT_SIGNATURE",
    Policy = new Policy { Profile = "BIOMETRIC_SERPRO_AUTO_FALLBACK" },
    Signer = new Signer
    {
        Name = "Maria Santos",
        UserExternalId = "user-001",
        Cpf = "12345678900",
        BirthDate = "1985-03-20",
    },
    Document = new InlineDocument { Content = documentBase64, Filename = "consignado.pdf" },
    FallbackDocument = new FallbackDocument
    {
        Image = cnhPhotoBase64,
        Type = "CNH",
    },
    Metadata = new Dictionary<string, string> { ["loanType"] = "CONSIGNADO_INSS" },
});
// tx.Status == "DOCUMENT_UPLOADED"

Nota: o campo fallbackDocument é opcional. Se omitido e o fallback for acionado, você deverá enviar a foto do documento manualmente ao completar a etapa DOCUMENT_PHOTO_MATCH.


Passo 2: Listar etapas

O perfil gera 5 etapas. A etapa DOCUMENT_PHOTO_MATCH inicia em status DORMANT — ela só será ativada automaticamente se o SERPRO não possuir imagem biométrica do beneficiário.

  1. PURPOSE_DISCLOSURE — consentimento informado (auto-injetada) → PENDING
  2. BIOMETRIC_LIVENESS — prova de vida → PENDING
  3. BIOMETRIC_MATCH — comparação biométrica → PENDING
  4. SERPRO_IDENTITY_CHECK — validação na base do SERPRO → PENDING
  5. DOCUMENT_PHOTO_MATCH — fallback por documento → DORMANT
const steps = await client.steps.list(tx.transactionId);
// steps[0].type === 'PURPOSE_DISCLOSURE'    → status: 'PENDING'
// steps[1].type === 'BIOMETRIC_LIVENESS'    → status: 'PENDING'
// steps[2].type === 'BIOMETRIC_MATCH'       → status: 'PENDING'
// steps[3].type === 'SERPRO_IDENTITY_CHECK'  → status: 'PENDING'
// steps[4].type === 'DOCUMENT_PHOTO_MATCH'   → status: 'DORMANT'

const dormantStep = steps.find(s => s.type === 'DOCUMENT_PHOTO_MATCH');
console.log(dormantStep.fallbackDocumentPreUploaded); // true (se pré-enviado)
steps = client.steps.list(tx.transaction_id)
# steps[0].type == 'PURPOSE_DISCLOSURE'    → status: 'PENDING'
# steps[1].type == 'BIOMETRIC_LIVENESS'    → status: 'PENDING'
# steps[2].type == 'BIOMETRIC_MATCH'       → status: 'PENDING'
# steps[3].type == 'SERPRO_IDENTITY_CHECK'  → status: 'PENDING'
# steps[4].type == 'DOCUMENT_PHOTO_MATCH'   → status: 'DORMANT'

dormant = next(s for s in steps if s.type == 'DOCUMENT_PHOTO_MATCH')
print(dormant.fallback_document_pre_uploaded)  # True (se pré-enviado)
steps, _ := client.Steps.List(ctx, tx.TransactionID)
// steps[0].Type == "PURPOSE_DISCLOSURE"    → Status: "PENDING"
// steps[1].Type == "BIOMETRIC_LIVENESS"    → Status: "PENDING"
// steps[2].Type == "BIOMETRIC_MATCH"       → Status: "PENDING"
// steps[3].Type == "SERPRO_IDENTITY_CHECK"  → Status: "PENDING"
// steps[4].Type == "DOCUMENT_PHOTO_MATCH"   → Status: "DORMANT"
List<Step> steps = client.steps().list(tx.getTransactionId());
// steps.get(0).getType() == "PURPOSE_DISCLOSURE"    → "PENDING"
// steps.get(1).getType() == "BIOMETRIC_LIVENESS"    → "PENDING"
// steps.get(2).getType() == "BIOMETRIC_MATCH"       → "PENDING"
// steps.get(3).getType() == "SERPRO_IDENTITY_CHECK"  → "PENDING"
// steps.get(4).getType() == "DOCUMENT_PHOTO_MATCH"   → "DORMANT"
$steps = $client->steps->list($tx->transactionId);
// $steps[0]->type === 'PURPOSE_DISCLOSURE'    → 'PENDING'
// $steps[1]->type === 'BIOMETRIC_LIVENESS'    → 'PENDING'
// $steps[2]->type === 'BIOMETRIC_MATCH'       → 'PENDING'
// $steps[3]->type === 'SERPRO_IDENTITY_CHECK'  → 'PENDING'
// $steps[4]->type === 'DOCUMENT_PHOTO_MATCH'   → 'DORMANT'
var steps = await client.Steps.ListAsync(tx.TransactionId);
// steps[0].Type == "PURPOSE_DISCLOSURE"    → Status: "PENDING"
// steps[1].Type == "BIOMETRIC_LIVENESS"    → Status: "PENDING"
// steps[2].Type == "BIOMETRIC_MATCH"       → Status: "PENDING"
// steps[3].Type == "SERPRO_IDENTITY_CHECK"  → Status: "PENDING"
// steps[4].Type == "DOCUMENT_PHOTO_MATCH"   → Status: "DORMANT"

var dormantStep = steps.First(s => s.Type == "DOCUMENT_PHOTO_MATCH");
Console.WriteLine($"Fallback pre-uploaded: {dormantStep.FallbackDocumentPreUploaded}");

Importante: Etapas com status DORMANT não podem ser iniciadas manualmente — o SDK lança exceção (HTTP 409 Conflict). A ativação ocorre automaticamente quando o SERPRO indica ausência de biometria.


Passo 3: Completar PURPOSE_DISCLOSURE

Antes de iniciar a biometria, o beneficiário deve receber e reconhecer a divulgação de finalidade. Inicie a etapa (a API envia a notificação) e complete com acknowledged: true.

const disclosureStep = steps.find(s => s.type === 'PURPOSE_DISCLOSURE');
await client.steps.start(tx.transactionId, disclosureStep.stepId);
const disclosure = await client.steps.complete(tx.transactionId, disclosureStep.stepId, {
  acknowledged: true,
});
// disclosure.result.purposeDisclosure.disclosureTextHash → SHA-256 do texto exibido
disclosure_step = next(s for s in steps if s.type == 'PURPOSE_DISCLOSURE')
client.steps.start(tx.transaction_id, disclosure_step.step_id)
disclosure = client.steps.complete(
    tx.transaction_id,
    disclosure_step.step_id,
    CompletePurposeDisclosureRequest(acknowledged=True),
)
# disclosure.result.purpose_disclosure.disclosure_text_hash
var disclosureStep signdocs.Step
for _, s := range steps {
    if s.Type == signdocs.StepTypePurposeDisclosure {
        disclosureStep = s
        break
    }
}
client.Steps.Start(ctx, tx.TransactionID, disclosureStep.StepID, nil)
disclosure, _ := client.Steps.Complete(ctx, tx.TransactionID, disclosureStep.StepID,
    &signdocs.CompletePurposeDisclosureRequest{Acknowledged: true})
Step disclosureStep = steps.stream()
    .filter(s -> "PURPOSE_DISCLOSURE".equals(s.getType()))
    .findFirst().orElseThrow();

client.steps().start(tx.getTransactionId(), disclosureStep.getStepId());
Step disclosure = client.steps().complete(tx.getTransactionId(), disclosureStep.getStepId(),
    Map.of("acknowledged", true));
$disclosureStep = current(array_filter($steps, fn($s) => $s->type === 'PURPOSE_DISCLOSURE'));
$client->steps->start($tx->transactionId, $disclosureStep->stepId);
$disclosure = $client->steps->complete($tx->transactionId, $disclosureStep->stepId, [
    'acknowledged' => true,
]);
var disclosureStep = steps.First(s => s.Type == "PURPOSE_DISCLOSURE");
await client.Steps.StartAsync(tx.TransactionId, disclosureStep.StepId);
var disclosure = await client.Steps.CompleteAsync(tx.TransactionId, disclosureStep.StepId,
    new { Acknowledged = true });
// disclosure.Result.PurposeDisclosure.DisclosureTextHash → SHA-256 do texto exibido

Passo 4: Iniciar BIOMETRIC_LIVENESS

Inicie a prova de vida escolhendo HOSTED_PAGE ou BANK_APP como modo de captura.

const livenessStep = steps.find(s => s.type === 'BIOMETRIC_LIVENESS');
const session = await client.steps.start(tx.transactionId, livenessStep.stepId, {
  captureMode: 'HOSTED_PAGE',
});
// session.hostedUrl → URL para redirecionar o beneficiário
// session.livenessSessionId → ID da sessão para completar
liveness_step = next(s for s in steps if s.type == 'BIOMETRIC_LIVENESS')
session = client.steps.start(
    tx.transaction_id,
    liveness_step.step_id,
    StartStepRequest(capture_mode='HOSTED_PAGE'),
)
# session.hosted_url → URL para redirecionar
# session.liveness_session_id → ID para completar
var livenessStep signdocs.Step
for _, s := range steps {
    if s.Type == signdocs.StepTypeBiometricLive {
        livenessStep = s
        break
    }
}
session, _ := client.Steps.Start(ctx, tx.TransactionID, livenessStep.StepID,
    &signdocs.StartStepRequest{CaptureMode: signdocs.CaptureModeHostedPage})
// session.HostedURL, session.LivenessSessionID
Step livenessStep = steps.stream()
    .filter(s -> "BIOMETRIC_LIVENESS".equals(s.getType()))
    .findFirst().orElseThrow();

StartStepResponse session = client.steps().start(tx.getTransactionId(), livenessStep.getStepId(),
    new StartStepRequest("HOSTED_PAGE"));
// session.getHostedUrl(), session.getLivenessSessionId()
$livenessStep = current(array_filter($steps, fn($s) => $s->type === 'BIOMETRIC_LIVENESS'));
$session = $client->steps->start($tx->transactionId, $livenessStep->stepId, [
    'captureMode' => 'HOSTED_PAGE',
]);
// $session->hostedUrl, $session->livenessSessionId
var livenessStep = steps.First(s => s.Type == "BIOMETRIC_LIVENESS");
var session = await client.Steps.StartAsync(tx.TransactionId, livenessStep.StepId,
    new StartStepRequest { CaptureMode = "HOSTED_PAGE" });
// session.HostedUrl → URL para redirecionar o beneficiário
// session.LivenessSessionId → ID da sessão para completar

Passo 5: Completar BIOMETRIC_LIVENESS com geolocalização

A NT65 exige geolocalização em todas as etapas biométricas. Inclua o objeto geolocation ao completar.

const liveness = await client.steps.complete(tx.transactionId, livenessStep.stepId, {
  livenessSessionId: session.livenessSessionId,
  geolocation: {
    latitude: -23.5505,
    longitude: -46.6333,
    accuracy: 10.5,
    source: 'GPS',
  },
});
// liveness.result.liveness.confidence → ex: 99.7
// liveness.result.liveness.complianceStandards → ["NT65", "ISO_30107_3"]
liveness = client.steps.complete(
    tx.transaction_id,
    liveness_step.step_id,
    CompleteLivenessRequest(
        liveness_session_id=session.liveness_session_id,
        geolocation=Geolocation(
            latitude=-23.5505,
            longitude=-46.6333,
            accuracy=10.5,
            source='GPS',
        ),
    ),
)
# liveness.result.liveness.compliance_standards → ["NT65", "ISO_30107_3"]
accuracy := 10.5
liveness, _ := client.Steps.Complete(ctx, tx.TransactionID, livenessStep.StepID,
    &signdocs.CompleteLivenessRequest{
        LivenessSessionID: session.LivenessSessionID,
        Geolocation: &signdocs.Geolocation{
            Latitude:  -23.5505,
            Longitude: -46.6333,
            Accuracy:  &accuracy,
            Source:    signdocs.GeolocationSourceGPS,
        },
    })
Map<String, Object> body = Map.of(
    "livenessSessionId", session.getLivenessSessionId(),
    "geolocation", Map.of(
        "latitude", -23.5505,
        "longitude", -46.6333,
        "accuracy", 10.5,
        "source", "GPS"
    )
);
Step liveness = client.steps().complete(tx.getTransactionId(), livenessStep.getStepId(), body);
$liveness = $client->steps->complete($tx->transactionId, $livenessStep->stepId, [
    'livenessSessionId' => $session->livenessSessionId,
    'geolocation' => [
        'latitude' => -23.5505,
        'longitude' => -46.6333,
        'accuracy' => 10.5,
        'source' => 'GPS',
    ],
]);
var liveness = await client.Steps.CompleteAsync(tx.TransactionId, livenessStep.StepId,
    new
    {
        LivenessSessionId = session.LivenessSessionId,
        Geolocation = new { Latitude = -23.5505, Longitude = -46.6333, Accuracy = 10.5, Source = "GPS" },
    });
// liveness.Result.Liveness.Confidence → ex: 99.7
// liveness.Result.Liveness.ComplianceStandards → ["NT65", "ISO_30107_3"]

Passo 6: Completar BIOMETRIC_MATCH com geolocalização

const matchStep = steps.find(s => s.type === 'BIOMETRIC_MATCH');
await client.steps.start(tx.transactionId, matchStep.stepId);
const match = await client.steps.complete(tx.transactionId, matchStep.stepId, {
  geolocation: {
    latitude: -23.5505,
    longitude: -46.6333,
    accuracy: 10.5,
    source: 'GPS',
  },
});
// match.result.match.similarity → ex: 99.2
match_step = next(s for s in steps if s.type == 'BIOMETRIC_MATCH')
client.steps.start(tx.transaction_id, match_step.step_id)
match = client.steps.complete(
    tx.transaction_id,
    match_step.step_id,
    CompleteBiometricMatchRequest(
        geolocation=Geolocation(latitude=-23.5505, longitude=-46.6333, accuracy=10.5, source='GPS'),
    ),
)
var matchStep signdocs.Step
for _, s := range steps {
    if s.Type == signdocs.StepTypeBiometricMatch {
        matchStep = s
        break
    }
}
client.Steps.Start(ctx, tx.TransactionID, matchStep.StepID, nil)
match, _ := client.Steps.Complete(ctx, tx.TransactionID, matchStep.StepID,
    &signdocs.CompleteBiometricMatchRequest{
        Geolocation: &signdocs.Geolocation{
            Latitude: -23.5505, Longitude: -46.6333, Accuracy: &accuracy, Source: signdocs.GeolocationSourceGPS,
        },
    })
Step matchStep = steps.stream()
    .filter(s -> "BIOMETRIC_MATCH".equals(s.getType()))
    .findFirst().orElseThrow();

client.steps().start(tx.getTransactionId(), matchStep.getStepId());
Step match = client.steps().complete(tx.getTransactionId(), matchStep.getStepId(), Map.of(
    "geolocation", Map.of("latitude", -23.5505, "longitude", -46.6333, "accuracy", 10.5, "source", "GPS")
));
$matchStep = current(array_filter($steps, fn($s) => $s->type === 'BIOMETRIC_MATCH'));
$client->steps->start($tx->transactionId, $matchStep->stepId);
$match = $client->steps->complete($tx->transactionId, $matchStep->stepId, [
    'geolocation' => ['latitude' => -23.5505, 'longitude' => -46.6333, 'accuracy' => 10.5, 'source' => 'GPS'],
]);
var matchStep = steps.First(s => s.Type == "BIOMETRIC_MATCH");
await client.Steps.StartAsync(tx.TransactionId, matchStep.StepId);
var match = await client.Steps.CompleteAsync(tx.TransactionId, matchStep.StepId,
    new
    {
        Geolocation = new { Latitude = -23.5505, Longitude = -46.6333, Accuracy = 10.5, Source = "GPS" },
    });
// match.Result.Match.Similarity → ex: 99.2

Passo 7: SERPRO_IDENTITY_CHECK — três desfechos possíveis

A verificação SERPRO é processada automaticamente (server-side). Basta iniciar e completar. A resposta indica um de três desfechos:

Desfecho status fallback Próximo passo
Sucesso COMPLETED ausente Finalizar transação (etapa DORMANT ignorada)
Biometria indisponível SKIPPED { triggered: true, ... } Completar DOCUMENT_PHOTO_MATCH (Passo 8)
Falha STARTED ou FAILED ausente Corrigir dados e retentar, ou abortar
const serproStep = steps.find(s => s.type === 'SERPRO_IDENTITY_CHECK');
await client.steps.start(tx.transactionId, serproStep.stepId);
const serpro = await client.steps.complete(tx.transactionId, serproStep.stepId);

if (serpro.status === 'COMPLETED') {
  // Sucesso — finalizar transação diretamente
  console.log('SERPRO validou:', serpro.result.serproIdentity.biometricConfidence);
} else if (serpro.status === 'SKIPPED' && serpro.fallback?.triggered) {
  // Fallback acionado — completar DOCUMENT_PHOTO_MATCH
  const fallbackStepId = serpro.fallback.nextStepId;
  console.log('Fallback acionado, completar etapa:', fallbackStepId);
  console.log('Documento pré-enviado:', serpro.fallback.fallbackDocumentPreUploaded);
} else {
  // Falha — dados incorretos, retentar ou abortar
  console.error('SERPRO falhou:', serpro.status);
}
serpro_step = next(s for s in steps if s.type == 'SERPRO_IDENTITY_CHECK')
client.steps.start(tx.transaction_id, serpro_step.step_id)
serpro = client.steps.complete(tx.transaction_id, serpro_step.step_id)

if serpro.status == 'COMPLETED':
    # Sucesso — finalizar transação
    print('SERPRO validou:', serpro.result.serpro_identity.biometric_confidence)
elif serpro.status == 'SKIPPED' and getattr(serpro, 'fallback', None) and serpro.fallback.triggered:
    # Fallback acionado — completar DOCUMENT_PHOTO_MATCH
    fallback_step_id = serpro.fallback.next_step_id
    print(f'Fallback acionado, completar etapa: {fallback_step_id}')
else:
    # Falha — dados incorretos
    print(f'SERPRO falhou: {serpro.status}')
var serproStep signdocs.Step
for _, s := range steps {
    if s.Type == signdocs.StepTypeSerproIdentity {
        serproStep = s
        break
    }
}
client.Steps.Start(ctx, tx.TransactionID, serproStep.StepID, nil)
serpro, _ := client.Steps.Complete(ctx, tx.TransactionID, serproStep.StepID, nil)

switch serpro.Status {
case "COMPLETED":
    // Sucesso — finalizar transação
    fmt.Println("SERPRO validou:", serpro.Result.SerproIdentity.BiometricConfidence)
case "SKIPPED":
    if serpro.Fallback != nil && serpro.Fallback.Triggered {
        // Fallback acionado
        fmt.Println("Fallback acionado, completar etapa:", serpro.Fallback.NextStepID)
    }
default:
    // Falha — dados incorretos
    fmt.Println("SERPRO falhou:", serpro.Status)
}
Step serproStep = steps.stream()
    .filter(s -> "SERPRO_IDENTITY_CHECK".equals(s.getType()))
    .findFirst().orElseThrow();

client.steps().start(tx.getTransactionId(), serproStep.getStepId());
Step serpro = client.steps().complete(tx.getTransactionId(), serproStep.getStepId());

if ("COMPLETED".equals(serpro.getStatus())) {
    // Sucesso — finalizar transação
    System.out.println("SERPRO validou: " + serpro.getResult().getSerproIdentity().getBiometricConfidence());
} else if ("SKIPPED".equals(serpro.getStatus()) && serpro.getFallback() != null && serpro.getFallback().isTriggered()) {
    // Fallback acionado
    String fallbackStepId = serpro.getFallback().getNextStepId();
    System.out.println("Fallback acionado, completar etapa: " + fallbackStepId);
} else {
    // Falha
    System.err.println("SERPRO falhou: " + serpro.getStatus());
}
$serproStep = current(array_filter($steps, fn($s) => $s->type === 'SERPRO_IDENTITY_CHECK'));
$client->steps->start($tx->transactionId, $serproStep->stepId);
$serpro = $client->steps->complete($tx->transactionId, $serproStep->stepId);

if ($serpro->status === 'COMPLETED') {
    // Sucesso — finalizar transação
    echo 'SERPRO validou: ' . $serpro->result->serproIdentity['biometricConfidence'];
} elseif ($serpro->status === 'SKIPPED' && ($serpro->fallback->triggered ?? false)) {
    // Fallback acionado
    $fallbackStepId = $serpro->fallback->nextStepId;
    echo "Fallback acionado, completar etapa: $fallbackStepId";
} else {
    // Falha
    echo 'SERPRO falhou: ' . $serpro->status;
}
var serproStep = steps.First(s => s.Type == "SERPRO_IDENTITY_CHECK");
await client.Steps.StartAsync(tx.TransactionId, serproStep.StepId);
var serpro = await client.Steps.CompleteAsync(tx.TransactionId, serproStep.StepId);

if (serpro.Status == "COMPLETED")
{
    // Sucesso — finalizar transação diretamente
    Console.WriteLine($"SERPRO validou: {serpro.Result.SerproIdentity.BiometricConfidence}");
}
else if (serpro.Status == "SKIPPED" && serpro.Fallback?.Triggered == true)
{
    // Fallback acionado — completar DOCUMENT_PHOTO_MATCH
    var fallbackStepId = serpro.Fallback.NextStepId;
    Console.WriteLine($"Fallback acionado, completar etapa: {fallbackStepId}");
    Console.WriteLine($"Documento pré-enviado: {serpro.Fallback.FallbackDocumentPreUploaded}");
}
else
{
    // Falha — dados incorretos, retentar ou abortar
    Console.WriteLine($"SERPRO falhou: {serpro.Status}");
}

Resposta — Sucesso (SERPRO validou):

{
  "status": "COMPLETED",
  "result": {
    "serproIdentity": {
      "valid": true,
      "biometricMatch": true,
      "biometricConfidence": 0.98,
      "nameMatch": true,
      "birthDateMatch": true
    }
  }
}

Resposta — Biometria indisponível (fallback acionado):

{
  "status": "SKIPPED",
  "fallback": {
    "triggered": true,
    "nextStepType": "DOCUMENT_PHOTO_MATCH",
    "nextStepId": "step_abc123",
    "fallbackDocumentPreUploaded": true
  }
}

Passo 8: DOCUMENT_PHOTO_MATCH (quando acionado por fallback)

Esta etapa só é relevante quando o SERPRO retorna SKIPPED com fallback.triggered: true. A etapa DOCUMENT_PHOTO_MATCH muda automaticamente de DORMANT para PENDING.

Se você pré-enviou fallbackDocument na criação da transação: basta indicar o documentType ao completar — a imagem pré-enviada será utilizada.

Se não pré-enviou: envie documentImage (base64) junto com documentType.

TypeScript — com documento pré-enviado

const fallbackStepId = serpro.fallback.nextStepId;
await client.steps.start(tx.transactionId, fallbackStepId);
const docMatch = await client.steps.complete(tx.transactionId, fallbackStepId, {
  documentType: 'CNH',
  geolocation: {
    latitude: -23.5505,
    longitude: -46.6333,
    accuracy: 10.5,
    source: 'GPS',
  },
});
// docMatch.result.documentPhotoMatch.similarity → ex: 97.5
// docMatch.result.documentPhotoMatch.documentType → "CNH"

TypeScript — sem documento pré-enviado

const docMatch = await client.steps.complete(tx.transactionId, fallbackStepId, {
  documentImage: documentPhotoBase64,
  documentType: 'CNH',
  geolocation: {
    latitude: -23.5505,
    longitude: -46.6333,
    accuracy: 10.5,
    source: 'GPS',
  },
});
fallback_step_id = serpro.fallback.next_step_id
client.steps.start(tx.transaction_id, fallback_step_id)

# Com fallbackDocument pré-enviado:
doc_match = client.steps.complete(
    tx.transaction_id,
    fallback_step_id,
    CompleteDocumentPhotoMatchRequest(
        document_type='CNH',
        geolocation=Geolocation(latitude=-23.5505, longitude=-46.6333, accuracy=10.5, source='GPS'),
    ),
)

# Sem pré-envio — incluir document_image:
# CompleteDocumentPhotoMatchRequest(
#     document_image=document_photo_base64,
#     document_type='CNH',
#     geolocation=Geolocation(...),
# )
fallbackStepID := serpro.Fallback.NextStepID
client.Steps.Start(ctx, tx.TransactionID, fallbackStepID, nil)

// Com fallbackDocument pré-enviado:
docMatch, _ := client.Steps.Complete(ctx, tx.TransactionID, fallbackStepID,
    &signdocs.CompleteDocumentPhotoMatchRequest{
        DocumentType: "CNH",
        Geolocation: &signdocs.Geolocation{
            Latitude: -23.5505, Longitude: -46.6333, Accuracy: &accuracy, Source: signdocs.GeolocationSourceGPS,
        },
    })

// Sem pré-envio — incluir DocumentImage:
// &signdocs.CompleteDocumentPhotoMatchRequest{
//     DocumentImage: documentPhotoBase64,
//     DocumentType:  "CNH",
//     Geolocation:   &signdocs.Geolocation{...},
// }
String fallbackStepId = serpro.getFallback().getNextStepId();
client.steps().start(tx.getTransactionId(), fallbackStepId);

// Com fallbackDocument pré-enviado:
Step docMatch = client.steps().complete(tx.getTransactionId(), fallbackStepId, Map.of(
    "documentType", "CNH",
    "geolocation", Map.of("latitude", -23.5505, "longitude", -46.6333, "accuracy", 10.5, "source", "GPS")
));

// Sem pré-envio — incluir documentImage:
// Map.of("documentImage", documentPhotoBase64, "documentType", "CNH", "geolocation", Map.of(...))
$fallbackStepId = $serpro->fallback->nextStepId;
$client->steps->start($tx->transactionId, $fallbackStepId);

// Com fallbackDocument pré-enviado:
$docMatch = $client->steps->complete($tx->transactionId, $fallbackStepId, [
    'documentType' => 'CNH',
    'geolocation' => ['latitude' => -23.5505, 'longitude' => -46.6333, 'accuracy' => 10.5, 'source' => 'GPS'],
]);

// Sem pré-envio — incluir documentImage:
// ['documentImage' => $documentPhotoBase64, 'documentType' => 'CNH', 'geolocation' => [...]]
var fallbackStepId = serpro.Fallback.NextStepId;
await client.Steps.StartAsync(tx.TransactionId, fallbackStepId);

// Com fallbackDocument pré-enviado:
var docMatch = await client.Steps.CompleteAsync(tx.TransactionId, fallbackStepId,
    new
    {
        DocumentType = "CNH",
        Geolocation = new { Latitude = -23.5505, Longitude = -46.6333, Accuracy = 10.5, Source = "GPS" },
    });
// docMatch.Result.DocumentPhotoMatch.Similarity → ex: 97.5
// docMatch.Result.DocumentPhotoMatch.DocumentType → "CNH"

// Sem pré-envio — incluir DocumentImage:
// new { DocumentImage = documentPhotoBase64, DocumentType = "CNH", Geolocation = new { ... } }

Passo 9: Finalizar e obter evidências

Após completar todas as etapas ativas, finalize a transação. Etapas com status DORMANT são excluídas automaticamente da evidência (não bloqueiam a finalização). A resposta inclui submissionDeadline (prazo de 7 dias úteis para submissão ao INSS).

const finalized = await client.transactions.finalize(tx.transactionId);
// finalized.status → "COMPLETED"
// finalized.submissionDeadline → "2026-03-12T23:59:59Z"
// finalized.deadlineStatus → "PENDING"
// finalized.evidenceId → "ev_..."

const evidence = await client.evidence.get(tx.transactionId);
finalized = client.transactions.finalize(tx.transaction_id)
# finalized.status → "COMPLETED"
# finalized.submission_deadline → "2026-03-12T23:59:59Z"
# finalized.deadline_status → "PENDING"

evidence = client.evidence.get(tx.transaction_id)
finalized, _ := client.Transactions.Finalize(ctx, tx.TransactionID)
// finalized.Status, finalized.SubmissionDeadline, finalized.DeadlineStatus

evidence, _ := client.Evidence.Get(ctx, tx.TransactionID)
Transaction finalized = client.transactions().finalize(tx.getTransactionId());
// finalized.getStatus(), finalized.getSubmissionDeadline(), finalized.getDeadlineStatus()

Evidence evidence = client.evidence().get(tx.getTransactionId());
$finalized = $client->transactions->finalize($tx->transactionId);
// $finalized->status, $finalized->submissionDeadline, $finalized->deadlineStatus

$evidence = $client->evidence->get($tx->transactionId);
var finalized = await client.Transactions.FinalizeAsync(tx.TransactionId);
// finalized.Status → "COMPLETED"
// finalized.SubmissionDeadline → "2026-03-12T23:59:59Z"
// finalized.DeadlineStatus → "PENDING"
// finalized.EvidenceId → "ev_..."

var evidence = await client.Evidence.GetAsync(tx.TransactionId);

Geolocalização obrigatória

A NT65 exige geolocalização em todas as etapas biométricas (BIOMETRIC_LIVENESS, BIOMETRIC_MATCH, DOCUMENT_PHOTO_MATCH). O objeto geolocation aceita:

Campo Tipo Obrigatório Descrição
latitude number Sim -90 a 90
longitude number Sim -180 a 180
accuracy number Não Precisão em metros
source string Não GPS, IP, WIFI ou CELL

Se a geolocalização não for enviada em etapas NT65, a API retorna erro 422 com código GEOLOCATION_REQUIRED.


Prazo de submissão de 7 dias úteis

Após a finalização, a transação recebe um submissionDeadline (prazo de 7 dias úteis para submissão ao INSS). O campo deadlineStatus pode ser:

Status Descrição
PENDING Dentro do prazo
APPROACHING Faltam 2 dias úteis ou menos
OVERDUE Prazo expirado

Webhooks NT65

Além dos webhooks padrão, o fluxo NT65 emite:

Evento Quando
STEP.PURPOSE_DISCLOSURE_SENT Notificação de divulgação enviada ao beneficiário
TRANSACTION.DEADLINE_APPROACHING Faltam 2 dias úteis para o prazo de submissão
TRANSACTION.FALLBACK Fallback acionado automaticamente (SERPRO sem biometria)

O evento TRANSACTION.FALLBACK inclui fallbackReason, fallbackStepId e fallbackDocumentPreUploaded no payload.


Campos obrigatórios

Campo Obrigatório Condicional
signer.cpf Sim —
signer.birthDate Sim —
signer.name Sim —
geolocation (liveness) Sim —
geolocation (match) Sim —
geolocation (doc match) — Sim (quando fallback acionado)
documentImage — Sim (quando fallback acionado e fallbackDocument não pré-enviado)
documentType — Sim (quando fallback acionado)
fallbackDocument.image — Opcional (na criação da transação)
fallbackDocument.type — Obrigatório se fallbackDocument.image for enviado