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:
nt65ComplianceEnabled: true, serpro.enabled: true, documentExtractionEnabled: truecpf, birthDateO 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 etapaDOCUMENT_PHOTO_MATCH.
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.
PURPOSE_DISCLOSURE — consentimento informado (auto-injetada) → PENDINGBIOMETRIC_LIVENESS — prova de vida → PENDINGBIOMETRIC_MATCH — comparação biométrica → PENDINGSERPRO_IDENTITY_CHECK — validação na base do SERPRO → PENDINGDOCUMENT_PHOTO_MATCH — fallback por documento → DORMANTconst 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
DORMANTnão podem ser iniciadas manualmente — o SDK lança exceção (HTTP409 Conflict). A ativação ocorre automaticamente quando o SERPRO indica ausência de biometria.
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,
]);
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
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',
],
]);
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'],
]);
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
}
}
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.
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"
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' => [...]]
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);
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.
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 |
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.
| 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 |