Complete o fluxo de assinatura eletrônica de ponta a ponta em aproximadamente 5 minutos.
| Item | Valor |
|---|---|
| Base URL (HML) | https://api-hml.signdocs.com.br |
| Autenticação | OAuth2 client_credentials — Bearer JWT |
| Content-Type (API) | application/json |
| Content-Type (token) | application/x-www-form-urlencoded |
| Formato de erros | RFC 7807 application/problem+json |
| Timestamps | UTC ISO 8601 |
Antes de começar, você precisa de:
Os exemplos abaixo usam o ambiente de homologação (HML). Substitua a base URL por
https://api.signdocs.com.brquando estiver pronto para produção.
Exporte as credenciais como variáveis de ambiente para facilitar a execução dos exemplos:
export BASE_URL="https://api-hml.signdocs.com.br"
export CLIENT_ID="seu_client_id"
export CLIENT_SECRET="seu_client_secret"
Solicite um token JWT via OAuth2 client_credentials. O token expira em 900 segundos (15 minutos).
Escopos disponíveis: transactions:read transactions:write steps:write evidence:read webhooks:write
TOKEN=$(curl -s -X POST "$BASE_URL/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials\
&client_id=$CLIENT_ID\
&client_secret=$CLIENT_SECRET\
&scope=transactions:read transactions:write steps:write evidence:read" \
| jq -r '.access_token')
echo "$TOKEN"
const BASE_URL = process.env.BASE_URL;
const tokenRes = await fetch(`${BASE_URL}/oauth2/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
scope: "transactions:read transactions:write steps:write evidence:read",
}),
});
const { access_token, token_type, expires_in, scope } = await tokenRes.json();
console.log("Token obtido, expira em", expires_in, "segundos");
import os, requests
BASE_URL = os.environ["BASE_URL"]
token_resp = requests.post(f"{BASE_URL}/oauth2/token", data={
"grant_type": "client_credentials",
"client_id": os.environ["CLIENT_ID"],
"client_secret": os.environ["CLIENT_SECRET"],
"scope": "transactions:read transactions:write steps:write evidence:read",
})
token_resp.raise_for_status()
access_token = token_resp.json()["access_token"]
print("Token obtido com sucesso")
Resposta (200):
{
"access_token": "eyJhbGciOiJFUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 900,
"scope": "transactions:read transactions:write steps:write evidence:read"
}
Crie uma transação com o perfil de política CLICK_ONLY (o mais simples — apenas aceite por clique). A API retorna a transação com as etapas resolvidas automaticamente pelo policy engine.
Perfis de política disponíveis:
| Perfil | Etapas geradas |
|---|---|
CLICK_ONLY | CLICK_ACCEPT |
CLICK_PLUS_OTP | CLICK_ACCEPT, OTP_CHALLENGE |
BIOMETRIC | BIOMETRIC_LIVENESS |
BIOMETRIC_PLUS_OTP | BIOMETRIC_LIVENESS, OTP_CHALLENGE |
DIGITAL_CERTIFICATE | CLICK_ACCEPT, DIGITAL_SIGN_A1 |
CUSTOM | Configuração personalizada |
Campos do signer:
| Campo | Obrigatório | Descrição |
|---|---|---|
name | Sim | Nome do assinante |
userExternalId | Sim | Identificador único no seu sistema |
email | Não* | Obrigatório para perfis com OTP |
displayName | Não | Nome de exibição (usa name se omitido) |
cpf | Condicional† | CPF do assinante (11 dígitos, sem pontuação) |
cnpj | Condicional† | CNPJ do assinante (14 dígitos, sem pontuação) |
† É obrigatório informar pelo menos um entre cpf e cnpj.
TX=$(curl -s -X POST "$BASE_URL/v1/transactions" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: quickstart-$(date +%s)" \
-d '{
"purpose": "DOCUMENT_SIGNATURE",
"policy": { "profile": "CLICK_ONLY" },
"signer": {
"name": "Maria Silva",
"userExternalId": "usr_12345",
"cpf": "12345678901"
}
}')
TX_ID=$(echo "$TX" | jq -r '.transactionId')
echo "Transação criada: $TX_ID"
echo "$TX" | jq .
const headers = {
Authorization: `Bearer ${access_token}`,
"Content-Type": "application/json",
"X-Idempotency-Key": `quickstart-${Date.now()}`,
};
const txRes = await fetch(`${BASE_URL}/v1/transactions`, {
method: "POST",
headers,
body: JSON.stringify({
purpose: "DOCUMENT_SIGNATURE",
policy: { profile: "CLICK_ONLY" },
signer: {
name: "Maria Silva",
userExternalId: "usr_12345",
cpf: "12345678901",
},
}),
});
const tx = await txRes.json();
const txId = tx.transactionId;
console.log("Transação criada:", txId);
console.log("Etapas:", tx.steps);
import time
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
"X-Idempotency-Key": f"quickstart-{int(time.time())}",
}
tx_resp = requests.post(f"{BASE_URL}/v1/transactions", headers=headers, json={
"purpose": "DOCUMENT_SIGNATURE",
"policy": {"profile": "CLICK_ONLY"},
"signer": {
"name": "Maria Silva",
"userExternalId": "usr_12345",
"cpf": "12345678901",
},
})
tx_resp.raise_for_status()
tx = tx_resp.json()
tx_id = tx["transactionId"]
print(f"Transação criada: {tx_id}")
print(f"Etapas: {tx['steps']}")
Resposta (201):
{
"transactionId": "01HWXYZ...",
"status": "CREATED",
"purpose": "DOCUMENT_SIGNATURE",
"policy": { "profile": "CLICK_ONLY" },
"steps": [
{
"stepId": "01HWXYZ...",
"type": "CLICK_ACCEPT",
"status": "PENDING",
"order": 0
}
],
"userExternalId": "usr_12345",
"createdAt": "2026-02-22T14:00:00.000Z",
"expiresAt": "2026-02-23T14:00:00.000Z"
}
Envie o PDF codificado em base64 no campo content. O documento será armazenado e um hash SHA-256 será calculado no servidor. Tamanho máximo: 10 MB.
Você também pode enviar o documento inline no momento da criação da transação usando o campo
document.content. Neste guia, usamos o endpoint separado para clareza.
# Codifique o PDF em base64
PDF_B64=$(base64 -w0 contrato.pdf)
curl -s -X POST "$BASE_URL/v1/transactions/$TX_ID/document" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"content\": \"$PDF_B64\"}" | jq .
import { readFileSync } from "node:fs";
const pdfBase64 = readFileSync("contrato.pdf").toString("base64");
const docRes = await fetch(`${BASE_URL}/v1/transactions/${txId}/document`, {
method: "POST",
headers: {
Authorization: `Bearer ${access_token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ content: pdfBase64 }),
});
const doc = await docRes.json();
console.log("Documento enviado. Hash:", doc.documentHash);
import base64
with open("contrato.pdf", "rb") as f:
pdf_b64 = base64.b64encode(f.read()).decode()
doc_resp = requests.post(
f"{BASE_URL}/v1/transactions/{tx_id}/document",
headers=headers,
json={"content": pdf_b64},
)
doc_resp.raise_for_status()
doc = doc_resp.json()
print(f"Documento enviado. Hash: {doc['documentHash']}")
Resposta (200):
{
"transactionId": "01HWXYZ...",
"status": "DOCUMENT_UPLOADED",
"documentHash": "sha256:a1b2c3d4e5f6..."
}
Primeiro, liste as etapas para obter os IDs. Depois, inicie a próxima etapa pendente. As etapas devem ser executadas em ordem — a API valida que etapas anteriores estejam concluídas antes de permitir o início da próxima.
curl -s -X GET "$BASE_URL/v1/transactions/$TX_ID/steps" \
-H "Authorization: Bearer $TOKEN" | jq .
const stepsRes = await fetch(`${BASE_URL}/v1/transactions/${txId}/steps`, {
headers: { Authorization: `Bearer ${access_token}` },
});
const { steps } = await stepsRes.json();
const stepId = steps[0].stepId;
console.log("Etapas:", steps);
steps_resp = requests.get(
f"{BASE_URL}/v1/transactions/{tx_id}/steps",
headers=headers,
)
steps_resp.raise_for_status()
steps = steps_resp.json()["steps"]
step_id = steps[0]["stepId"]
print(f"Etapa a iniciar: {step_id} ({steps[0]['type']})")
Resposta (200):
{
"steps": [
{
"stepId": "01HWXYZ...",
"type": "CLICK_ACCEPT",
"status": "PENDING",
"order": 0,
"attempts": 0,
"maxAttempts": 10
}
]
}
STEP_ID=$(echo "$TX" | jq -r '.steps[0].stepId')
curl -s -X POST "$BASE_URL/v1/transactions/$TX_ID/steps/$STEP_ID/start" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{}' | jq .
const startRes = await fetch(
`${BASE_URL}/v1/transactions/${txId}/steps/${stepId}/start`,
{
method: "POST",
headers: {
Authorization: `Bearer ${access_token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
},
);
const startData = await startRes.json();
console.log("Etapa iniciada:", startData);
start_resp = requests.post(
f"{BASE_URL}/v1/transactions/{tx_id}/steps/{step_id}/start",
headers=headers,
json={},
)
start_resp.raise_for_status()
print(f"Etapa iniciada: {start_resp.json()}")
Resposta (200):
{
"stepId": "01HWXYZ...",
"type": "CLICK_ACCEPT",
"status": "STARTED"
}
Para etapas do tipo CLICK_ACCEPT, envie accepted: true no corpo da requisição. Cada tipo de etapa possui parâmetros diferentes:
| Tipo de etapa | Campos do corpo |
|---|---|
CLICK_ACCEPT | { "accepted": true } |
OTP_CHALLENGE | { "otpCode": "123456" } |
BIOMETRIC_LIVENESS | {} (validação automática via sessão) |
DOCUMENT_PHOTO_MATCH | { "documentImage": "<base64>", "documentType": "CNH" } |
SERPRO_IDENTITY_CHECK | { "cpf": "...", "name": "...", "birthDate": "..." } |
Reenvio de OTP: Para reenviar o código OTP, basta chamarPOST .../steps/{stepId}/startnovamente na etapaOTP_CHALLENGE. A etapaOTP_VERIFYpareada será automaticamente resetada se estiver em statusFAILEDouSTARTED.
curl -s -X POST "$BASE_URL/v1/transactions/$TX_ID/steps/$STEP_ID/complete" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"accepted": true}' | jq .
const completeRes = await fetch(
`${BASE_URL}/v1/transactions/${txId}/steps/${stepId}/complete`,
{
method: "POST",
headers: {
Authorization: `Bearer ${access_token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ accepted: true }),
},
);
const completeData = await completeRes.json();
console.log("Etapa concluída:", completeData);
complete_resp = requests.post(
f"{BASE_URL}/v1/transactions/{tx_id}/steps/{step_id}/complete",
headers=headers,
json={"accepted": True},
)
complete_resp.raise_for_status()
print(f"Etapa concluída: {complete_resp.json()}")
Resposta (200):
{
"stepId": "01HWXYZ...",
"type": "CLICK_ACCEPT",
"status": "COMPLETED",
"completedAt": "2026-02-22T14:02:00.000Z"
}
Após todas as etapas estarem concluídas, finalize a transação. O servidor valida que todas as etapas foram completadas, gera o Evidence Pack (arquivo .p7m assinado com ECDSA via KMS), aplica o carimbo no PDF e retorna o evidenceId para verificação pública.
curl -s -X POST "$BASE_URL/v1/transactions/$TX_ID/finalize" \
-H "Authorization: Bearer $TOKEN" | jq .
const finalizeRes = await fetch(
`${BASE_URL}/v1/transactions/${txId}/finalize`,
{
method: "POST",
headers: { Authorization: `Bearer ${access_token}` },
},
);
const result = await finalizeRes.json();
console.log("Transação finalizada!");
console.log("Evidence ID:", result.evidenceId);
console.log("Evidence Hash:", result.evidenceHash);
finalize_resp = requests.post(
f"{BASE_URL}/v1/transactions/{tx_id}/finalize",
headers=headers,
)
finalize_resp.raise_for_status()
result = finalize_resp.json()
evidence_id = result["evidenceId"]
print(f"Transação finalizada! Evidence ID: {evidence_id}")
print(f"Evidence Hash: {result['evidenceHash']}")
Resposta (200):
{
"transactionId": "01HWXYZ...",
"status": "COMPLETED",
"evidenceId": "01HWABC...",
"evidenceHash": "sha256:f7e8d9c0b1a2...",
"completedAt": "2026-02-22T14:03:00.000Z"
}
O endpoint de verificação é público — não requer autenticação. Qualquer pessoa com o evidenceId pode validar a existência e integridade da assinatura. Nenhuma informação pessoal (PII) é exposta nesta consulta.
EVIDENCE_ID="01HWABC..." # Use o evidenceId da etapa anterior
curl -s -X GET "$BASE_URL/v1/verify/$EVIDENCE_ID" | jq .
const evidenceId = result.evidenceId;
const verifyRes = await fetch(`${BASE_URL}/v1/verify/${evidenceId}`);
const verification = await verifyRes.json();
console.log("Verificação pública:");
console.log(" Status:", verification.status);
console.log(" Política:", verification.policy.profile);
console.log(" Etapas concluídas:", verification.steps.length);
console.log(" Evidence Hash:", verification.evidenceHash);
# Sem header de Authorization — endpoint público
verify_resp = requests.get(f"{BASE_URL}/v1/verify/{evidence_id}")
verify_resp.raise_for_status()
verification = verify_resp.json()
print(f"Status: {verification['status']}")
print(f"Política: {verification['policy']['profile']}")
print(f"Etapas: {len(verification['steps'])}")
print(f"Evidence Hash: {verification['evidenceHash']}")
Resposta (200):
{
"evidenceId": "01HWABC...",
"status": "COMPLETED",
"transactionId": "01HWXYZ...",
"purpose": "DOCUMENT_SIGNATURE",
"documentHash": "sha256:a1b2c3d4e5f6...",
"evidenceHash": "sha256:f7e8d9c0b1a2...",
"policy": { "profile": "CLICK_ONLY" },
"steps": [
{
"type": "CLICK_ACCEPT",
"status": "COMPLETED",
"order": 0,
"completedAt": "2026-02-22T14:02:00.000Z"
}
],
"signer": { "displayName": "Maria Silva" },
"tenantName": "Sua Empresa Ltda",
"createdAt": "2026-02-22T14:00:00.000Z",
"completedAt": "2026-02-22T14:03:00.000Z"
}
Agora que você completou o fluxo básico, explore os recursos avançados:
Receba notificações em tempo real sobre eventos da transação (TRANSACTION.CREATED, TRANSACTION.COMPLETED, etc.). Registre um endpoint:
curl -s -X POST "$BASE_URL/v1/webhooks" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://seu-servidor.com/webhooks/signdocs",
"events": ["TRANSACTION.COMPLETED"]
}'
Use o perfil BIOMETRIC ou BIOMETRIC_PLUS_OTP para adicionar verificação de liveness facial via Amazon Rekognition. Suporta modo HOSTED_PAGE (página hospedada pelo SignDocs Brasil) ou BANK_APP (integração direta via SDK).
Use o perfil DIGITAL_CERTIFICATE para assinaturas com certificado A1. O protocolo de duas fases (prepare + complete) garante que a chave privada nunca saia da sua infraestrutura. Consulte o Guia de Assinatura Digital para detalhes.
Pré-cadastre usuários com foto de referência para comparação facial em transações futuras. O campo cpf é obrigatório:
PUT /v1/users/{userExternalId}/enrollment
Content-Type: application/json
{
"image": "<base64 da imagem JPEG>",
"cpf": "12345678901",
"source": "BANK_PROVIDED"
}
Fontes disponíveis para source:
BANK_PROVIDED (padrão) — selfie ou foto fornecida pelo bancoFIRST_LIVENESS — imagem capturada na primeira sessão de livenessDOCUMENT_PHOTO — foto de documento (CNH/RG/passaporte), com extração facial automática. Requer documentExtractionEnabled.Para documentos maiores ou cenários onde o base64 inline não é ideal, use o fluxo de upload em duas etapas:
POST /v1/transactions/{id}/document/presign -> uploadUrl
PUT {uploadUrl} -> upload direto ao S3
POST /v1/transactions/{id}/document/confirm -> confirmação
Todos os erros seguem o formato RFC 7807 Problem Details:
{
"type": "https://api.signdocs.com.br/problems/conflict",
"title": "Conflict",
"status": 409,
"detail": "Transaction is already finalized",
"instance": "/v1/transactions/01HWXYZ.../finalize"
}
Códigos HTTP comuns:
| Código | Descrição |
|---|---|
400 | Requisição inválida (campos obrigatórios ausentes, formato incorreto) |
401 | Token ausente, expirado ou inválido |
404 | Recurso não encontrado |
409 | Conflito de estado (etapa já concluída, transação já finalizada) |
422 | Erro de validação de negócio (política incompatível, email obrigatório para OTP) |
429 | Limite de requisições excedido |
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="https://api-hml.signdocs.com.br"
# 1. Token
TOKEN=$(curl -sf -X POST "$BASE_URL/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&scope=transactions:read transactions:write steps:write evidence:read" \
| jq -r '.access_token')
# 2. Criar transação
TX=$(curl -sf -X POST "$BASE_URL/v1/transactions" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: qs-$(date +%s)" \
-d '{
"purpose": "DOCUMENT_SIGNATURE",
"policy": {"profile": "CLICK_ONLY"},
"signer": {"name": "Maria Silva", "userExternalId": "usr_12345", "cpf": "12345678901"}
}')
TX_ID=$(echo "$TX" | jq -r '.transactionId')
STEP_ID=$(echo "$TX" | jq -r '.steps[0].stepId')
# 3. Upload do documento
PDF_B64=$(base64 -w0 contrato.pdf)
curl -sf -X POST "$BASE_URL/v1/transactions/$TX_ID/document" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"content\": \"$PDF_B64\"}" > /dev/null
# 4. Iniciar etapa
curl -sf -X POST "$BASE_URL/v1/transactions/$TX_ID/steps/$STEP_ID/start" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{}' > /dev/null
# 5. Concluir etapa
curl -sf -X POST "$BASE_URL/v1/transactions/$TX_ID/steps/$STEP_ID/complete" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"accepted": true}' > /dev/null
# 6. Finalizar
RESULT=$(curl -sf -X POST "$BASE_URL/v1/transactions/$TX_ID/finalize" \
-H "Authorization: Bearer $TOKEN")
EVIDENCE_ID=$(echo "$RESULT" | jq -r '.evidenceId')
echo "Transação finalizada! Evidence ID: $EVIDENCE_ID"
# 7. Verificar (público)
curl -sf "$BASE_URL/v1/verify/$EVIDENCE_ID" | jq .
#!/usr/bin/env python3
"""SignDocs Brasil External API — fluxo completo CLICK_ONLY."""
import base64, os, time, requests
BASE_URL = "https://api-hml.signdocs.com.br"
CLIENT_ID = os.environ["CLIENT_ID"]
CLIENT_SECRET = os.environ["CLIENT_SECRET"]
# 1. Token
token_resp = requests.post(f"{BASE_URL}/oauth2/token", data={
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"scope": "transactions:read transactions:write steps:write evidence:read",
})
token_resp.raise_for_status()
token = token_resp.json()["access_token"]
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
# 2. Criar transação
tx_resp = requests.post(f"{BASE_URL}/v1/transactions", headers={
**headers, "X-Idempotency-Key": f"qs-{int(time.time())}",
}, json={
"purpose": "DOCUMENT_SIGNATURE",
"policy": {"profile": "CLICK_ONLY"},
"signer": {"name": "Maria Silva", "userExternalId": "usr_12345", "cpf": "12345678901"},
})
tx_resp.raise_for_status()
tx = tx_resp.json()
tx_id, step_id = tx["transactionId"], tx["steps"][0]["stepId"]
# 3. Upload do documento
with open("contrato.pdf", "rb") as f:
pdf_b64 = base64.b64encode(f.read()).decode()
requests.post(f"{BASE_URL}/v1/transactions/{tx_id}/document",
headers=headers, json={"content": pdf_b64}).raise_for_status()
# 4. Iniciar etapa
requests.post(f"{BASE_URL}/v1/transactions/{tx_id}/steps/{step_id}/start",
headers=headers, json={}).raise_for_status()
# 5. Concluir etapa
requests.post(f"{BASE_URL}/v1/transactions/{tx_id}/steps/{step_id}/complete",
headers=headers, json={"accepted": True}).raise_for_status()
# 6. Finalizar
result = requests.post(f"{BASE_URL}/v1/transactions/{tx_id}/finalize",
headers=headers)
result.raise_for_status()
evidence_id = result.json()["evidenceId"]
print(f"Transação finalizada! Evidence ID: {evidence_id}")
# 7. Verificar (público — sem autenticação)
verification = requests.get(f"{BASE_URL}/v1/verify/{evidence_id}")
verification.raise_for_status()
print(verification.json())