Guias de Desenvolvimento

Signing Sessions

Guias dos SDKs

Guia NT65 — Biometria para Consignado INSS

Fluxo completo de autenticação biométrica em conformidade com a Nota Técnica 65/2023 (INSS/Dataprev) para contratos de empréstimo consignado.

Sumário

  1. Visão geral da NT65
  2. Pré-requisitos
  3. Fluxo completo: BIOMETRIC_SERPRO_AUTO_FALLBACK
  4. Geolocalização obrigatória
  5. Prazo de submissão de 7 dias úteis
  6. Webhooks NT65
  7. Campos obrigatórios
  8. Tratamento de erros NT65
  9. Diagrama de decisão
  10. Checklist de conformidade

Convenções

Item Valor
Base URL (HML) https://api-hml.signdocs.com.br
Autenticação OAuth2 client_credentials — Bearer JWT
Content-Type application/json
Formato de erros RFC 7807 application/problem+json

1. Visão geral da NT65

A NT65/2023 estabelece 15 requisitos técnicos para autenticação biométrica em consignados INSS:

A API implementa um único perfil consolidado para conformidade NT65:

Perfil Descrição
BIOMETRIC_SERPRO_AUTO_FALLBACK Verificação contra base SERPRO com fallback automático por documento. Se o SERPRO não possui imagem biométrica do beneficiário, a etapa DOCUMENT_PHOTO_MATCH é ativada automaticamente — sem necessidade de criar uma segunda transação.
Nota: o perfil BIOMETRIC_SERPRO_AUTO_FALLBACK substitui a abordagem anterior de dois perfis separados (BIOMETRIC_SERPRO + BIOMETRIC_DOCUMENT_FALLBACK). Todo o fluxo é resolvido em uma única transação.

2. Pré-requisitos

export BASE_URL="https://api-hml.signdocs.com.br"
export TOKEN="seu_jwt_token"

3. Fluxo completo: BIOMETRIC_SERPRO_AUTO_FALLBACK

3.1 Criar transação

O perfil BIOMETRIC_SERPRO_AUTO_FALLBACK exige cpf e birthDate no signer. A API injeta automaticamente a etapa PURPOSE_DISCLOSURE e cria uma etapa DOCUMENT_PHOTO_MATCH em status DORMANT (dormante — ativada automaticamente se necessário).

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

cURL

TX=$(curl -s -X POST "$BASE_URL/v1/transactions" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: nt65-$(date +%s)" \
  -d '{
    "purpose": "DOCUMENT_SIGNATURE",
    "policy": { "profile": "BIOMETRIC_SERPRO_AUTO_FALLBACK" },
    "signer": {
      "name": "Maria Santos",
      "userExternalId": "user-001",
      "cpf": "12345678900",
      "birthDate": "1985-03-20"
    },
    "document": {
      "content": "'$(base64 -w0 consignado.pdf)'",
      "filename": "consignado.pdf"
    },
    "fallbackDocument": {
      "image": "'$(base64 -w0 cnh_foto.jpg)'",
      "type": "CNH"
    },
    "metadata": { "loanType": "CONSIGNADO_INSS" }
  }')

TX_ID=$(echo "$TX" | jq -r '.transactionId')
echo "Transação criada: $TX_ID"

Node.js (fetch)

const txRes = await fetch(`${BASE_URL}/v1/transactions`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${TOKEN}`,
    "Content-Type": "application/json",
    "X-Idempotency-Key": `nt65-${Date.now()}`,
  },
  body: JSON.stringify({
    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" },
  }),
});
const tx = await txRes.json();

Python (requests)

import requests, base64

tx = requests.post(f"{BASE_URL}/v1/transactions", headers={
    "Authorization": f"Bearer {TOKEN}",
    "Content-Type": "application/json",
    "X-Idempotency-Key": f"nt65-{int(time.time())}",
}, json={
    "purpose": "DOCUMENT_SIGNATURE",
    "policy": {"profile": "BIOMETRIC_SERPRO_AUTO_FALLBACK"},
    "signer": {
        "name": "Maria Santos",
        "userExternalId": "user-001",
        "cpf": "12345678900",
        "birthDate": "1985-03-20",
    },
    "document": {"content": pdf_base64, "filename": "consignado.pdf"},
    "fallbackDocument": {
        "image": cnh_photo_base64,
        "type": "CNH",
    },
    "metadata": {"loanType": "CONSIGNADO_INSS"},
}).json()

tx_id = tx["transactionId"]
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.

3.2 Listar etapas

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

cURL

STEPS=$(curl -s "$BASE_URL/v1/transactions/$TX_ID/steps" \
  -H "Authorization: Bearer $TOKEN")

echo "$STEPS" | jq '.steps[] | {type, status}'
# { "type": "PURPOSE_DISCLOSURE",    "status": "PENDING" }
# { "type": "BIOMETRIC_LIVENESS",    "status": "PENDING" }
# { "type": "BIOMETRIC_MATCH",       "status": "PENDING" }
# { "type": "SERPRO_IDENTITY_CHECK",  "status": "PENDING" }
# { "type": "DOCUMENT_PHOTO_MATCH",   "status": "DORMANT" }  <- ativada automaticamente se necessário

Node.js (fetch)

const stepsRes = await fetch(`${BASE_URL}/v1/transactions/${tx.transactionId}/steps`, {
  headers: { Authorization: `Bearer ${TOKEN}` },
});
const { steps } = await stepsRes.json();

// 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

// Se fallbackDocument foi pré-enviado:
const dormantStep = steps.find((s) => s.type === "DOCUMENT_PHOTO_MATCH");
console.log(dormantStep.fallbackDocumentPreUploaded); // true

Python (requests)

steps_res = requests.get(
    f"{BASE_URL}/v1/transactions/{tx_id}/steps",
    headers={"Authorization": f"Bearer {TOKEN}"},
).json()

steps = steps_res["steps"]
# 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"

dormant = next(s for s in steps if s["type"] == "DOCUMENT_PHOTO_MATCH")
print(dormant["fallbackDocumentPreUploaded"])  # True (se pré-enviado)
Importante: Etapas com status DORMANT não podem ser iniciadas manualmente — a API retorna 409 Conflict se você tentar. A ativação ocorre automaticamente quando o SERPRO indica ausência de biometria.

3.3 Completar PURPOSE_DISCLOSURE

Inicie a etapa (a API envia a notificação ao beneficiário) e complete com acknowledged: true.

cURL

DISCLOSURE_ID=$(echo "$STEPS" | jq -r '.steps[] | select(.type=="PURPOSE_DISCLOSURE") | .stepId')

# Iniciar — envia notificação ao beneficiário
curl -s -X POST "$BASE_URL/v1/transactions/$TX_ID/steps/$DISCLOSURE_ID/start" \
  -H "Authorization: Bearer $TOKEN"

# Completar — beneficiário reconheceu
curl -s -X POST "$BASE_URL/v1/transactions/$TX_ID/steps/$DISCLOSURE_ID/complete" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "acknowledged": true }'

Node.js (fetch)

const disclosureStep = steps.find((s) => s.type === "PURPOSE_DISCLOSURE");

await fetch(`${BASE_URL}/v1/transactions/${tx.transactionId}/steps/${disclosureStep.stepId}/start`, {
  method: "POST",
  headers: { Authorization: `Bearer ${TOKEN}` },
});

const disclosure = await fetch(
  `${BASE_URL}/v1/transactions/${tx.transactionId}/steps/${disclosureStep.stepId}/complete`,
  {
    method: "POST",
    headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
    body: JSON.stringify({ acknowledged: true }),
  }
).then((r) => r.json());

Python (requests)

disclosure_step = next(s for s in steps if s["type"] == "PURPOSE_DISCLOSURE")

requests.post(
    f"{BASE_URL}/v1/transactions/{tx_id}/steps/{disclosure_step['stepId']}/start",
    headers={"Authorization": f"Bearer {TOKEN}"},
)

disclosure = requests.post(
    f"{BASE_URL}/v1/transactions/{tx_id}/steps/{disclosure_step['stepId']}/complete",
    headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"},
    json={"acknowledged": True},
).json()

3.4 Iniciar e completar BIOMETRIC_LIVENESS com geolocalização

A NT65 exige geolocalização em todas as etapas biométricas.

cURL

LIVENESS_ID=$(echo "$STEPS" | jq -r '.steps[] | select(.type=="BIOMETRIC_LIVENESS") | .stepId')

# Iniciar
SESSION=$(curl -s -X POST "$BASE_URL/v1/transactions/$TX_ID/steps/$LIVENESS_ID/start" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "captureMode": "HOSTED_PAGE" }')

SESSION_ID=$(echo "$SESSION" | jq -r '.livenessSessionId')
HOSTED_URL=$(echo "$SESSION" | jq -r '.hostedUrl')
echo "Redirecionar beneficiário para: $HOSTED_URL"

# Completar (após captura facial)
curl -s -X POST "$BASE_URL/v1/transactions/$TX_ID/steps/$LIVENESS_ID/complete" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "livenessSessionId": "'$SESSION_ID'",
    "geolocation": {
      "latitude": -23.5505,
      "longitude": -46.6333,
      "accuracy": 10.5,
      "source": "GPS"
    }
  }'

Node.js (fetch)

const livenessStep = steps.find((s) => s.type === "BIOMETRIC_LIVENESS");

const session = await fetch(
  `${BASE_URL}/v1/transactions/${tx.transactionId}/steps/${livenessStep.stepId}/start`,
  {
    method: "POST",
    headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
    body: JSON.stringify({ captureMode: "HOSTED_PAGE" }),
  }
).then((r) => r.json());

// Após captura facial na hostedUrl...
const liveness = await fetch(
  `${BASE_URL}/v1/transactions/${tx.transactionId}/steps/${livenessStep.stepId}/complete`,
  {
    method: "POST",
    headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      livenessSessionId: session.livenessSessionId,
      geolocation: { latitude: -23.5505, longitude: -46.6333, accuracy: 10.5, source: "GPS" },
    }),
  }
).then((r) => r.json());

Python (requests)

liveness_step = next(s for s in steps if s["type"] == "BIOMETRIC_LIVENESS")

session = requests.post(
    f"{BASE_URL}/v1/transactions/{tx_id}/steps/{liveness_step['stepId']}/start",
    headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"},
    json={"captureMode": "HOSTED_PAGE"},
).json()

# Após captura facial na hosted_url...
liveness = requests.post(
    f"{BASE_URL}/v1/transactions/{tx_id}/steps/{liveness_step['stepId']}/complete",
    headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"},
    json={
        "livenessSessionId": session["livenessSessionId"],
        "geolocation": {"latitude": -23.5505, "longitude": -46.6333, "accuracy": 10.5, "source": "GPS"},
    },
).json()

3.5 Completar BIOMETRIC_MATCH com geolocalização

cURL

MATCH_ID=$(echo "$STEPS" | jq -r '.steps[] | select(.type=="BIOMETRIC_MATCH") | .stepId')

curl -s -X POST "$BASE_URL/v1/transactions/$TX_ID/steps/$MATCH_ID/start" \
  -H "Authorization: Bearer $TOKEN"

curl -s -X POST "$BASE_URL/v1/transactions/$TX_ID/steps/$MATCH_ID/complete" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "geolocation": {
      "latitude": -23.5505,
      "longitude": -46.6333,
      "accuracy": 10.5,
      "source": "GPS"
    }
  }'

Node.js (fetch)

const matchStep = steps.find((s) => s.type === "BIOMETRIC_MATCH");
await fetch(
  `${BASE_URL}/v1/transactions/${tx.transactionId}/steps/${matchStep.stepId}/start`,
  { method: "POST", headers: { Authorization: `Bearer ${TOKEN}` } }
);

const match = await fetch(
  `${BASE_URL}/v1/transactions/${tx.transactionId}/steps/${matchStep.stepId}/complete`,
  {
    method: "POST",
    headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      geolocation: { latitude: -23.5505, longitude: -46.6333, accuracy: 10.5, source: "GPS" },
    }),
  }
).then((r) => r.json());

Python (requests)

match_step = next(s for s in steps if s["type"] == "BIOMETRIC_MATCH")
requests.post(
    f"{BASE_URL}/v1/transactions/{tx_id}/steps/{match_step['stepId']}/start",
    headers={"Authorization": f"Bearer {TOKEN}"},
)

match = requests.post(
    f"{BASE_URL}/v1/transactions/{tx_id}/steps/{match_step['stepId']}/complete",
    headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"},
    json={
        "geolocation": {"latitude": -23.5505, "longitude": -46.6333, "accuracy": 10.5, "source": "GPS"},
    },
).json()

3.6 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 — SERPRO validou biometria COMPLETED ausente Finalizar transação (etapa DORMANT é ignorada)
Biometria indisponível — SERPRO não possui imagem SKIPPED { triggered: true, nextStepId, ... } Completar DOCUMENT_PHOTO_MATCH (ativada automaticamente)
Falha — dados incorretos ou score baixo STARTED ou FAILED ausente Corrigir dados e retentar, ou abortar

cURL

SERPRO_ID=$(echo "$STEPS" | jq -r '.steps[] | select(.type=="SERPRO_IDENTITY_CHECK") | .stepId')

curl -s -X POST "$BASE_URL/v1/transactions/$TX_ID/steps/$SERPRO_ID/start" \
  -H "Authorization: Bearer $TOKEN"

SERPRO=$(curl -s -X POST "$BASE_URL/v1/transactions/$TX_ID/steps/$SERPRO_ID/complete" \
  -H "Authorization: Bearer $TOKEN")

echo "$SERPRO" | jq '{status, fallback, result}'

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

Quando fallback.triggered é true, a etapa DOCUMENT_PHOTO_MATCH muda de DORMANT para PENDING automaticamente. Prossiga para a seção 3.7.

Node.js (fetch)

const serproStep = steps.find((s) => s.type === "SERPRO_IDENTITY_CHECK");
await fetch(
  `${BASE_URL}/v1/transactions/${tx.transactionId}/steps/${serproStep.stepId}/start`,
  { method: "POST", headers: { Authorization: `Bearer ${TOKEN}` } }
);

const serproRes = await fetch(
  `${BASE_URL}/v1/transactions/${tx.transactionId}/steps/${serproStep.stepId}/complete`,
  { method: "POST", headers: { Authorization: `Bearer ${TOKEN}` } }
).then((r) => r.json());

if (serproRes.status === "COMPLETED") {
  // Sucesso — finalizar transação diretamente
  console.log("SERPRO validou:", serproRes.result.serproIdentity.biometricConfidence);
} else if (serproRes.status === "SKIPPED" && serproRes.fallback?.triggered) {
  // Fallback acionado — completar DOCUMENT_PHOTO_MATCH
  const fallbackStepId = serproRes.fallback.nextStepId;
  console.log("Fallback acionado, completar etapa:", fallbackStepId);
} else {
  // Falha — dados incorretos, retentar ou abortar
  console.error("SERPRO falhou:", serproRes.status);
}

Python (requests)

serpro_step = next(s for s in steps if s["type"] == "SERPRO_IDENTITY_CHECK")
requests.post(
    f"{BASE_URL}/v1/transactions/{tx_id}/steps/{serpro_step['stepId']}/start",
    headers={"Authorization": f"Bearer {TOKEN}"},
)

serpro = requests.post(
    f"{BASE_URL}/v1/transactions/{tx_id}/steps/{serpro_step['stepId']}/complete",
    headers={"Authorization": f"Bearer {TOKEN}"},
).json()

if serpro["status"] == "COMPLETED":
    # Sucesso — finalizar transação
    print("SERPRO validou:", serpro["result"]["serproIdentity"]["biometricConfidence"])
elif serpro["status"] == "SKIPPED" and serpro.get("fallback", {}).get("triggered"):
    # Fallback acionado — completar DOCUMENT_PHOTO_MATCH
    fallback_step_id = serpro["fallback"]["nextStepId"]
    print(f"Fallback acionado, completar etapa: {fallback_step_id}")
else:
    # Falha — dados incorretos
    print(f"SERPRO falhou: {serpro['status']}")

3.7 DOCUMENT_PHOTO_MATCH (quando acionado por fallback)

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

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.

cURL — com documento pré-enviado

FALLBACK_STEP_ID=$(echo "$SERPRO" | jq -r '.fallback.nextStepId')

curl -s -X POST "$BASE_URL/v1/transactions/$TX_ID/steps/$FALLBACK_STEP_ID/start" \
  -H "Authorization: Bearer $TOKEN"

# Com fallbackDocument pré-enviado, basta informar documentType
DOC_MATCH=$(curl -s -X POST "$BASE_URL/v1/transactions/$TX_ID/steps/$FALLBACK_STEP_ID/complete" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "documentType": "CNH",
    "geolocation": {
      "latitude": -23.5505,
      "longitude": -46.6333,
      "accuracy": 10.5,
      "source": "GPS"
    }
  }')

echo "$DOC_MATCH" | jq '.result.documentPhotoMatch'
# { "similarity": 97.5, "documentType": "CNH", "biographicValidation": { "overallValid": true } }

cURL — sem documento pré-enviado

DOC_MATCH=$(curl -s -X POST "$BASE_URL/v1/transactions/$TX_ID/steps/$FALLBACK_STEP_ID/complete" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "documentImage": "'$(base64 -w0 cnh_foto.jpg)'",
    "documentType": "CNH",
    "geolocation": {
      "latitude": -23.5505,
      "longitude": -46.6333,
      "accuracy": 10.5,
      "source": "GPS"
    }
  }')

Node.js (fetch)

const fallbackStepId = serproRes.fallback.nextStepId;

await fetch(
  `${BASE_URL}/v1/transactions/${tx.transactionId}/steps/${fallbackStepId}/start`,
  { method: "POST", headers: { Authorization: `Bearer ${TOKEN}` } }
);

// Com fallbackDocument pré-enviado:
const docMatch = await fetch(
  `${BASE_URL}/v1/transactions/${tx.transactionId}/steps/${fallbackStepId}/complete`,
  {
    method: "POST",
    headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      documentType: "CNH",
      geolocation: { latitude: -23.5505, longitude: -46.6333, accuracy: 10.5, source: "GPS" },
    }),
  }
).then((r) => r.json());

// Sem pré-envio — incluir documentImage:
// body: JSON.stringify({
//   documentImage: documentPhotoBase64,
//   documentType: "CNH",
//   geolocation: { ... },
// })

Python (requests)

fallback_step_id = serpro["fallback"]["nextStepId"]

requests.post(
    f"{BASE_URL}/v1/transactions/{tx_id}/steps/{fallback_step_id}/start",
    headers={"Authorization": f"Bearer {TOKEN}"},
)

# Com fallbackDocument pré-enviado:
doc_match = requests.post(
    f"{BASE_URL}/v1/transactions/{tx_id}/steps/{fallback_step_id}/complete",
    headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"},
    json={
        "documentType": "CNH",
        "geolocation": {"latitude": -23.5505, "longitude": -46.6333, "accuracy": 10.5, "source": "GPS"},
    },
).json()

# Sem pré-envio — incluir documentImage:
# json={
#     "documentImage": document_photo_base64,
#     "documentType": "CNH",
#     "geolocation": {...},
# }

3.8 Finalizar transação

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

cURL

FINALIZED=$(curl -s -X POST "$BASE_URL/v1/transactions/$TX_ID/finalize" \
  -H "Authorization: Bearer $TOKEN")

echo "$FINALIZED" | jq '{status, submissionDeadline, deadlineStatus, evidenceId}'
# { "status": "COMPLETED", "submissionDeadline": "2026-03-12T23:59:59Z", "deadlineStatus": "PENDING", "evidenceId": "ev_..." }

Node.js (fetch)

const finalized = await fetch(`${BASE_URL}/v1/transactions/${tx.transactionId}/finalize`, {
  method: "POST",
  headers: { Authorization: `Bearer ${TOKEN}` },
}).then((r) => r.json());

console.log(finalized.status);             // "COMPLETED"
console.log(finalized.submissionDeadline); // "2026-03-12T23:59:59Z"
console.log(finalized.deadlineStatus);     // "PENDING"
console.log(finalized.evidenceId);         // "ev_..."

Python (requests)

finalized = requests.post(
    f"{BASE_URL}/v1/transactions/{tx_id}/finalize",
    headers={"Authorization": f"Bearer {TOKEN}"},
).json()

print(finalized["status"])              # "COMPLETED"
print(finalized["submissionDeadline"])  # "2026-03-12T23:59:59Z"
print(finalized["deadlineStatus"])       # "PENDING"
print(finalized["evidenceId"])          # "ev_..."

4. Geolocalização obrigatória

A NT65 exige geolocalização em todas as etapas biométricas. 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, a API retorna:

{
  "type": "https://api.signdocs.com.br/problems/validation-error",
  "title": "Erro de validação",
  "status": 422,
  "detail": "Geolocalização obrigatória para etapas biométricas NT65",
  "code": "GEOLOCATION_REQUIRED"
}

5. Prazo de submissão de 7 dias úteis

Após a finalização, a transação recebe um submissionDeadline (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

Consulte o status a qualquer momento:

curl -s "$BASE_URL/v1/transactions/$TX_ID" \
  -H "Authorization: Bearer $TOKEN" | jq '{submissionDeadline, deadlineStatus}'

6. Webhooks NT65

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

Evento Quando Payload extra
STEP.PURPOSE_DISCLOSURE_SENT Notificação de divulgação enviada notificationChannel, disclosureVersion
TRANSACTION.DEADLINE_APPROACHING Faltam 2 dias úteis submissionDeadline, deadlineStatus
TRANSACTION.FALLBACK Fallback acionado automaticamente (SERPRO sem biometria) fallbackReason, fallbackStepId, fallbackDocumentPreUploaded

O evento TRANSACTION.FALLBACK é emitido quando a etapa SERPRO_IDENTITY_CHECK retorna SKIPPED e a etapa DOCUMENT_PHOTO_MATCH é ativada. Use este webhook para notificar sua aplicação de que o fluxo alternativo foi acionado.

Registre webhooks para esses eventos:

curl -s -X POST "$BASE_URL/v1/webhooks" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://seu-servidor.com/webhooks/nt65",
    "events": [
      "STEP.PURPOSE_DISCLOSURE_SENT",
      "TRANSACTION.DEADLINE_APPROACHING",
      "TRANSACTION.FALLBACK",
      "TRANSACTION.COMPLETED"
    ]
  }'

7. 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 foi 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

8. Tratamento de erros NT65

Código HTTP Código de erro Causa Solução
400 INVALID_FALLBACK_DOCUMENT fallbackDocument enviado em perfil que não é BIOMETRIC_SERPRO_AUTO_FALLBACK Usar o perfil correto ou remover o campo
409 STEP_IS_DORMANT Tentativa de iniciar uma etapa dormante manualmente Não iniciar DOCUMENT_PHOTO_MATCH diretamente — ela é ativada automaticamente quando o SERPRO retorna SKIPPED
422 GEOLOCATION_REQUIRED Geolocalização ausente em etapa biométrica Incluir objeto geolocation
422 CPF_REQUIRED CPF ausente no signer para perfil NT65 Informar cpf no signer
422 BIRTH_DATE_REQUIRED Data de nascimento ausente para perfil NT65 Informar birthDate no signer
422 PURPOSE_NOT_ACKNOWLEDGED Tentativa de prosseguir sem completar PURPOSE_DISCLOSURE Completar etapa com acknowledged: true
422 DOCUMENT_FACE_NOT_FOUND Face não detectada na foto do documento Enviar foto com melhor qualidade
422 DEADLINE_OVERDUE Prazo de submissão expirado Criar nova transação
502 SERPRO_UNAVAILABLE SERPRO DataValid temporariamente indisponível Retry com backoff exponencial

9. Diagrama de decisão

Criar transação (BIOMETRIC_SERPRO_AUTO_FALLBACK)
  |
  +- PURPOSE_DISCLOSURE --- acknowledged: true
  +- BIOMETRIC_LIVENESS --- prova de vida + geolocalização
  +- BIOMETRIC_MATCH ------ comparação facial + geolocalização
  |
  +- SERPRO_IDENTITY_CHECK
       |
       +- status: COMPLETED (valid: true)
       |    -> Finalizar transação normalmente
       |    -> Etapa DORMANT (DOCUMENT_PHOTO_MATCH) é ignorada na evidência
       |
       +- status: SKIPPED (fallback.triggered: true)
       |    -> DOCUMENT_PHOTO_MATCH muda de DORMANT -> PENDING
       |    -> Iniciar e completar DOCUMENT_PHOTO_MATCH
       |      +- Com fallbackDocument pré-enviado: enviar apenas documentType
       |      +- Sem pré-envio: enviar documentImage + documentType
       |    -> Finalizar transação (evidência inclui fallbackUsed: true)
       |
       +- status: STARTED ou FAILED (dados incorretos / score baixo)
            -> Fallback NÃO é acionado
            -> Corrigir dados e retentar, ou abortar transação

10. Checklist de conformidade

Antes de ir para produção com fluxos NT65, verifique: