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: