Guias de Desenvolvimento

Signing Sessions

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: pdfBase64, 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=pdf_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: pdfBase64, 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(pdfBase64, "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' => $pdfBase64, 'filename' => 'consignado.pdf'],
    fallbackDocument: ['image' => $cnhPhotoBase64, 'type' => 'CNH'],
    metadata: ['loanType' => 'CONSIGNADO_INSS'],
));

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'

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,
]);

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

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',
    ],
]);

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'],
]);

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;
}

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' => [...]]

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);

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