Guias de Desenvolvimento

Signing Sessions

Guias dos SDKs

Receitas de Integração — Assinatura Expressa

Scripts prontos para copiar e colar. Escolha o perfil de assinatura, copie o código, execute.

Referência completa: consulte o Guia de Sessões de Assinatura para detalhes sobre cada campo, personalização e tratamento de erros.


Sumário

  1. Pré-requisitos e Instalação
  2. Frontend: Instalar o Widget
  3. Receita: CLICK_ONLY — Aceite Simples
  4. Receita: CLICK_PLUS_OTP — Aceite + OTP
  5. Receita: BIOMETRIC — Verificação Facial
  6. Receita: DIGITAL_CERTIFICATE — Certificado A1
  7. Receita: BIOMETRIC_SERPRO_AUTO_FALLBACK — NT65 Consignado
  8. Webhook (alternativa ao polling)
  9. Troubleshooting

1. Pré-requisitos e Instalação

Variáveis de ambiente

Todas as receitas assumem estas variáveis configuradas:

export SIGNDOCS_CLIENT_ID="your_client_id"
export SIGNDOCS_CLIENT_SECRET="your_client_secret"
export SIGNDOCS_BASE_URL="https://api-hml.signdocs.com.br"   # HML
# export SIGNDOCS_BASE_URL="https://api.signdocs.com.br"     # Produção

PDF de teste

Tenha um arquivo contrato.pdf no diretório de trabalho. As receitas codificam ele em base64 automaticamente.

Instalar o SDK do backend

npm install @signdocs-brasil/api
import { SignDocsBrasilClient } from '@signdocs-brasil/api';

const client = new SignDocsBrasilClient({
  clientId: process.env.SIGNDOCS_CLIENT_ID!,
  clientSecret: process.env.SIGNDOCS_CLIENT_SECRET!,
  baseUrl: process.env.SIGNDOCS_BASE_URL,
});
pip install signdocs-brasil
import os
from signdocs_brasil import SignDocsBrasilClient, ClientConfig

client = SignDocsBrasilClient(ClientConfig(
    client_id=os.environ['SIGNDOCS_CLIENT_ID'],
    client_secret=os.environ['SIGNDOCS_CLIENT_SECRET'],
    base_url=os.environ.get('SIGNDOCS_BASE_URL', 'https://api-hml.signdocs.com.br'),
))
go get github.com/signdocsbrasil/signdocsbrasil-go
import signdocs "github.com/signdocsbrasil/signdocsbrasil-go"

client, err := signdocs.NewClient(
    signdocs.WithCredentials(os.Getenv("SIGNDOCS_CLIENT_ID"), os.Getenv("SIGNDOCS_CLIENT_SECRET")),
    signdocs.WithBaseURL(os.Getenv("SIGNDOCS_BASE_URL")),
)
if err != nil { log.Fatal(err) }
<dependency>
  <groupId>com.signdocsbrasil</groupId>
  <artifactId>signdocsbrasil-api</artifactId>
  <version>1.0.0</version>
</dependency>
import com.signdocsbrasil.SignDocsBrasilClient;

SignDocsBrasilClient client = SignDocsBrasilClient.builder()
    .clientId(System.getenv("SIGNDOCS_CLIENT_ID"))
    .clientSecret(System.getenv("SIGNDOCS_CLIENT_SECRET"))
    .baseUrl(System.getenv("SIGNDOCS_BASE_URL"))
    .build();
composer require signdocs-brasil/signdocs-brasil-php
use SignDocsBrasil\Api\SignDocsBrasilClient;

$client = new SignDocsBrasilClient([
    'clientId' => getenv('SIGNDOCS_CLIENT_ID'),
    'clientSecret' => getenv('SIGNDOCS_CLIENT_SECRET'),
    'baseUrl' => getenv('SIGNDOCS_BASE_URL') ?: 'https://api-hml.signdocs.com.br',
]);
dotnet add package SignDocsBrasil.Api
using SignDocsBrasil.Api;

var client = SignDocsBrasilClient.CreateBuilder()
    .ClientId(Environment.GetEnvironmentVariable("SIGNDOCS_CLIENT_ID")!)
    .ClientSecret(Environment.GetEnvironmentVariable("SIGNDOCS_CLIENT_SECRET")!)
    .BaseUrl(Environment.GetEnvironmentVariable("SIGNDOCS_BASE_URL")
        ?? "https://api-hml.signdocs.com.br")
    .Build();

Alternativa: HTTP puro (curl)

Todas as receitas podem ser feitas via HTTP. Obtenha um token primeiro:

ACCESS_TOKEN=$(curl -s -X POST "$SIGNDOCS_BASE_URL/oauth2/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&client_id=$SIGNDOCS_CLIENT_ID&client_secret=$SIGNDOCS_CLIENT_SECRET" \
  | jq -r '.access_token')

HML vs Produção

HML Produção
Base URL https://api-hml.signdocs.com.br https://api.signdocs.com.br
Signing domain https://sign-hml.signdocs.com.br https://sign.signdocs.com.br
Dados reais? Não — sandbox Sim
OTP sandbox.otpCode retornado na resposta Enviado por email/SMS

2. Frontend: Instalar o Widget

O widget JS funciona para todos os perfis de assinatura. Instale uma vez, use em qualquer receita.

Via npm

npm install @signdocs-brasil/js

Via CDN

<script src="https://cdn.signdocs.com.br/v1/signdocs-brasil.js"></script>

Template HTML mínimo

Este template funciona para todos os perfis. O frontend não precisa saber qual perfil foi escolhido — a página hospedada adapta a UI automaticamente.

<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <title>Assinar Documento</title>
</head>
<body>
  <h1>Assinatura de Contrato</h1>
  <button id="btn-assinar">Assinar agora</button>

  <script src="https://cdn.signdocs.com.br/v1/signdocs-brasil.js"></script>
  <script>
    const sd = SignDocsBrasil.init({ locale: 'pt-BR' });

    document.getElementById('btn-assinar').addEventListener('click', async () => {
      // 1. Seu backend cria a sessão e retorna o clientSecret
      const res = await fetch('/api/signing-session', { method: 'POST' });
      const { clientSecret } = await res.json();

      // 2. Abre o popup de assinatura
      sd.checkout({
        clientSecret,
        onComplete(event) {
          alert('Assinatura concluída! Evidence ID: ' + event.evidenceId);
        },
        onError(event) {
          console.error('Erro:', event.message, event.code);
        },
        onClose() {
          console.log('Popup fechado — verifique o status no backend');
        },
      });
    });
  </script>
</body>
</html>

Popup bloqueado? Chame sd.checkout() dentro de um handler de click do usuário (como acima). Navegadores bloqueiam popups fora de interações diretas.


3. Receita: CLICK_ONLY — Aceite Simples

O fluxo mais básico. O signatário visualiza o documento e aceita com um clique.

Backend: create session ──> clientSecret
                              │
Frontend: sd.checkout() ──> popup abre
                              │
Signatário: clica "Aceitar" ──> COMPLETED
                              │
Backend: poll ou webhook ──> evidenceId

Fluxo hospedado (recomendado)

Crie a sessão no backend, passe o clientSecret para o frontend. A página hospedada cuida de tudo.

import { readFileSync } from 'fs';

const pdfBase64 = readFileSync('contrato.pdf').toString('base64');

const session = await client.signingSessions.create({
  purpose: 'DOCUMENT_SIGNATURE',
  policy: { profile: 'CLICK_ONLY' },
  signer: { name: 'João Silva', email: 'joao@example.com', userExternalId: 'user-001' },
  document: { content: pdfBase64, filename: 'contrato.pdf' },
  returnUrl: 'https://app.example.com/done',
  locale: 'pt-BR',
  expiresInMinutes: 60,
});

// Envie session.clientSecret para o frontend
console.log('Session ID:', session.sessionId);
console.log('Client Secret:', session.clientSecret);

// Aguarde a conclusão (server-side polling)
const result = await client.signingSessions.waitForCompletion(session.sessionId);
console.log('Status:', result.status); // COMPLETED
console.log('Evidence ID:', result.evidenceId);
import base64

with open('contrato.pdf', 'rb') as f:
    pdf_base64 = base64.b64encode(f.read()).decode()

session = client.signing_sessions.create(CreateSigningSessionRequest(
    purpose='DOCUMENT_SIGNATURE',
    policy=Policy(profile='CLICK_ONLY'),
    signer=Signer(name='João Silva', email='joao@example.com', user_external_id='user-001'),
    document=InlineDocument(content=pdf_base64, filename='contrato.pdf'),
    return_url='https://app.example.com/done',
    locale='pt-BR',
    expires_in_minutes=60,
))

print('Session ID:', session.session_id)
print('Client Secret:', session.client_secret)

result = client.signing_sessions.wait_for_completion(session.session_id)
print('Status:', result.status)  # COMPLETED
print('Evidence ID:', result.evidence_id)
pdfBytes, _ := os.ReadFile("contrato.pdf")
pdfBase64 := base64.StdEncoding.EncodeToString(pdfBytes)

session, err := client.SigningSessions.Create(ctx, &signdocs.CreateSigningSessionRequest{
    Purpose:          signdocs.PurposeDocumentSignature,
    Policy:           signdocs.Policy{Profile: signdocs.PolicyProfileClickOnly},
    Signer:           signdocs.Signer{Name: "João Silva", Email: "joao@example.com", UserExternalID: "user-001"},
    Document:         &signdocs.DocumentInline{Content: pdfBase64, Filename: "contrato.pdf"},
    ReturnURL:        "https://app.example.com/done",
    Locale:           "pt-BR",
    ExpiresInMinutes: 60,
})
if err != nil { log.Fatal(err) }

fmt.Println("Session ID:", session.SessionID)
fmt.Println("Client Secret:", session.ClientSecret)

result, err := client.SigningSessions.WaitForCompletion(ctx, session.SessionID)
if err != nil { log.Fatal(err) }
fmt.Println("Status:", result.Status)       // COMPLETED
fmt.Println("Evidence ID:", result.EvidenceID)
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;

String pdfBase64 = Base64.getEncoder().encodeToString(Files.readAllBytes(Path.of("contrato.pdf")));

CreateSigningSessionRequest request = new CreateSigningSessionRequest();
request.purpose = "DOCUMENT_SIGNATURE";
request.policy = new Policy("CLICK_ONLY");
request.signer = new Signer("João Silva", "joao@example.com", "user-001");
request.document = new CreateSigningSessionRequest.InlineDocument(pdfBase64, "contrato.pdf");
request.returnUrl = "https://app.example.com/done";
request.locale = "pt-BR";
request.expiresInMinutes = 60;

SigningSession session = client.signingSessions().create(request);
System.out.println("Session ID: " + session.sessionId);
System.out.println("Client Secret: " + session.clientSecret);

SigningSessionStatusResponse result = client.signingSessions().waitForCompletion(session.sessionId);
System.out.println("Status: " + result.status);       // COMPLETED
System.out.println("Evidence ID: " + result.evidenceId);
$pdfBase64 = base64_encode(file_get_contents('contrato.pdf'));

$session = $client->signingSessions->create(new CreateSigningSessionRequest(
    purpose: 'DOCUMENT_SIGNATURE',
    policy: new Policy(profile: 'CLICK_ONLY'),
    signer: new Signer(name: 'João Silva', email: 'joao@example.com', userExternalId: 'user-001'),
    document: ['content' => $pdfBase64, 'filename' => 'contrato.pdf'],
    returnUrl: 'https://app.example.com/done',
    locale: 'pt-BR',
    expiresInMinutes: 60,
));

echo "Session ID: " . $session->sessionId . "\n";
echo "Client Secret: " . $session->clientSecret . "\n";

$result = $client->signingSessions->waitForCompletion($session->sessionId);
echo "Status: " . $result->status . "\n";       // COMPLETED
echo "Evidence ID: " . $result->evidenceId . "\n";
using SignDocsBrasil.Api.Models;

var pdfBase64 = Convert.ToBase64String(await File.ReadAllBytesAsync("contrato.pdf"));

var session = await client.SigningSessions.CreateAsync(new CreateSigningSessionRequest
{
    Purpose = "DOCUMENT_SIGNATURE",
    Policy = new Policy { Profile = "CLICK_ONLY" },
    Signer = new Signer { Name = "João Silva", Email = "joao@example.com", UserExternalId = "user-001" },
    Document = new InlineDocument { Content = pdfBase64, Filename = "contrato.pdf" },
    ReturnUrl = "https://app.example.com/done",
    Locale = "pt-BR",
    ExpiresInMinutes = 60,
});

Console.WriteLine($"Session ID: {session.SessionId}");
Console.WriteLine($"Client Secret: {session.ClientSecret}");

var result = await client.SigningSessions.WaitForCompletionAsync(session.SessionId);
Console.WriteLine($"Status: {result.Status}");       // COMPLETED
Console.WriteLine($"Evidence ID: {result.EvidenceId}");

Fluxo programático (headless — sem frontend)

Para integrações server-to-server. Crie a sessão e avance as etapas via API.

const session = await client.signingSessions.create({
  purpose: 'DOCUMENT_SIGNATURE',
  policy: { profile: 'CLICK_ONLY' },
  signer: { name: 'João Silva', email: 'joao@example.com', userExternalId: 'user-001' },
  document: { content: pdfBase64, filename: 'contrato.pdf' },
});

// Avançar: aceitar o documento
const result = await fetch(`${process.env.SIGNDOCS_BASE_URL}/v1/signing-sessions/${session.sessionId}/advance`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${session.clientSecret}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ action: 'accept' }),
});
const body = await result.json();
console.log('Status:', body.status); // COMPLETED
console.log('Evidence ID:', body.evidenceId);
import requests

session = client.signing_sessions.create(CreateSigningSessionRequest(
    purpose='DOCUMENT_SIGNATURE',
    policy=Policy(profile='CLICK_ONLY'),
    signer=Signer(name='João Silva', email='joao@example.com', user_external_id='user-001'),
    document=InlineDocument(content=pdf_base64, filename='contrato.pdf'),
))

result = requests.post(
    f"{os.environ['SIGNDOCS_BASE_URL']}/v1/signing-sessions/{session.session_id}/advance",
    headers={'Authorization': f'Bearer {session.client_secret}', 'Content-Type': 'application/json'},
    json={'action': 'accept'},
)
body = result.json()
print('Status:', body['status'])       # COMPLETED
print('Evidence ID:', body['evidenceId'])
session, err := client.SigningSessions.Create(ctx, &signdocs.CreateSigningSessionRequest{
    Purpose:  signdocs.PurposeDocumentSignature,
    Policy:   signdocs.Policy{Profile: signdocs.PolicyProfileClickOnly},
    Signer:   signdocs.Signer{Name: "João Silva", Email: "joao@example.com", UserExternalID: "user-001"},
    Document: &signdocs.DocumentInline{Content: pdfBase64, Filename: "contrato.pdf"},
})
if err != nil { log.Fatal(err) }

advanceBody, _ := json.Marshal(map[string]string{"action": "accept"})
req, _ := http.NewRequest("POST",
    fmt.Sprintf("%s/v1/signing-sessions/%s/advance", os.Getenv("SIGNDOCS_BASE_URL"), session.SessionID),
    bytes.NewReader(advanceBody))
req.Header.Set("Authorization", "Bearer "+session.ClientSecret)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
var body map[string]interface{}
json.NewDecoder(resp.Body).Decode(&body)
fmt.Println("Status:", body["status"])       // COMPLETED
fmt.Println("Evidence ID:", body["evidenceId"])
SigningSession session = client.signingSessions().create(request); // (ver hosted acima)

HttpClient http = HttpClient.newHttpClient();
HttpRequest advanceReq = HttpRequest.newBuilder()
    .uri(URI.create(System.getenv("SIGNDOCS_BASE_URL")
        + "/v1/signing-sessions/" + session.sessionId + "/advance"))
    .header("Authorization", "Bearer " + session.clientSecret)
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("{\"action\":\"accept\"}"))
    .build();
HttpResponse<String> resp = http.send(advanceReq, HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body()); // {"status":"COMPLETED","evidenceId":"..."}
$session = $client->signingSessions->create(/* ... ver hosted acima ... */);

$ch = curl_init(getenv('SIGNDOCS_BASE_URL')
    . '/v1/signing-sessions/' . $session->sessionId . '/advance');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . $session->clientSecret,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['action' => 'accept']),
    CURLOPT_RETURNTRANSFER => true,
]);
$body = json_decode(curl_exec($ch));
echo "Status: " . $body->status . "\n";       // COMPLETED
echo "Evidence ID: " . $body->evidenceId . "\n";
var session = await client.SigningSessions.CreateAsync(/* ... ver hosted acima ... */);

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
    new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", session.ClientSecret);
var advanceRes = await http.PostAsync(
    $"{Environment.GetEnvironmentVariable("SIGNDOCS_BASE_URL")}/v1/signing-sessions/{session.SessionId}/advance",
    new StringContent("{\"action\":\"accept\"}", System.Text.Encoding.UTF8, "application/json"));
var body = await advanceRes.Content.ReadAsStringAsync();
Console.WriteLine(body); // {"status":"COMPLETED","evidenceId":"..."}

O que aconteceu?


4. Receita: CLICK_PLUS_OTP — Aceite + OTP

O signatário aceita o documento e depois confirma com um código OTP enviado por email ou SMS.

Backend: create session ──> clientSecret
                              │
Frontend: sd.checkout() ──> popup abre
                              │
Signatário: clica "Aceitar" ──> OTP enviado
                              │
Signatário: digita código ──> COMPLETED
                              │
Backend: poll ou webhook ──> evidenceId

Obrigatório: signer.email (ou signer.phone + otpChannel: 'sms').

Fluxo hospedado (recomendado)

A página hospedada cuida do aceite + input do OTP. Seu backend só cria e espera.

const pdfBase64 = readFileSync('contrato.pdf').toString('base64');

const session = await client.signingSessions.create({
  purpose: 'DOCUMENT_SIGNATURE',
  policy: { profile: 'CLICK_PLUS_OTP' },
  signer: {
    name: 'Maria Souza',
    email: 'maria@example.com',
    userExternalId: 'user-002',
  },
  document: { content: pdfBase64, filename: 'contrato.pdf' },
  returnUrl: 'https://app.example.com/done',
  locale: 'pt-BR',
  expiresInMinutes: 60,
});

console.log('Client Secret:', session.clientSecret);
// Em HML: session.sandbox.otpCode contém o código para testes

const result = await client.signingSessions.waitForCompletion(session.sessionId);
console.log('Status:', result.status);       // COMPLETED
console.log('Evidence ID:', result.evidenceId);
session = client.signing_sessions.create(CreateSigningSessionRequest(
    purpose='DOCUMENT_SIGNATURE',
    policy=Policy(profile='CLICK_PLUS_OTP'),
    signer=Signer(name='Maria Souza', email='maria@example.com', user_external_id='user-002'),
    document=InlineDocument(content=pdf_base64, filename='contrato.pdf'),
    return_url='https://app.example.com/done',
    locale='pt-BR',
    expires_in_minutes=60,
))

print('Client Secret:', session.client_secret)
# Em HML: session.sandbox.otp_code

result = client.signing_sessions.wait_for_completion(session.session_id)
print('Status:', result.status)       # COMPLETED
print('Evidence ID:', result.evidence_id)
session, err := client.SigningSessions.Create(ctx, &signdocs.CreateSigningSessionRequest{
    Purpose:          signdocs.PurposeDocumentSignature,
    Policy:           signdocs.Policy{Profile: signdocs.PolicyProfileClickPlusOTP},
    Signer:           signdocs.Signer{Name: "Maria Souza", Email: "maria@example.com", UserExternalID: "user-002"},
    Document:         &signdocs.DocumentInline{Content: pdfBase64, Filename: "contrato.pdf"},
    ReturnURL:        "https://app.example.com/done",
    Locale:           "pt-BR",
    ExpiresInMinutes: 60,
})
if err != nil { log.Fatal(err) }

fmt.Println("Client Secret:", session.ClientSecret)
// Em HML: session.Sandbox.OTPCode

result, err := client.SigningSessions.WaitForCompletion(ctx, session.SessionID)
if err != nil { log.Fatal(err) }
fmt.Println("Status:", result.Status)       // COMPLETED
fmt.Println("Evidence ID:", result.EvidenceID)
String pdfBase64 = Base64.getEncoder().encodeToString(Files.readAllBytes(Path.of("contrato.pdf")));

CreateSigningSessionRequest request = new CreateSigningSessionRequest();
request.purpose = "DOCUMENT_SIGNATURE";
request.policy = new Policy("CLICK_PLUS_OTP");
request.signer = new Signer("Maria Souza", "maria@example.com", "user-002");
request.document = new CreateSigningSessionRequest.InlineDocument(pdfBase64, "contrato.pdf");
request.returnUrl = "https://app.example.com/done";
request.locale = "pt-BR";
request.expiresInMinutes = 60;

SigningSession session = client.signingSessions().create(request);
System.out.println("Client Secret: " + session.clientSecret);
// Em HML: session.sandbox.otpCode

SigningSessionStatusResponse result = client.signingSessions().waitForCompletion(session.sessionId);
System.out.println("Status: " + result.status);       // COMPLETED
System.out.println("Evidence ID: " + result.evidenceId);
$pdfBase64 = base64_encode(file_get_contents('contrato.pdf'));

$session = $client->signingSessions->create(new CreateSigningSessionRequest(
    purpose: 'DOCUMENT_SIGNATURE',
    policy: new Policy(profile: 'CLICK_PLUS_OTP'),
    signer: new Signer(name: 'Maria Souza', email: 'maria@example.com', userExternalId: 'user-002'),
    document: ['content' => $pdfBase64, 'filename' => 'contrato.pdf'],
    returnUrl: 'https://app.example.com/done',
    locale: 'pt-BR',
    expiresInMinutes: 60,
));

echo "Client Secret: " . $session->clientSecret . "\n";
// Em HML: $session->sandbox->otpCode

$result = $client->signingSessions->waitForCompletion($session->sessionId);
echo "Status: " . $result->status . "\n";       // COMPLETED
echo "Evidence ID: " . $result->evidenceId . "\n";
var pdfBase64 = Convert.ToBase64String(await File.ReadAllBytesAsync("contrato.pdf"));

var session = await client.SigningSessions.CreateAsync(new CreateSigningSessionRequest
{
    Purpose = "DOCUMENT_SIGNATURE",
    Policy = new Policy { Profile = "CLICK_PLUS_OTP" },
    Signer = new Signer { Name = "Maria Souza", Email = "maria@example.com", UserExternalId = "user-002" },
    Document = new InlineDocument { Content = pdfBase64, Filename = "contrato.pdf" },
    ReturnUrl = "https://app.example.com/done",
    Locale = "pt-BR",
    ExpiresInMinutes = 60,
});

Console.WriteLine($"Client Secret: {session.ClientSecret}");
// Em HML: session.Sandbox.OtpCode

var result = await client.SigningSessions.WaitForCompletionAsync(session.SessionId);
Console.WriteLine($"Status: {result.Status}");       // COMPLETED
Console.WriteLine($"Evidence ID: {result.EvidenceId}");

Fluxo programático (headless)

Avance as etapas manualmente: aceitar → OTP challenge dispara → verificar OTP.

const session = await client.signingSessions.create({
  purpose: 'DOCUMENT_SIGNATURE',
  policy: { profile: 'CLICK_PLUS_OTP' },
  signer: { name: 'Maria Souza', email: 'maria@example.com', userExternalId: 'user-002' },
  document: { content: pdfBase64, filename: 'contrato.pdf' },
});

const baseUrl = process.env.SIGNDOCS_BASE_URL;
const headers = {
  'Authorization': `Bearer ${session.clientSecret}`,
  'Content-Type': 'application/json',
};

// Passo 1: Aceitar o documento (dispara envio de OTP automaticamente)
const acceptRes = await fetch(
  `${baseUrl}/v1/signing-sessions/${session.sessionId}/advance`,
  { method: 'POST', headers, body: JSON.stringify({ action: 'accept' }) },
);
const acceptBody = await acceptRes.json();
console.log('Após accept — status:', acceptBody.status); // AWAITING_OTP

// Em HML: o código OTP vem na resposta
const otpCode = acceptBody.sandbox?.otpCode;
console.log('OTP (sandbox):', otpCode);

// Passo 2: Verificar OTP
const verifyRes = await fetch(
  `${baseUrl}/v1/signing-sessions/${session.sessionId}/advance`,
  { method: 'POST', headers, body: JSON.stringify({ action: 'verify_otp', otpCode }) },
);
const verifyBody = await verifyRes.json();
console.log('Status:', verifyBody.status);       // COMPLETED
console.log('Evidence ID:', verifyBody.evidenceId);
import requests

session = client.signing_sessions.create(CreateSigningSessionRequest(
    purpose='DOCUMENT_SIGNATURE',
    policy=Policy(profile='CLICK_PLUS_OTP'),
    signer=Signer(name='Maria Souza', email='maria@example.com', user_external_id='user-002'),
    document=InlineDocument(content=pdf_base64, filename='contrato.pdf'),
))

base_url = os.environ['SIGNDOCS_BASE_URL']
headers = {'Authorization': f'Bearer {session.client_secret}', 'Content-Type': 'application/json'}

# Passo 1: Aceitar
accept_res = requests.post(
    f'{base_url}/v1/signing-sessions/{session.session_id}/advance',
    headers=headers, json={'action': 'accept'})
accept_body = accept_res.json()
otp_code = accept_body.get('sandbox', {}).get('otpCode')
print('OTP (sandbox):', otp_code)

# Passo 2: Verificar OTP
verify_res = requests.post(
    f'{base_url}/v1/signing-sessions/{session.session_id}/advance',
    headers=headers, json={'action': 'verify_otp', 'otpCode': otp_code})
verify_body = verify_res.json()
print('Status:', verify_body['status'])       # COMPLETED
print('Evidence ID:', verify_body['evidenceId'])
session, _ := client.SigningSessions.Create(ctx, &signdocs.CreateSigningSessionRequest{
    Purpose:  signdocs.PurposeDocumentSignature,
    Policy:   signdocs.Policy{Profile: signdocs.PolicyProfileClickPlusOTP},
    Signer:   signdocs.Signer{Name: "Maria Souza", Email: "maria@example.com", UserExternalID: "user-002"},
    Document: &signdocs.DocumentInline{Content: pdfBase64, Filename: "contrato.pdf"},
})

baseURL := os.Getenv("SIGNDOCS_BASE_URL")
advanceURL := fmt.Sprintf("%s/v1/signing-sessions/%s/advance", baseURL, session.SessionID)
headers := map[string]string{
    "Authorization": "Bearer " + session.ClientSecret,
    "Content-Type":  "application/json",
}

// Passo 1: Aceitar
acceptBody, _ := json.Marshal(map[string]string{"action": "accept"})
acceptReq, _ := http.NewRequest("POST", advanceURL, bytes.NewReader(acceptBody))
for k, v := range headers { acceptReq.Header.Set(k, v) }
acceptResp, _ := http.DefaultClient.Do(acceptReq)
var acceptResult map[string]interface{}
json.NewDecoder(acceptResp.Body).Decode(&acceptResult)
otpCode := acceptResult["sandbox"].(map[string]interface{})["otpCode"].(string)

// Passo 2: Verificar OTP
verifyBody, _ := json.Marshal(map[string]string{"action": "verify_otp", "otpCode": otpCode})
verifyReq, _ := http.NewRequest("POST", advanceURL, bytes.NewReader(verifyBody))
for k, v := range headers { verifyReq.Header.Set(k, v) }
verifyResp, _ := http.DefaultClient.Do(verifyReq)
var verifyResult map[string]interface{}
json.NewDecoder(verifyResp.Body).Decode(&verifyResult)
fmt.Println("Status:", verifyResult["status"])       // COMPLETED
fmt.Println("Evidence ID:", verifyResult["evidenceId"])
SigningSession session = client.signingSessions().create(request); // (ver hosted acima)

HttpClient http = HttpClient.newHttpClient();
String advanceUrl = System.getenv("SIGNDOCS_BASE_URL")
    + "/v1/signing-sessions/" + session.sessionId + "/advance";

// Passo 1: Aceitar
HttpRequest acceptReq = HttpRequest.newBuilder().uri(URI.create(advanceUrl))
    .header("Authorization", "Bearer " + session.clientSecret)
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("{\"action\":\"accept\"}")).build();
String acceptBody = http.send(acceptReq, HttpResponse.BodyHandlers.ofString()).body();
// Extrair otpCode do sandbox (use sua lib JSON preferida)

// Passo 2: Verificar OTP
HttpRequest verifyReq = HttpRequest.newBuilder().uri(URI.create(advanceUrl))
    .header("Authorization", "Bearer " + session.clientSecret)
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(
        "{\"action\":\"verify_otp\",\"otpCode\":\"" + otpCode + "\"}")).build();
String verifyBody = http.send(verifyReq, HttpResponse.BodyHandlers.ofString()).body();
System.out.println(verifyBody); // {"status":"COMPLETED","evidenceId":"..."}
$session = $client->signingSessions->create(/* ... ver hosted acima ... */);

$advanceUrl = getenv('SIGNDOCS_BASE_URL')
    . '/v1/signing-sessions/' . $session->sessionId . '/advance';
$headers = [
    'Authorization: Bearer ' . $session->clientSecret,
    'Content-Type: application/json',
];

// Passo 1: Aceitar
$ch = curl_init($advanceUrl);
curl_setopt_array($ch, [
    CURLOPT_POST => true, CURLOPT_HTTPHEADER => $headers,
    CURLOPT_POSTFIELDS => json_encode(['action' => 'accept']),
    CURLOPT_RETURNTRANSFER => true,
]);
$acceptBody = json_decode(curl_exec($ch));
$otpCode = $acceptBody->sandbox->otpCode ?? null;

// Passo 2: Verificar OTP
$ch = curl_init($advanceUrl);
curl_setopt_array($ch, [
    CURLOPT_POST => true, CURLOPT_HTTPHEADER => $headers,
    CURLOPT_POSTFIELDS => json_encode(['action' => 'verify_otp', 'otpCode' => $otpCode]),
    CURLOPT_RETURNTRANSFER => true,
]);
$verifyBody = json_decode(curl_exec($ch));
echo "Status: " . $verifyBody->status . "\n";       // COMPLETED
echo "Evidence ID: " . $verifyBody->evidenceId . "\n";
var session = await client.SigningSessions.CreateAsync(/* ... ver hosted acima ... */);

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
    new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", session.ClientSecret);
var advanceUrl = $"{Environment.GetEnvironmentVariable("SIGNDOCS_BASE_URL")}/v1/signing-sessions/{session.SessionId}/advance";

// Passo 1: Aceitar
var acceptRes = await http.PostAsync(advanceUrl,
    new StringContent("{\"action\":\"accept\"}", System.Text.Encoding.UTF8, "application/json"));
var acceptJson = System.Text.Json.JsonDocument.Parse(await acceptRes.Content.ReadAsStringAsync());
var otpCode = acceptJson.RootElement.GetProperty("sandbox").GetProperty("otpCode").GetString();

// Passo 2: Verificar OTP
var verifyRes = await http.PostAsync(advanceUrl,
    new StringContent($"{{\"action\":\"verify_otp\",\"otpCode\":\"{otpCode}\"}}",
        System.Text.Encoding.UTF8, "application/json"));
Console.WriteLine(await verifyRes.Content.ReadAsStringAsync());
// {"status":"COMPLETED","evidenceId":"..."}

Sandbox: Em HML, o sandbox.otpCode é retornado na resposta do accept. Em produção, o código é enviado por email/SMS e o signatário precisa digitá-lo.


5. Receita: BIOMETRIC — Verificação Facial

O signatário passa por verificação facial via liveness no checkout hospedado.

Backend: create session ──> clientSecret
                              │
Frontend: sd.checkout() ──> popup abre
                              │
Signatário: liveness facial ──> match biométrico
                              │
                           COMPLETED
                              │
Backend: poll ou webhook ──> evidenceId

Obrigatório: signer.cpf para matching biométrico.

Fluxo hospedado (recomendado)

A página hospedada executa toda a captura de liveness. Seu backend só cria e espera.

const pdfBase64 = readFileSync('contrato.pdf').toString('base64');

const session = await client.signingSessions.create({
  purpose: 'DOCUMENT_SIGNATURE',
  policy: { profile: 'BIOMETRIC' },
  signer: {
    name: 'Carlos Lima',
    cpf: '12345678901',
    userExternalId: 'user-003',
  },
  document: { content: pdfBase64, filename: 'contrato.pdf' },
  returnUrl: 'https://app.example.com/done',
  locale: 'pt-BR',
  expiresInMinutes: 60,
});

console.log('Client Secret:', session.clientSecret);

const result = await client.signingSessions.waitForCompletion(session.sessionId);
console.log('Status:', result.status);       // COMPLETED
console.log('Evidence ID:', result.evidenceId);
session = client.signing_sessions.create(CreateSigningSessionRequest(
    purpose='DOCUMENT_SIGNATURE',
    policy=Policy(profile='BIOMETRIC'),
    signer=Signer(name='Carlos Lima', cpf='12345678901', user_external_id='user-003'),
    document=InlineDocument(content=pdf_base64, filename='contrato.pdf'),
    return_url='https://app.example.com/done',
    locale='pt-BR',
    expires_in_minutes=60,
))

print('Client Secret:', session.client_secret)

result = client.signing_sessions.wait_for_completion(session.session_id)
print('Status:', result.status)       # COMPLETED
print('Evidence ID:', result.evidence_id)
session, err := client.SigningSessions.Create(ctx, &signdocs.CreateSigningSessionRequest{
    Purpose:          signdocs.PurposeDocumentSignature,
    Policy:           signdocs.Policy{Profile: signdocs.PolicyProfileBiometric},
    Signer:           signdocs.Signer{Name: "Carlos Lima", CPF: "12345678901", UserExternalID: "user-003"},
    Document:         &signdocs.DocumentInline{Content: pdfBase64, Filename: "contrato.pdf"},
    ReturnURL:        "https://app.example.com/done",
    Locale:           "pt-BR",
    ExpiresInMinutes: 60,
})
if err != nil { log.Fatal(err) }

fmt.Println("Client Secret:", session.ClientSecret)

result, err := client.SigningSessions.WaitForCompletion(ctx, session.SessionID)
if err != nil { log.Fatal(err) }
fmt.Println("Status:", result.Status)       // COMPLETED
fmt.Println("Evidence ID:", result.EvidenceID)
String pdfBase64 = Base64.getEncoder().encodeToString(Files.readAllBytes(Path.of("contrato.pdf")));

CreateSigningSessionRequest request = new CreateSigningSessionRequest();
request.purpose = "DOCUMENT_SIGNATURE";
request.policy = new Policy("BIOMETRIC");
request.signer = new Signer("Carlos Lima", "user-003");
request.signer.cpf = "12345678901";
request.document = new CreateSigningSessionRequest.InlineDocument(pdfBase64, "contrato.pdf");
request.returnUrl = "https://app.example.com/done";
request.locale = "pt-BR";
request.expiresInMinutes = 60;

SigningSession session = client.signingSessions().create(request);
System.out.println("Client Secret: " + session.clientSecret);

SigningSessionStatusResponse result = client.signingSessions().waitForCompletion(session.sessionId);
System.out.println("Status: " + result.status);       // COMPLETED
System.out.println("Evidence ID: " + result.evidenceId);
$pdfBase64 = base64_encode(file_get_contents('contrato.pdf'));

$session = $client->signingSessions->create(new CreateSigningSessionRequest(
    purpose: 'DOCUMENT_SIGNATURE',
    policy: new Policy(profile: 'BIOMETRIC'),
    signer: new Signer(name: 'Carlos Lima', cpf: '12345678901', userExternalId: 'user-003'),
    document: ['content' => $pdfBase64, 'filename' => 'contrato.pdf'],
    returnUrl: 'https://app.example.com/done',
    locale: 'pt-BR',
    expiresInMinutes: 60,
));

echo "Client Secret: " . $session->clientSecret . "\n";

$result = $client->signingSessions->waitForCompletion($session->sessionId);
echo "Status: " . $result->status . "\n";       // COMPLETED
echo "Evidence ID: " . $result->evidenceId . "\n";
var pdfBase64 = Convert.ToBase64String(await File.ReadAllBytesAsync("contrato.pdf"));

var session = await client.SigningSessions.CreateAsync(new CreateSigningSessionRequest
{
    Purpose = "DOCUMENT_SIGNATURE",
    Policy = new Policy { Profile = "BIOMETRIC" },
    Signer = new Signer { Name = "Carlos Lima", Cpf = "12345678901", UserExternalId = "user-003" },
    Document = new InlineDocument { Content = pdfBase64, Filename = "contrato.pdf" },
    ReturnUrl = "https://app.example.com/done",
    Locale = "pt-BR",
    ExpiresInMinutes = 60,
});

Console.WriteLine($"Client Secret: {session.ClientSecret}");

var result = await client.SigningSessions.WaitForCompletionAsync(session.SessionId);
Console.WriteLine($"Status: {result.Status}");       // COMPLETED
Console.WriteLine($"Evidence ID: {result.EvidenceId}");

Fluxo programático (headless)

Para integrações server-to-server: inicie o liveness, aguarde o resultado, complete o match.

const session = await client.signingSessions.create({
  purpose: 'DOCUMENT_SIGNATURE',
  policy: { profile: 'BIOMETRIC' },
  signer: { name: 'Carlos Lima', cpf: '12345678901', userExternalId: 'user-003' },
  document: { content: pdfBase64, filename: 'contrato.pdf' },
});

const baseUrl = process.env.SIGNDOCS_BASE_URL;
const headers = {
  'Authorization': `Bearer ${session.clientSecret}`,
  'Content-Type': 'application/json',
};
const advanceUrl = `${baseUrl}/v1/signing-sessions/${session.sessionId}/advance`;

// Passo 1: Iniciar liveness — retorna hostedUrl para o signatário
const startRes = await fetch(advanceUrl, {
  method: 'POST', headers,
  body: JSON.stringify({ action: 'start_liveness' }),
});
const startBody = await startRes.json();
console.log('Hosted URL:', startBody.hostedUrl);
console.log('Liveness Session ID:', startBody.livenessSessionId);

// (Signatário completa a verificação facial na hostedUrl)

// Passo 2: Completar liveness (após o signatário finalizar)
const completeRes = await fetch(advanceUrl, {
  method: 'POST', headers,
  body: JSON.stringify({
    action: 'complete_liveness',
    livenessSessionId: startBody.livenessSessionId,
  }),
});
const completeBody = await completeRes.json();
console.log('Status:', completeBody.status);       // COMPLETED
console.log('Evidence ID:', completeBody.evidenceId);
session = client.signing_sessions.create(CreateSigningSessionRequest(
    purpose='DOCUMENT_SIGNATURE',
    policy=Policy(profile='BIOMETRIC'),
    signer=Signer(name='Carlos Lima', cpf='12345678901', user_external_id='user-003'),
    document=InlineDocument(content=pdf_base64, filename='contrato.pdf'),
))

base_url = os.environ['SIGNDOCS_BASE_URL']
headers = {'Authorization': f'Bearer {session.client_secret}', 'Content-Type': 'application/json'}
advance_url = f'{base_url}/v1/signing-sessions/{session.session_id}/advance'

# Passo 1: Iniciar liveness
start_res = requests.post(advance_url, headers=headers, json={'action': 'start_liveness'})
start_body = start_res.json()
print('Hosted URL:', start_body['hostedUrl'])
liveness_session_id = start_body['livenessSessionId']

# (Signatário completa a verificação facial)

# Passo 2: Completar liveness
complete_res = requests.post(advance_url, headers=headers, json={
    'action': 'complete_liveness',
    'livenessSessionId': liveness_session_id,
})
complete_body = complete_res.json()
print('Status:', complete_body['status'])       # COMPLETED
print('Evidence ID:', complete_body['evidenceId'])
session, _ := client.SigningSessions.Create(ctx, &signdocs.CreateSigningSessionRequest{
    Purpose:  signdocs.PurposeDocumentSignature,
    Policy:   signdocs.Policy{Profile: signdocs.PolicyProfileBiometric},
    Signer:   signdocs.Signer{Name: "Carlos Lima", CPF: "12345678901", UserExternalID: "user-003"},
    Document: &signdocs.DocumentInline{Content: pdfBase64, Filename: "contrato.pdf"},
})

advanceURL := fmt.Sprintf("%s/v1/signing-sessions/%s/advance",
    os.Getenv("SIGNDOCS_BASE_URL"), session.SessionID)

// Passo 1: Iniciar liveness
startBody, _ := json.Marshal(map[string]string{"action": "start_liveness"})
startReq, _ := http.NewRequest("POST", advanceURL, bytes.NewReader(startBody))
startReq.Header.Set("Authorization", "Bearer "+session.ClientSecret)
startReq.Header.Set("Content-Type", "application/json")
startResp, _ := http.DefaultClient.Do(startReq)
var startResult map[string]interface{}
json.NewDecoder(startResp.Body).Decode(&startResult)
livenessSessionID := startResult["livenessSessionId"].(string)

// (Signatário completa a verificação facial)

// Passo 2: Completar liveness
completeBody, _ := json.Marshal(map[string]interface{}{
    "action": "complete_liveness", "livenessSessionId": livenessSessionID,
})
completeReq, _ := http.NewRequest("POST", advanceURL, bytes.NewReader(completeBody))
completeReq.Header.Set("Authorization", "Bearer "+session.ClientSecret)
completeReq.Header.Set("Content-Type", "application/json")
completeResp, _ := http.DefaultClient.Do(completeReq)
var completeResult map[string]interface{}
json.NewDecoder(completeResp.Body).Decode(&completeResult)
fmt.Println("Status:", completeResult["status"])       // COMPLETED
fmt.Println("Evidence ID:", completeResult["evidenceId"])
SigningSession session = client.signingSessions().create(request); // (ver hosted acima)

HttpClient http = HttpClient.newHttpClient();
String advanceUrl = System.getenv("SIGNDOCS_BASE_URL")
    + "/v1/signing-sessions/" + session.sessionId + "/advance";

// Passo 1: Iniciar liveness
HttpRequest startReq = HttpRequest.newBuilder().uri(URI.create(advanceUrl))
    .header("Authorization", "Bearer " + session.clientSecret)
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("{\"action\":\"start_liveness\"}")).build();
String startBody = http.send(startReq, HttpResponse.BodyHandlers.ofString()).body();
// Extrair hostedUrl e livenessSessionId

// (Signatário completa a verificação facial)

// Passo 2: Completar liveness
HttpRequest completeReq = HttpRequest.newBuilder().uri(URI.create(advanceUrl))
    .header("Authorization", "Bearer " + session.clientSecret)
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(
        "{\"action\":\"complete_liveness\",\"livenessSessionId\":\"" + livenessSessionId + "\"}")).build();
String completeBody = http.send(completeReq, HttpResponse.BodyHandlers.ofString()).body();
System.out.println(completeBody); // {"status":"COMPLETED","evidenceId":"..."}
$session = $client->signingSessions->create(/* ... ver hosted acima ... */);

$advanceUrl = getenv('SIGNDOCS_BASE_URL')
    . '/v1/signing-sessions/' . $session->sessionId . '/advance';
$headers = [
    'Authorization: Bearer ' . $session->clientSecret,
    'Content-Type: application/json',
];

// Passo 1: Iniciar liveness
$ch = curl_init($advanceUrl);
curl_setopt_array($ch, [
    CURLOPT_POST => true, CURLOPT_HTTPHEADER => $headers,
    CURLOPT_POSTFIELDS => json_encode(['action' => 'start_liveness']),
    CURLOPT_RETURNTRANSFER => true,
]);
$startBody = json_decode(curl_exec($ch));
$livenessSessionId = $startBody->livenessSessionId;

// (Signatário completa a verificação facial)

// Passo 2: Completar liveness
$ch = curl_init($advanceUrl);
curl_setopt_array($ch, [
    CURLOPT_POST => true, CURLOPT_HTTPHEADER => $headers,
    CURLOPT_POSTFIELDS => json_encode([
        'action' => 'complete_liveness',
        'livenessSessionId' => $livenessSessionId,
    ]),
    CURLOPT_RETURNTRANSFER => true,
]);
$completeBody = json_decode(curl_exec($ch));
echo "Status: " . $completeBody->status . "\n";       // COMPLETED
echo "Evidence ID: " . $completeBody->evidenceId . "\n";
var session = await client.SigningSessions.CreateAsync(/* ... ver hosted acima ... */);

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
    new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", session.ClientSecret);
var advanceUrl = $"{Environment.GetEnvironmentVariable("SIGNDOCS_BASE_URL")}/v1/signing-sessions/{session.SessionId}/advance";

// Passo 1: Iniciar liveness
var startRes = await http.PostAsync(advanceUrl,
    new StringContent("{\"action\":\"start_liveness\"}", System.Text.Encoding.UTF8, "application/json"));
var startJson = System.Text.Json.JsonDocument.Parse(await startRes.Content.ReadAsStringAsync());
var livenessSessionId = startJson.RootElement.GetProperty("livenessSessionId").GetString();

// (Signatário completa a verificação facial)

// Passo 2: Completar liveness
var completeRes = await http.PostAsync(advanceUrl,
    new StringContent($"{{\"action\":\"complete_liveness\",\"livenessSessionId\":\"{livenessSessionId}\"}}",
        System.Text.Encoding.UTF8, "application/json"));
Console.WriteLine(await completeRes.Content.ReadAsStringAsync());
// {"status":"COMPLETED","evidenceId":"..."}

HML: A etapa de liveness pode ser simulada em sandbox. Em produção, requer câmera do dispositivo.


6. Receita: DIGITAL_CERTIFICATE — Certificado A1

Assinatura via certificado digital ICP-Brasil. O fluxo usa protocolo de duas fases: o servidor prepara um hash, você assina localmente com a chave privada.

Backend: create session ──> clientSecret
                              │
Frontend: sd.checkout() ──> popup abre
                              │
Signatário: seleciona certificado ──> prepare_signing
                              │
Servidor: hashToSign ──> assinar localmente
                              │
                     complete_signing ──> COMPLETED
                              │
Backend: poll ou webhook ──> evidenceId

Pré-requisito: Certificado A1 (certificate.pem) e chave privada (private-key.pem). Consulte o Guia de Assinatura Digital para detalhes sobre o protocolo.

Fluxo hospedado (recomendado)

A página hospedada gerencia a seleção do certificado e a assinatura local. Seu backend só cria e espera.

const pdfBase64 = readFileSync('contrato.pdf').toString('base64');

const session = await client.signingSessions.create({
  purpose: 'DOCUMENT_SIGNATURE',
  policy: { profile: 'DIGITAL_CERTIFICATE' },
  signer: {
    name: 'Ana Pereira',
    cpf: '98765432100',
    userExternalId: 'user-004',
  },
  document: { content: pdfBase64, filename: 'contrato.pdf' },
  returnUrl: 'https://app.example.com/done',
  locale: 'pt-BR',
  expiresInMinutes: 60,
});

console.log('Client Secret:', session.clientSecret);

const result = await client.signingSessions.waitForCompletion(session.sessionId);
console.log('Status:', result.status);       // COMPLETED
console.log('Evidence ID:', result.evidenceId);
session = client.signing_sessions.create(CreateSigningSessionRequest(
    purpose='DOCUMENT_SIGNATURE',
    policy=Policy(profile='DIGITAL_CERTIFICATE'),
    signer=Signer(name='Ana Pereira', cpf='98765432100', user_external_id='user-004'),
    document=InlineDocument(content=pdf_base64, filename='contrato.pdf'),
    return_url='https://app.example.com/done',
    locale='pt-BR',
    expires_in_minutes=60,
))

print('Client Secret:', session.client_secret)

result = client.signing_sessions.wait_for_completion(session.session_id)
print('Status:', result.status)       # COMPLETED
print('Evidence ID:', result.evidence_id)
session, err := client.SigningSessions.Create(ctx, &signdocs.CreateSigningSessionRequest{
    Purpose:          signdocs.PurposeDocumentSignature,
    Policy:           signdocs.Policy{Profile: signdocs.PolicyProfileDigitalCertificate},
    Signer:           signdocs.Signer{Name: "Ana Pereira", CPF: "98765432100", UserExternalID: "user-004"},
    Document:         &signdocs.DocumentInline{Content: pdfBase64, Filename: "contrato.pdf"},
    ReturnURL:        "https://app.example.com/done",
    Locale:           "pt-BR",
    ExpiresInMinutes: 60,
})
if err != nil { log.Fatal(err) }

fmt.Println("Client Secret:", session.ClientSecret)

result, err := client.SigningSessions.WaitForCompletion(ctx, session.SessionID)
if err != nil { log.Fatal(err) }
fmt.Println("Status:", result.Status)       // COMPLETED
fmt.Println("Evidence ID:", result.EvidenceID)
String pdfBase64 = Base64.getEncoder().encodeToString(Files.readAllBytes(Path.of("contrato.pdf")));

CreateSigningSessionRequest request = new CreateSigningSessionRequest();
request.purpose = "DOCUMENT_SIGNATURE";
request.policy = new Policy("DIGITAL_CERTIFICATE");
request.signer = new Signer("Ana Pereira", "user-004");
request.signer.cpf = "98765432100";
request.document = new CreateSigningSessionRequest.InlineDocument(pdfBase64, "contrato.pdf");
request.returnUrl = "https://app.example.com/done";
request.locale = "pt-BR";
request.expiresInMinutes = 60;

SigningSession session = client.signingSessions().create(request);
System.out.println("Client Secret: " + session.clientSecret);

SigningSessionStatusResponse result = client.signingSessions().waitForCompletion(session.sessionId);
System.out.println("Status: " + result.status);       // COMPLETED
System.out.println("Evidence ID: " + result.evidenceId);
$pdfBase64 = base64_encode(file_get_contents('contrato.pdf'));

$session = $client->signingSessions->create(new CreateSigningSessionRequest(
    purpose: 'DOCUMENT_SIGNATURE',
    policy: new Policy(profile: 'DIGITAL_CERTIFICATE'),
    signer: new Signer(name: 'Ana Pereira', cpf: '98765432100', userExternalId: 'user-004'),
    document: ['content' => $pdfBase64, 'filename' => 'contrato.pdf'],
    returnUrl: 'https://app.example.com/done',
    locale: 'pt-BR',
    expiresInMinutes: 60,
));

echo "Client Secret: " . $session->clientSecret . "\n";

$result = $client->signingSessions->waitForCompletion($session->sessionId);
echo "Status: " . $result->status . "\n";       // COMPLETED
echo "Evidence ID: " . $result->evidenceId . "\n";
var pdfBase64 = Convert.ToBase64String(await File.ReadAllBytesAsync("contrato.pdf"));

var session = await client.SigningSessions.CreateAsync(new CreateSigningSessionRequest
{
    Purpose = "DOCUMENT_SIGNATURE",
    Policy = new Policy { Profile = "DIGITAL_CERTIFICATE" },
    Signer = new Signer { Name = "Ana Pereira", Cpf = "98765432100", UserExternalId = "user-004" },
    Document = new InlineDocument { Content = pdfBase64, Filename = "contrato.pdf" },
    ReturnUrl = "https://app.example.com/done",
    Locale = "pt-BR",
    ExpiresInMinutes = 60,
});

Console.WriteLine($"Client Secret: {session.ClientSecret}");

var result = await client.SigningSessions.WaitForCompletionAsync(session.SessionId);
Console.WriteLine($"Status: {result.Status}");       // COMPLETED
Console.WriteLine($"Evidence ID: {result.EvidenceId}");

Fluxo programático (headless)

Para assinatura server-side com certificado A1. Protocolo de 2 fases: prepare_signing → assinar localmente → complete_signing.

Algoritmo: RSASSA-PKCS1-v1_5 com SHA-256. O hashToSign já é o digest — não aplique hash novamente.

import { createPrivateKey, sign } from 'crypto';
import { readFileSync } from 'fs';

const session = await client.signingSessions.create({
  purpose: 'DOCUMENT_SIGNATURE',
  policy: { profile: 'DIGITAL_CERTIFICATE' },
  signer: { name: 'Ana Pereira', cpf: '98765432100', userExternalId: 'user-004' },
  document: { content: pdfBase64, filename: 'contrato.pdf' },
});

const baseUrl = process.env.SIGNDOCS_BASE_URL;
const headers = {
  'Authorization': `Bearer ${session.clientSecret}`,
  'Content-Type': 'application/json',
};
const advanceUrl = `${baseUrl}/v1/signing-sessions/${session.sessionId}/advance`;

// Passo 1: Aceitar o documento
await fetch(advanceUrl, {
  method: 'POST', headers,
  body: JSON.stringify({ action: 'accept' }),
});

// Passo 2: Preparar assinatura — enviar certificado, receber hash
const certPem = readFileSync('certificate.pem', 'utf-8');
const prepareRes = await fetch(advanceUrl, {
  method: 'POST', headers,
  body: JSON.stringify({
    action: 'prepare_signing',
    certificateChainPems: [certPem],
  }),
});
const prepareBody = await prepareRes.json();
const { signatureRequestId, hashToSign } = prepareBody;

// Passo 3: Assinar localmente com a chave privada
const privateKey = createPrivateKey(readFileSync('private-key.pem'));
const hashBuffer = Buffer.from(hashToSign, 'hex');
const signature = sign(null, hashBuffer, {
  key: privateKey,
  padding: 1, // RSA_PKCS1_PADDING
});
const rawSignatureBase64 = signature.toString('base64');

// Passo 4: Completar assinatura
const completeRes = await fetch(advanceUrl, {
  method: 'POST', headers,
  body: JSON.stringify({
    action: 'complete_signing',
    signatureRequestId,
    rawSignatureBase64,
  }),
});
const completeBody = await completeRes.json();
console.log('Status:', completeBody.status);       // COMPLETED
console.log('Evidence ID:', completeBody.evidenceId);
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, utils

session = client.signing_sessions.create(CreateSigningSessionRequest(
    purpose='DOCUMENT_SIGNATURE',
    policy=Policy(profile='DIGITAL_CERTIFICATE'),
    signer=Signer(name='Ana Pereira', cpf='98765432100', user_external_id='user-004'),
    document=InlineDocument(content=pdf_base64, filename='contrato.pdf'),
))

base_url = os.environ['SIGNDOCS_BASE_URL']
headers = {'Authorization': f'Bearer {session.client_secret}', 'Content-Type': 'application/json'}
advance_url = f'{base_url}/v1/signing-sessions/{session.session_id}/advance'

# Passo 1: Aceitar
requests.post(advance_url, headers=headers, json={'action': 'accept'})

# Passo 2: Preparar assinatura
with open('certificate.pem') as f:
    cert_pem = f.read()
prepare_res = requests.post(advance_url, headers=headers, json={
    'action': 'prepare_signing',
    'certificateChainPems': [cert_pem],
})
prepare_body = prepare_res.json()
signature_request_id = prepare_body['signatureRequestId']
hash_to_sign = bytes.fromhex(prepare_body['hashToSign'])

# Passo 3: Assinar localmente
with open('private-key.pem', 'rb') as f:
    private_key = serialization.load_pem_private_key(f.read(), password=None)
raw_signature = private_key.sign(
    hash_to_sign,
    padding.PKCS1v15(),
    utils.Prehashed(hashes.SHA256()),
)
raw_signature_base64 = base64.b64encode(raw_signature).decode()

# Passo 4: Completar assinatura
complete_res = requests.post(advance_url, headers=headers, json={
    'action': 'complete_signing',
    'signatureRequestId': signature_request_id,
    'rawSignatureBase64': raw_signature_base64,
})
complete_body = complete_res.json()
print('Status:', complete_body['status'])       # COMPLETED
print('Evidence ID:', complete_body['evidenceId'])
import (
    "crypto"
    "crypto/rsa"
    "crypto/x509"
    "encoding/pem"
)

session, _ := client.SigningSessions.Create(ctx, &signdocs.CreateSigningSessionRequest{
    Purpose:  signdocs.PurposeDocumentSignature,
    Policy:   signdocs.Policy{Profile: signdocs.PolicyProfileDigitalCertificate},
    Signer:   signdocs.Signer{Name: "Ana Pereira", CPF: "98765432100", UserExternalID: "user-004"},
    Document: &signdocs.DocumentInline{Content: pdfBase64, Filename: "contrato.pdf"},
})

advanceURL := fmt.Sprintf("%s/v1/signing-sessions/%s/advance",
    os.Getenv("SIGNDOCS_BASE_URL"), session.SessionID)
doAdvance := func(payload interface{}) map[string]interface{} {
    body, _ := json.Marshal(payload)
    req, _ := http.NewRequest("POST", advanceURL, bytes.NewReader(body))
    req.Header.Set("Authorization", "Bearer "+session.ClientSecret)
    req.Header.Set("Content-Type", "application/json")
    resp, _ := http.DefaultClient.Do(req)
    var result map[string]interface{}
    json.NewDecoder(resp.Body).Decode(&result)
    return result
}

// Passo 1: Aceitar
doAdvance(map[string]string{"action": "accept"})

// Passo 2: Preparar assinatura
certPEM, _ := os.ReadFile("certificate.pem")
prepareResult := doAdvance(map[string]interface{}{
    "action": "prepare_signing", "certificateChainPems": []string{string(certPEM)},
})
signatureRequestID := prepareResult["signatureRequestId"].(string)
hashHex := prepareResult["hashToSign"].(string)
hashBytes, _ := hex.DecodeString(hashHex)

// Passo 3: Assinar localmente
keyPEM, _ := os.ReadFile("private-key.pem")
block, _ := pem.Decode(keyPEM)
key, _ := x509.ParsePKCS8PrivateKey(block.Bytes)
rsaKey := key.(*rsa.PrivateKey)
signature, _ := rsa.SignPKCS1v15(rand.Reader, rsaKey, crypto.SHA256, hashBytes)
rawSignatureBase64 := base64.StdEncoding.EncodeToString(signature)

// Passo 4: Completar assinatura
completeResult := doAdvance(map[string]interface{}{
    "action": "complete_signing",
    "signatureRequestId": signatureRequestID,
    "rawSignatureBase64": rawSignatureBase64,
})
fmt.Println("Status:", completeResult["status"])       // COMPLETED
fmt.Println("Evidence ID:", completeResult["evidenceId"])
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;

SigningSession session = client.signingSessions().create(request); // (ver hosted acima)

HttpClient http = HttpClient.newHttpClient();
String advanceUrl = System.getenv("SIGNDOCS_BASE_URL")
    + "/v1/signing-sessions/" + session.sessionId + "/advance";

// Passo 1: Aceitar
http.send(HttpRequest.newBuilder().uri(URI.create(advanceUrl))
    .header("Authorization", "Bearer " + session.clientSecret)
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("{\"action\":\"accept\"}")).build(),
    HttpResponse.BodyHandlers.ofString());

// Passo 2: Preparar assinatura
String certPem = Files.readString(Path.of("certificate.pem"));
HttpRequest prepareReq = HttpRequest.newBuilder().uri(URI.create(advanceUrl))
    .header("Authorization", "Bearer " + session.clientSecret)
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(
        "{\"action\":\"prepare_signing\",\"certificateChainPems\":[\"" + certPem.replace("\n","\\n") + "\"]}"))
    .build();
String prepareBody = http.send(prepareReq, HttpResponse.BodyHandlers.ofString()).body();
// Extrair signatureRequestId e hashToSign (use sua lib JSON preferida)

// Passo 3: Assinar localmente (NONEwithRSA — hash já é digest)
byte[] hashBytes = hexToBytes(hashToSign);
Signature sig = Signature.getInstance("NONEwithRSA");
sig.initSign(privateKey); // Carregar de private-key.pem
// Aplicar DigestInfo wrapping: SHA-256 OID prefix + hashBytes
byte[] digestInfo = wrapDigestInfo(hashBytes);
sig.update(digestInfo);
String rawSignatureBase64 = Base64.getEncoder().encodeToString(sig.sign());

// Passo 4: Completar assinatura
HttpRequest completeReq = HttpRequest.newBuilder().uri(URI.create(advanceUrl))
    .header("Authorization", "Bearer " + session.clientSecret)
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(
        "{\"action\":\"complete_signing\",\"signatureRequestId\":\"" + signatureRequestId
        + "\",\"rawSignatureBase64\":\"" + rawSignatureBase64 + "\"}"))
    .build();
System.out.println(http.send(completeReq, HttpResponse.BodyHandlers.ofString()).body());
$session = $client->signingSessions->create(/* ... ver hosted acima ... */);

$advanceUrl = getenv('SIGNDOCS_BASE_URL')
    . '/v1/signing-sessions/' . $session->sessionId . '/advance';
$headers = [
    'Authorization: Bearer ' . $session->clientSecret,
    'Content-Type: application/json',
];

$advance = function(array $payload) use ($advanceUrl, $headers) {
    $ch = curl_init($advanceUrl);
    curl_setopt_array($ch, [
        CURLOPT_POST => true, CURLOPT_HTTPHEADER => $headers,
        CURLOPT_POSTFIELDS => json_encode($payload),
        CURLOPT_RETURNTRANSFER => true,
    ]);
    return json_decode(curl_exec($ch));
};

// Passo 1: Aceitar
$advance(['action' => 'accept']);

// Passo 2: Preparar assinatura
$certPem = file_get_contents('certificate.pem');
$prepareBody = $advance([
    'action' => 'prepare_signing',
    'certificateChainPems' => [$certPem],
]);

// Passo 3: Assinar localmente
$privateKey = openssl_pkey_get_private(file_get_contents('private-key.pem'));
$hashBytes = hex2bin($prepareBody->hashToSign);
openssl_private_encrypt($hashBytes, $signature, $privateKey, OPENSSL_PKCS1_PADDING);
$rawSignatureBase64 = base64_encode($signature);

// Passo 4: Completar assinatura
$completeBody = $advance([
    'action' => 'complete_signing',
    'signatureRequestId' => $prepareBody->signatureRequestId,
    'rawSignatureBase64' => $rawSignatureBase64,
]);
echo "Status: " . $completeBody->status . "\n";       // COMPLETED
echo "Evidence ID: " . $completeBody->evidenceId . "\n";
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

var session = await client.SigningSessions.CreateAsync(/* ... ver hosted acima ... */);

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
    new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", session.ClientSecret);
var advanceUrl = $"{Environment.GetEnvironmentVariable("SIGNDOCS_BASE_URL")}/v1/signing-sessions/{session.SessionId}/advance";

async Task<System.Text.Json.JsonDocument> AdvanceAsync(object payload)
{
    var res = await http.PostAsync(advanceUrl,
        new StringContent(System.Text.Json.JsonSerializer.Serialize(payload),
            System.Text.Encoding.UTF8, "application/json"));
    return System.Text.Json.JsonDocument.Parse(await res.Content.ReadAsStringAsync());
}

// Passo 1: Aceitar
await AdvanceAsync(new { action = "accept" });

// Passo 2: Preparar assinatura
var certPem = await File.ReadAllTextAsync("certificate.pem");
var prepareJson = await AdvanceAsync(new {
    action = "prepare_signing",
    certificateChainPems = new[] { certPem },
});
var signatureRequestId = prepareJson.RootElement.GetProperty("signatureRequestId").GetString()!;
var hashHex = prepareJson.RootElement.GetProperty("hashToSign").GetString()!;
var hashBytes = Convert.FromHexString(hashHex);

// Passo 3: Assinar localmente
using var rsa = RSA.Create();
rsa.ImportFromPem(await File.ReadAllTextAsync("private-key.pem"));
var signature = rsa.SignHash(hashBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
var rawSignatureBase64 = Convert.ToBase64String(signature);

// Passo 4: Completar assinatura
var completeJson = await AdvanceAsync(new {
    action = "complete_signing",
    signatureRequestId,
    rawSignatureBase64,
});
Console.WriteLine($"Status: {completeJson.RootElement.GetProperty("status")}");       // COMPLETED
Console.WriteLine($"Evidence ID: {completeJson.RootElement.GetProperty("evidenceId")}");

7. Receita: BIOMETRIC_SERPRO_AUTO_FALLBACK — NT65 Consignado

Perfil consolidado para empréstimos consignados INSS (Nota Técnica 65). Combina biometria facial com verificação SERPRO e fallback automático para foto de documento quando o SERPRO não possui dados biométricos.

Pré-requisitos específicos

Sequência de etapas

PURPOSE_DISCLOSURE        (se NT65 habilitado)
       │
       ▼
BIOMETRIC_LIVENESS        captura facial
       │
       ▼
BIOMETRIC_MATCH           comparação facial
       │
       ▼
SERPRO_IDENTITY_CHECK     verificação biográfica + biométrica
       │
       ├── SERPRO OK ──────────────────────> COMPLETED
       │
       └── biometricAvailable: false ──┐
           (mas nome+nascimento OK)    │
                                       ▼
                          DOCUMENT_PHOTO_MATCH (era DORMANT → PENDING)
                                       │
                                       ▼
                                   COMPLETED

Comportamento do auto-fallback

O fallback dispara somente quando o SERPRO retorna que: - biometricAvailable: false (CPF não possui biometria no SERPRO) - nameMatch: true (dados biográficos conferem) - birthDateMatch: true (data de nascimento confere)

Nesse caso: 1. Etapa SERPRO → status SKIPPED com skipReason: 'BIOMETRIC_UNAVAILABLE' 2. Etapa DOCUMENT_PHOTO_MATCH (dormant) → status PENDING 3. Webhook TRANSACTION.FALLBACK emitido

O fallback NÃO dispara quando: - Score biométrico é baixo (falha normal) - Dados biográficos não conferem (CPF/nome/nascimento divergentes)

Fluxo hospedado (recomendado)

A página hospedada gerencia todas as etapas, incluindo o fallback. Seu backend só cria e espera.

const pdfBase64 = readFileSync('contrato.pdf').toString('base64');

const session = await client.signingSessions.create({
  purpose: 'DOCUMENT_SIGNATURE',
  policy: { profile: 'BIOMETRIC_SERPRO_AUTO_FALLBACK' },
  signer: {
    name: 'Roberto Santos',
    cpf: '12345678901',
    birthDate: '1985-03-15',
    userExternalId: 'user-005',
  },
  document: { content: pdfBase64, filename: 'contrato-consignado.pdf' },
  returnUrl: 'https://app.example.com/done',
  locale: 'pt-BR',
  expiresInMinutes: 60,
});

console.log('Session ID:', session.sessionId);
console.log('Client Secret:', session.clientSecret);

// A página hospedada cuida de:
// - PURPOSE_DISCLOSURE (reconhecimento de coleta biométrica)
// - BIOMETRIC_LIVENESS (captura facial)
// - BIOMETRIC_MATCH (comparação)
// - SERPRO_IDENTITY_CHECK (verificação)
// - DOCUMENT_PHOTO_MATCH (se fallback ativado — foto do documento)

const result = await client.signingSessions.waitForCompletion(session.sessionId);
console.log('Status:', result.status);       // COMPLETED
console.log('Evidence ID:', result.evidenceId);
session = client.signing_sessions.create(CreateSigningSessionRequest(
    purpose='DOCUMENT_SIGNATURE',
    policy=Policy(profile='BIOMETRIC_SERPRO_AUTO_FALLBACK'),
    signer=Signer(
        name='Roberto Santos',
        cpf='12345678901',
        birth_date='1985-03-15',
        user_external_id='user-005',
    ),
    document=InlineDocument(content=pdf_base64, filename='contrato-consignado.pdf'),
    return_url='https://app.example.com/done',
    locale='pt-BR',
    expires_in_minutes=60,
))

print('Session ID:', session.session_id)
print('Client Secret:', session.client_secret)

result = client.signing_sessions.wait_for_completion(session.session_id)
print('Status:', result.status)       # COMPLETED
print('Evidence ID:', result.evidence_id)
session, err := client.SigningSessions.Create(ctx, &signdocs.CreateSigningSessionRequest{
    Purpose: signdocs.PurposeDocumentSignature,
    Policy:  signdocs.Policy{Profile: "BIOMETRIC_SERPRO_AUTO_FALLBACK"},
    Signer: signdocs.Signer{
        Name: "Roberto Santos", CPF: "12345678901",
        BirthDate: "1985-03-15", UserExternalID: "user-005",
    },
    Document:         &signdocs.DocumentInline{Content: pdfBase64, Filename: "contrato-consignado.pdf"},
    ReturnURL:        "https://app.example.com/done",
    Locale:           "pt-BR",
    ExpiresInMinutes: 60,
})
if err != nil { log.Fatal(err) }

fmt.Println("Session ID:", session.SessionID)
fmt.Println("Client Secret:", session.ClientSecret)

result, err := client.SigningSessions.WaitForCompletion(ctx, session.SessionID)
if err != nil { log.Fatal(err) }
fmt.Println("Status:", result.Status)       // COMPLETED
fmt.Println("Evidence ID:", result.EvidenceID)
String pdfBase64 = Base64.getEncoder().encodeToString(
    Files.readAllBytes(Path.of("contrato-consignado.pdf")));

CreateSigningSessionRequest request = new CreateSigningSessionRequest();
request.purpose = "DOCUMENT_SIGNATURE";
request.policy = new Policy("BIOMETRIC_SERPRO_AUTO_FALLBACK");
request.signer = new Signer("Roberto Santos", "user-005");
request.signer.cpf = "12345678901";
request.signer.birthDate = "1985-03-15";
request.document = new CreateSigningSessionRequest.InlineDocument(pdfBase64, "contrato-consignado.pdf");
request.returnUrl = "https://app.example.com/done";
request.locale = "pt-BR";
request.expiresInMinutes = 60;

SigningSession session = client.signingSessions().create(request);
System.out.println("Session ID: " + session.sessionId);
System.out.println("Client Secret: " + session.clientSecret);

SigningSessionStatusResponse result = client.signingSessions().waitForCompletion(session.sessionId);
System.out.println("Status: " + result.status);       // COMPLETED
System.out.println("Evidence ID: " + result.evidenceId);
$pdfBase64 = base64_encode(file_get_contents('contrato-consignado.pdf'));

$session = $client->signingSessions->create(new CreateSigningSessionRequest(
    purpose: 'DOCUMENT_SIGNATURE',
    policy: new Policy(profile: 'BIOMETRIC_SERPRO_AUTO_FALLBACK'),
    signer: new Signer(
        name: 'Roberto Santos',
        cpf: '12345678901',
        birthDate: '1985-03-15',
        userExternalId: 'user-005',
    ),
    document: ['content' => $pdfBase64, 'filename' => 'contrato-consignado.pdf'],
    returnUrl: 'https://app.example.com/done',
    locale: 'pt-BR',
    expiresInMinutes: 60,
));

echo "Session ID: " . $session->sessionId . "\n";
echo "Client Secret: " . $session->clientSecret . "\n";

$result = $client->signingSessions->waitForCompletion($session->sessionId);
echo "Status: " . $result->status . "\n";       // COMPLETED
echo "Evidence ID: " . $result->evidenceId . "\n";
var pdfBase64 = Convert.ToBase64String(
    await File.ReadAllBytesAsync("contrato-consignado.pdf"));

var session = await client.SigningSessions.CreateAsync(new CreateSigningSessionRequest
{
    Purpose = "DOCUMENT_SIGNATURE",
    Policy = new Policy { Profile = "BIOMETRIC_SERPRO_AUTO_FALLBACK" },
    Signer = new Signer
    {
        Name = "Roberto Santos",
        Cpf = "12345678901",
        BirthDate = "1985-03-15",
        UserExternalId = "user-005",
    },
    Document = new InlineDocument { Content = pdfBase64, Filename = "contrato-consignado.pdf" },
    ReturnUrl = "https://app.example.com/done",
    Locale = "pt-BR",
    ExpiresInMinutes = 60,
});

Console.WriteLine($"Session ID: {session.SessionId}");
Console.WriteLine($"Client Secret: {session.ClientSecret}");

var result = await client.SigningSessions.WaitForCompletionAsync(session.SessionId);
Console.WriteLine($"Status: {result.Status}");       // COMPLETED
Console.WriteLine($"Evidence ID: {result.EvidenceId}");

Fluxo programático (avançado)

As etapas SERPRO são gerenciadas pela API de transações (/v1/transactions/{id}/steps/), não pelo endpoint advance. Para integração server-side completa, use a Transaction API diretamente.

// 1. Obter token OAuth2
const tokenRes = await fetch(`${baseUrl}/oauth2/token`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: `grant_type=client_credentials&client_id=${clientId}&client_secret=${clientSecret}`,
});
const { access_token } = await tokenRes.json();

// 2. Criar transação com perfil BIOMETRIC_SERPRO_AUTO_FALLBACK
const txRes = await fetch(`${baseUrl}/v1/transactions`, {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${access_token}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    purpose: 'DOCUMENT_SIGNATURE',
    policy: { profile: 'BIOMETRIC_SERPRO_AUTO_FALLBACK' },
    signer: { name: 'Roberto Santos', cpf: '12345678901', birthDate: '1985-03-15',
              email: 'roberto@example.com', userExternalId: 'user-005' },
  }),
});
const tx = await txRes.json();
const txId = tx.transactionId;

// 3. Upload do documento
await fetch(`${baseUrl}/v1/transactions/${txId}/document`, {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${access_token}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ content: pdfBase64 }),
});

// 4. Listar etapas
const stepsRes = await fetch(`${baseUrl}/v1/transactions/${txId}/steps`,
  { headers: { 'Authorization': `Bearer ${access_token}` } });
const steps = await stepsRes.json();
// steps: PURPOSE_DISCLOSURE, BIOMETRIC_LIVENESS, BIOMETRIC_MATCH,
//        SERPRO_IDENTITY_CHECK, DOCUMENT_PHOTO_MATCH (DORMANT)

// 5. Completar cada etapa em sequência (start → complete)
for (const step of steps.filter(s => s.status !== 'DORMANT')) {
  await fetch(`${baseUrl}/v1/transactions/${txId}/steps/${step.stepId}/start`,
    { method: 'POST', headers: { 'Authorization': `Bearer ${access_token}` } });

  // Payload depende do tipo de etapa:
  // PURPOSE_DISCLOSURE: { acknowledged: true }
  // BIOMETRIC_LIVENESS: (liveness session via hosted page)
  // BIOMETRIC_MATCH: (automático após liveness)
  // SERPRO_IDENTITY_CHECK: (automático — server-side)

  await fetch(`${baseUrl}/v1/transactions/${txId}/steps/${step.stepId}/complete`,
    { method: 'POST', headers: { 'Authorization': `Bearer ${access_token}`,
      'Content-Type': 'application/json' },
      body: JSON.stringify(/* payload específico da etapa */) });
}

// 6. Se fallback ativou, DOCUMENT_PHOTO_MATCH agora é PENDING
// Completar com foto do documento + geolocalização

// 7. Finalizar transação
const finalizeRes = await fetch(`${baseUrl}/v1/transactions/${txId}/finalize`,
  { method: 'POST', headers: { 'Authorization': `Bearer ${access_token}` } });
const finalized = await finalizeRes.json();
console.log('Evidence ID:', finalized.evidenceId);

Dicas de sandbox

CPF Comportamento
11111111111 biometricAvailable: falsedispara fallback
12345678901 SERPRO sucesso — fluxo sem fallback

Geolocalização

Para etapas biométricas com biometricRequired ativo, geolocalização é obrigatória:

{
  "geolocation": {
    "latitude": -23.5505,
    "longitude": -46.6333,
    "accuracy": 10.0,
    "source": "GPS"
  }
}

Documentação completa NT65: nt65-biometria-consignado.md


8. Webhook (alternativa ao polling)

Em vez de waitForCompletion (polling), registre um webhook para receber eventos em tempo real.

Eventos relevantes

Evento Quando
SIGNING_SESSION.COMPLETED Sessão concluída com sucesso
SIGNING_SESSION.EXPIRED Sessão expirou
SIGNING_SESSION.CANCELLED Sessão cancelada
TRANSACTION.FALLBACK Auto-fallback ativado (NT65)

Payload do webhook

{
  "event": "SIGNING_SESSION.COMPLETED",
  "payload": {
    "sessionId": "ss_abc123",
    "transactionId": "tx_def456",
    "status": "COMPLETED",
    "evidenceId": "ev_ghi789",
    "completedAt": "2026-03-10T14:30:00Z"
  },
  "signature": "sha256=..."
}
const express = require('express');
const crypto = require('crypto');
const app = express();

app.post('/webhooks/signdocs', express.raw({ type: 'application/json' }), (req, res) => {
  // Verificar assinatura
  const signature = req.headers['x-signdocs-signature'];
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(req.body)
    .digest('hex');

  if (signature !== expected) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body);

  switch (event.event) {
    case 'SIGNING_SESSION.COMPLETED':
      console.log('Sessão concluída:', event.payload.sessionId);
      console.log('Evidence ID:', event.payload.evidenceId);
      // Atualizar seu banco de dados
      break;
    case 'TRANSACTION.FALLBACK':
      console.log('Fallback ativado:', event.payload.transactionId);
      // Notificar equipe de compliance
      break;
  }

  res.status(200).send('OK');
});

app.listen(3000);
import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.post('/webhooks/signdocs')
def handle_webhook():
    signature = request.headers.get('X-SignDocs-Signature', '')
    expected = 'sha256=' + hmac.new(
        os.environ['WEBHOOK_SECRET'].encode(),
        request.data,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        return 'Invalid signature', 401

    event = request.get_json()

    if event['event'] == 'SIGNING_SESSION.COMPLETED':
        print('Sessão concluída:', event['payload']['sessionId'])
        print('Evidence ID:', event['payload']['evidenceId'])
    elif event['event'] == 'TRANSACTION.FALLBACK':
        print('Fallback ativado:', event['payload']['transactionId'])

    return 'OK', 200
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("webhooks")]
public class WebhookController : ControllerBase
{
    [HttpPost("signdocs")]
    public async Task<IActionResult> HandleWebhook()
    {
        using var reader = new StreamReader(Request.Body);
        var body = await reader.ReadToEndAsync();

        var signature = Request.Headers["X-SignDocs-Signature"].ToString();
        var secret = Environment.GetEnvironmentVariable("WEBHOOK_SECRET")!;
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
        var expected = "sha256=" + Convert.ToHexString(
            hmac.ComputeHash(Encoding.UTF8.GetBytes(body))).ToLower();

        if (signature != expected)
            return Unauthorized("Invalid signature");

        var doc = System.Text.Json.JsonDocument.Parse(body);
        var eventType = doc.RootElement.GetProperty("event").GetString();

        switch (eventType)
        {
            case "SIGNING_SESSION.COMPLETED":
                var sessionId = doc.RootElement.GetProperty("payload")
                    .GetProperty("sessionId").GetString();
                Console.WriteLine($"Sessão concluída: {sessionId}");
                break;
            case "TRANSACTION.FALLBACK":
                var txId = doc.RootElement.GetProperty("payload")
                    .GetProperty("transactionId").GetString();
                Console.WriteLine($"Fallback ativado: {txId}");
                break;
        }

        return Ok("OK");
    }
}

Guia completo de webhooks: webhooks-guide.md


9. Troubleshooting

Erro Causa Solução
409 Session is COMPLETED Tentou avançar uma sessão já concluída Verifique o status antes de chamar advance
401 Token expired clientSecret expirou (padrão: 60min) Crie nova sessão ou aumente expiresInMinutes
422 Missing required field: signer.cpf Perfil biométrico sem CPF Inclua signer.cpf (11 dígitos, sem formatação)
422 Missing required field: signer.email Perfil OTP sem email Inclua signer.email ou use otpChannel: 'sms' com signer.phone
422 Missing required field: signer.birthDate Perfil SERPRO sem data de nascimento Inclua signer.birthDate (YYYY-MM-DD)
400 Document too large PDF > 10MB em base64 Reduza o tamanho do PDF
400 Profile requires feature flag Tenant sem feature flag habilitada Contate suporte para habilitar o perfil
Popup bloqueado pelo navegador sd.checkout() fora de evento de clique Vincule a chamada a um click handler
409 Signature request expired Mais de 15min entre prepare_signing e complete_signing Chame prepare_signing novamente
429 Rate limit exceeded Cota diária/mensal excedida Verifique headers X-RateLimit-* na resposta

Referência completa de erros: error-reference.md


10. Receita: Envelope PARALLEL com 2 signatários CLICK_ONLY

Cria um envelope com modo paralelo, adiciona 2 signatários com perfil CLICK_ONLY e imprime as URLs de assinatura.

Backend: create envelope ──> envelopeId
                              │
        add session 1 ──> url1, clientSecret1
        add session 2 ──> url2, clientSecret2
                              │
Signatários assinam em paralelo ──> ENVELOPE.COMPLETED
                              │
Backend: poll ou webhook ──> combinedStamp
import { readFileSync } from 'fs';

const pdfBase64 = readFileSync('contrato.pdf').toString('base64');

// 1. Criar envelope
const envelope = await client.envelopes.create({
  signingMode: 'PARALLEL',
  totalSigners: 2,
  document: { content: pdfBase64, filename: 'contrato.pdf' },
  returnUrl: 'https://app.example.com/done',
  locale: 'pt-BR',
  expiresInMinutes: 1440,
});
console.log('Envelope ID:', envelope.envelopeId);

// 2. Adicionar signatário 1
const session1 = await client.envelopes.addSession(envelope.envelopeId, {
  signer: { name: 'João Silva', email: 'joao@example.com', userExternalId: 'user-001' },
  policy: { profile: 'CLICK_ONLY' },
  purpose: 'DOCUMENT_SIGNATURE',
  signerIndex: 1,
});

// 3. Adicionar signatário 2
const session2 = await client.envelopes.addSession(envelope.envelopeId, {
  signer: { name: 'Maria Souza', email: 'maria@example.com', userExternalId: 'user-002' },
  policy: { profile: 'CLICK_ONLY' },
  purpose: 'DOCUMENT_SIGNATURE',
  signerIndex: 2,
});

console.log('URL Signatário 1:', session1.url);
console.log('URL Signatário 2:', session2.url);
import base64
from signdocs_brasil.models import CreateEnvelopeRequest, AddEnvelopeSessionRequest

with open('contrato.pdf', 'rb') as f:
    pdf_base64 = base64.b64encode(f.read()).decode()

# 1. Criar envelope
envelope = client.envelopes.create(CreateEnvelopeRequest(
    signing_mode='PARALLEL',
    total_signers=2,
    document_content=pdf_base64,
    document_filename='contrato.pdf',
    return_url='https://app.example.com/done',
    locale='pt-BR',
    expires_in_minutes=1440,
))
print('Envelope ID:', envelope.envelope_id)

# 2. Adicionar signatário 1
session1 = client.envelopes.add_session(envelope.envelope_id, AddEnvelopeSessionRequest(
    signer_name='João Silva',
    signer_email='joao@example.com',
    signer_user_external_id='user-001',
    policy_profile='CLICK_ONLY',
    purpose='DOCUMENT_SIGNATURE',
    signer_index=1,
))

# 3. Adicionar signatário 2
session2 = client.envelopes.add_session(envelope.envelope_id, AddEnvelopeSessionRequest(
    signer_name='Maria Souza',
    signer_email='maria@example.com',
    signer_user_external_id='user-002',
    policy_profile='CLICK_ONLY',
    purpose='DOCUMENT_SIGNATURE',
    signer_index=2,
))

print('URL Signatário 1:', session1.url)
print('URL Signatário 2:', session2.url)
pdfBytes, _ := os.ReadFile("contrato.pdf")
pdfBase64 := base64.StdEncoding.EncodeToString(pdfBytes)

// 1. Criar envelope
envelope, err := client.Envelopes.Create(ctx, &signdocs.CreateEnvelopeRequest{
    SigningMode:       signdocs.SigningModeParallel,
    TotalSigners:     2,
    DocumentContent:  pdfBase64,
    DocumentFilename: "contrato.pdf",
    ReturnURL:        "https://app.example.com/done",
    Locale:           "pt-BR",
    ExpiresInMinutes: 1440,
})
if err != nil { log.Fatal(err) }
fmt.Println("Envelope ID:", envelope.EnvelopeID)

// 2. Adicionar signatário 1
session1, err := client.Envelopes.AddSession(ctx, envelope.EnvelopeID, &signdocs.AddEnvelopeSessionRequest{
    SignerName:           "João Silva",
    SignerEmail:          "joao@example.com",
    SignerUserExternalID: "user-001",
    PolicyProfile:       signdocs.PolicyProfileClickOnly,
    Purpose:             signdocs.PurposeDocumentSignature,
    SignerIndex:          1,
})
if err != nil { log.Fatal(err) }

// 3. Adicionar signatário 2
session2, err := client.Envelopes.AddSession(ctx, envelope.EnvelopeID, &signdocs.AddEnvelopeSessionRequest{
    SignerName:           "Maria Souza",
    SignerEmail:          "maria@example.com",
    SignerUserExternalID: "user-002",
    PolicyProfile:       signdocs.PolicyProfileClickOnly,
    Purpose:             signdocs.PurposeDocumentSignature,
    SignerIndex:          2,
})
if err != nil { log.Fatal(err) }

fmt.Println("URL Signatário 1:", session1.URL)
fmt.Println("URL Signatário 2:", session2.URL)
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;

String pdfBase64 = Base64.getEncoder().encodeToString(Files.readAllBytes(Path.of("contrato.pdf")));

// 1. Criar envelope
CreateEnvelopeRequest envReq = new CreateEnvelopeRequest();
envReq.signingMode = "PARALLEL";
envReq.totalSigners = 2;
envReq.document = new CreateEnvelopeRequest.Document(pdfBase64, "contrato.pdf");
envReq.returnUrl = "https://app.example.com/done";
envReq.locale = "pt-BR";
envReq.expiresInMinutes = 1440;

Envelope envelope = client.envelopes().create(envReq);
System.out.println("Envelope ID: " + envelope.envelopeId);

// 2. Adicionar signatário 1
AddEnvelopeSessionRequest req1 = new AddEnvelopeSessionRequest();
req1.signer = new AddEnvelopeSessionRequest.Signer("João Silva", "joao@example.com", "user-001");
req1.policy = new Policy("CLICK_ONLY");
req1.purpose = "DOCUMENT_SIGNATURE";
req1.signerIndex = 1;
EnvelopeSession session1 = client.envelopes().addSession(envelope.envelopeId, req1);

// 3. Adicionar signatário 2
AddEnvelopeSessionRequest req2 = new AddEnvelopeSessionRequest();
req2.signer = new AddEnvelopeSessionRequest.Signer("Maria Souza", "maria@example.com", "user-002");
req2.policy = new Policy("CLICK_ONLY");
req2.purpose = "DOCUMENT_SIGNATURE";
req2.signerIndex = 2;
EnvelopeSession session2 = client.envelopes().addSession(envelope.envelopeId, req2);

System.out.println("URL Signatário 1: " + session1.url);
System.out.println("URL Signatário 2: " + session2.url);
$pdfBase64 = base64_encode(file_get_contents('contrato.pdf'));

// 1. Criar envelope
$envelope = $client->envelopes->create(new CreateEnvelopeRequest(
    signingMode: 'PARALLEL',
    totalSigners: 2,
    documentContent: $pdfBase64,
    documentFilename: 'contrato.pdf',
    returnUrl: 'https://app.example.com/done',
    locale: 'pt-BR',
    expiresInMinutes: 1440,
));
echo "Envelope ID: " . $envelope->envelopeId . "\n";

// 2. Adicionar signatário 1
$session1 = $client->envelopes->addSession($envelope->envelopeId, new AddEnvelopeSessionRequest(
    signerName: 'João Silva',
    signerEmail: 'joao@example.com',
    signerUserExternalId: 'user-001',
    policyProfile: 'CLICK_ONLY',
    purpose: 'DOCUMENT_SIGNATURE',
    signerIndex: 1,
));

// 3. Adicionar signatário 2
$session2 = $client->envelopes->addSession($envelope->envelopeId, new AddEnvelopeSessionRequest(
    signerName: 'Maria Souza',
    signerEmail: 'maria@example.com',
    signerUserExternalId: 'user-002',
    policyProfile: 'CLICK_ONLY',
    purpose: 'DOCUMENT_SIGNATURE',
    signerIndex: 2,
));

echo "URL Signatário 1: " . $session1->url . "\n";
echo "URL Signatário 2: " . $session2->url . "\n";
var pdfBase64 = Convert.ToBase64String(File.ReadAllBytes("contrato.pdf"));

// 1. Criar envelope
var envelope = await client.Envelopes.CreateAsync(new CreateEnvelopeRequest
{
    SigningMode = "PARALLEL",
    TotalSigners = 2,
    Document = new EnvelopeDocument
    {
        Content = pdfBase64,
        Filename = "contrato.pdf",
    },
    ReturnUrl = "https://app.example.com/done",
    Locale = "pt-BR",
    ExpiresInMinutes = 1440,
});
Console.WriteLine($"Envelope ID: {envelope.EnvelopeId}");

// 2. Adicionar signatário 1
var session1 = await client.Envelopes.AddSessionAsync(envelope.EnvelopeId, new AddEnvelopeSessionRequest
{
    Signer = new Signer
    {
        UserExternalId = "user-001",
        Name = "João Silva",
        Email = "joao@example.com",
    },
    Policy = new Policy { Profile = "CLICK_ONLY" },
    SignerIndex = 1,
});

// 3. Adicionar signatário 2
var session2 = await client.Envelopes.AddSessionAsync(envelope.EnvelopeId, new AddEnvelopeSessionRequest
{
    Signer = new Signer
    {
        UserExternalId = "user-002",
        Name = "Maria Souza",
        Email = "maria@example.com",
    },
    Policy = new Policy { Profile = "CLICK_ONLY" },
    SignerIndex = 2,
});

Console.WriteLine($"URL Signatário 1: {session1.Url}");
Console.WriteLine($"URL Signatário 2: {session2.Url}");

11. Receita: Envelope SEQUENTIAL com 3 signatários BIOMETRIC

Cria um envelope com modo sequencial e 3 signatários com verificação biométrica. Cada signatário só pode assinar após o anterior concluir.

Backend: create envelope (SEQUENTIAL) ──> envelopeId
                              │
        add session 1 (signerIndex: 1) ──> url1
        add session 2 (signerIndex: 2) ──> url2 (aguarda session 1)
        add session 3 (signerIndex: 3) ──> url3 (aguarda session 2)
                              │
Signatário 1 assina ──> Signatário 2 assina ──> Signatário 3 assina
                              │
              ENVELOPE.COMPLETED

Obrigatório: signer.cpf para perfil BIOMETRIC.

import { readFileSync } from 'fs';

const pdfBase64 = readFileSync('contrato.pdf').toString('base64');

// 1. Criar envelope sequencial
const envelope = await client.envelopes.create({
  signingMode: 'SEQUENTIAL',
  totalSigners: 3,
  document: { content: pdfBase64, filename: 'contrato.pdf' },
  returnUrl: 'https://app.example.com/done',
  locale: 'pt-BR',
  expiresInMinutes: 4320,
});
console.log('Envelope ID:', envelope.envelopeId);

// 2. Adicionar signatário 1 — assina primeiro
const session1 = await client.envelopes.addSession(envelope.envelopeId, {
  signer: { name: 'João Silva', cpf: '12345678900', email: 'joao@example.com', userExternalId: 'user-001' },
  policy: { profile: 'BIOMETRIC' },
  purpose: 'DOCUMENT_SIGNATURE',
  signerIndex: 1,
});

// 3. Adicionar signatário 2 — assina após signatário 1
const session2 = await client.envelopes.addSession(envelope.envelopeId, {
  signer: { name: 'Maria Souza', cpf: '98765432100', email: 'maria@example.com', userExternalId: 'user-002' },
  policy: { profile: 'BIOMETRIC' },
  purpose: 'DOCUMENT_SIGNATURE',
  signerIndex: 2,
});

// 4. Adicionar signatário 3 — assina por último
const session3 = await client.envelopes.addSession(envelope.envelopeId, {
  signer: { name: 'Carlos Lima', cpf: '11122233344', email: 'carlos@example.com', userExternalId: 'user-003' },
  policy: { profile: 'BIOMETRIC' },
  purpose: 'DOCUMENT_SIGNATURE',
  signerIndex: 3,
});

console.log('URL Signatário 1:', session1.url);
console.log('URL Signatário 2:', session2.url);
console.log('URL Signatário 3:', session3.url);
import base64
from signdocs_brasil.models import CreateEnvelopeRequest, AddEnvelopeSessionRequest

with open('contrato.pdf', 'rb') as f:
    pdf_base64 = base64.b64encode(f.read()).decode()

# 1. Criar envelope sequencial
envelope = client.envelopes.create(CreateEnvelopeRequest(
    signing_mode='SEQUENTIAL',
    total_signers=3,
    document_content=pdf_base64,
    document_filename='contrato.pdf',
    return_url='https://app.example.com/done',
    locale='pt-BR',
    expires_in_minutes=4320,
))
print('Envelope ID:', envelope.envelope_id)

# 2. Adicionar signatário 1 — assina primeiro
session1 = client.envelopes.add_session(envelope.envelope_id, AddEnvelopeSessionRequest(
    signer_name='João Silva',
    signer_cpf='12345678900',
    signer_email='joao@example.com',
    signer_user_external_id='user-001',
    policy_profile='BIOMETRIC',
    purpose='DOCUMENT_SIGNATURE',
    signer_index=1,
))

# 3. Adicionar signatário 2 — assina após signatário 1
session2 = client.envelopes.add_session(envelope.envelope_id, AddEnvelopeSessionRequest(
    signer_name='Maria Souza',
    signer_cpf='98765432100',
    signer_email='maria@example.com',
    signer_user_external_id='user-002',
    policy_profile='BIOMETRIC',
    purpose='DOCUMENT_SIGNATURE',
    signer_index=2,
))

# 4. Adicionar signatário 3 — assina por último
session3 = client.envelopes.add_session(envelope.envelope_id, AddEnvelopeSessionRequest(
    signer_name='Carlos Lima',
    signer_cpf='11122233344',
    signer_email='carlos@example.com',
    signer_user_external_id='user-003',
    policy_profile='BIOMETRIC',
    purpose='DOCUMENT_SIGNATURE',
    signer_index=3,
))

print('URL Signatário 1:', session1.url)
print('URL Signatário 2:', session2.url)
print('URL Signatário 3:', session3.url)
pdfBytes, _ := os.ReadFile("contrato.pdf")
pdfBase64 := base64.StdEncoding.EncodeToString(pdfBytes)

// 1. Criar envelope sequencial
envelope, err := client.Envelopes.Create(ctx, &signdocs.CreateEnvelopeRequest{
    SigningMode:       signdocs.SigningModeSequential,
    TotalSigners:     3,
    DocumentContent:  pdfBase64,
    DocumentFilename: "contrato.pdf",
    ReturnURL:        "https://app.example.com/done",
    Locale:           "pt-BR",
    ExpiresInMinutes: 4320,
})
if err != nil { log.Fatal(err) }
fmt.Println("Envelope ID:", envelope.EnvelopeID)

// 2. Adicionar signatário 1 — assina primeiro
session1, err := client.Envelopes.AddSession(ctx, envelope.EnvelopeID, &signdocs.AddEnvelopeSessionRequest{
    SignerName:           "João Silva",
    SignerCPF:            "12345678900",
    SignerEmail:          "joao@example.com",
    SignerUserExternalID: "user-001",
    PolicyProfile:       signdocs.PolicyProfileBiometric,
    Purpose:             signdocs.PurposeDocumentSignature,
    SignerIndex:          1,
})
if err != nil { log.Fatal(err) }

// 3. Adicionar signatário 2 — assina após signatário 1
session2, err := client.Envelopes.AddSession(ctx, envelope.EnvelopeID, &signdocs.AddEnvelopeSessionRequest{
    SignerName:           "Maria Souza",
    SignerCPF:            "98765432100",
    SignerEmail:          "maria@example.com",
    SignerUserExternalID: "user-002",
    PolicyProfile:       signdocs.PolicyProfileBiometric,
    Purpose:             signdocs.PurposeDocumentSignature,
    SignerIndex:          2,
})
if err != nil { log.Fatal(err) }

// 4. Adicionar signatário 3 — assina por último
session3, err := client.Envelopes.AddSession(ctx, envelope.EnvelopeID, &signdocs.AddEnvelopeSessionRequest{
    SignerName:           "Carlos Lima",
    SignerCPF:            "11122233344",
    SignerEmail:          "carlos@example.com",
    SignerUserExternalID: "user-003",
    PolicyProfile:       signdocs.PolicyProfileBiometric,
    Purpose:             signdocs.PurposeDocumentSignature,
    SignerIndex:          3,
})
if err != nil { log.Fatal(err) }

fmt.Println("URL Signatário 1:", session1.URL)
fmt.Println("URL Signatário 2:", session2.URL)
fmt.Println("URL Signatário 3:", session3.URL)
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;

String pdfBase64 = Base64.getEncoder().encodeToString(Files.readAllBytes(Path.of("contrato.pdf")));

// 1. Criar envelope sequencial
CreateEnvelopeRequest envReq = new CreateEnvelopeRequest();
envReq.signingMode = "SEQUENTIAL";
envReq.totalSigners = 3;
envReq.document = new CreateEnvelopeRequest.Document(pdfBase64, "contrato.pdf");
envReq.returnUrl = "https://app.example.com/done";
envReq.locale = "pt-BR";
envReq.expiresInMinutes = 4320;

Envelope envelope = client.envelopes().create(envReq);
System.out.println("Envelope ID: " + envelope.envelopeId);

// 2. Adicionar signatário 1 — assina primeiro
AddEnvelopeSessionRequest req1 = new AddEnvelopeSessionRequest();
req1.signer = new AddEnvelopeSessionRequest.Signer("João Silva", "joao@example.com", "user-001");
req1.signer.cpf = "12345678900";
req1.policy = new Policy("BIOMETRIC");
req1.purpose = "DOCUMENT_SIGNATURE";
req1.signerIndex = 1;
EnvelopeSession session1 = client.envelopes().addSession(envelope.envelopeId, req1);

// 3. Adicionar signatário 2 — assina após signatário 1
AddEnvelopeSessionRequest req2 = new AddEnvelopeSessionRequest();
req2.signer = new AddEnvelopeSessionRequest.Signer("Maria Souza", "maria@example.com", "user-002");
req2.signer.cpf = "98765432100";
req2.policy = new Policy("BIOMETRIC");
req2.purpose = "DOCUMENT_SIGNATURE";
req2.signerIndex = 2;
EnvelopeSession session2 = client.envelopes().addSession(envelope.envelopeId, req2);

// 4. Adicionar signatário 3 — assina por último
AddEnvelopeSessionRequest req3 = new AddEnvelopeSessionRequest();
req3.signer = new AddEnvelopeSessionRequest.Signer("Carlos Lima", "carlos@example.com", "user-003");
req3.signer.cpf = "11122233344";
req3.policy = new Policy("BIOMETRIC");
req3.purpose = "DOCUMENT_SIGNATURE";
req3.signerIndex = 3;
EnvelopeSession session3 = client.envelopes().addSession(envelope.envelopeId, req3);

System.out.println("URL Signatário 1: " + session1.url);
System.out.println("URL Signatário 2: " + session2.url);
System.out.println("URL Signatário 3: " + session3.url);
$pdfBase64 = base64_encode(file_get_contents('contrato.pdf'));

// 1. Criar envelope sequencial
$envelope = $client->envelopes->create(new CreateEnvelopeRequest(
    signingMode: 'SEQUENTIAL',
    totalSigners: 3,
    documentContent: $pdfBase64,
    documentFilename: 'contrato.pdf',
    returnUrl: 'https://app.example.com/done',
    locale: 'pt-BR',
    expiresInMinutes: 4320,
));
echo "Envelope ID: " . $envelope->envelopeId . "\n";

// 2. Adicionar signatário 1 — assina primeiro
$session1 = $client->envelopes->addSession($envelope->envelopeId, new AddEnvelopeSessionRequest(
    signerName: 'João Silva',
    signerCpf: '12345678900',
    signerEmail: 'joao@example.com',
    signerUserExternalId: 'user-001',
    policyProfile: 'BIOMETRIC',
    purpose: 'DOCUMENT_SIGNATURE',
    signerIndex: 1,
));

// 3. Adicionar signatário 2 — assina após signatário 1
$session2 = $client->envelopes->addSession($envelope->envelopeId, new AddEnvelopeSessionRequest(
    signerName: 'Maria Souza',
    signerCpf: '98765432100',
    signerEmail: 'maria@example.com',
    signerUserExternalId: 'user-002',
    policyProfile: 'BIOMETRIC',
    purpose: 'DOCUMENT_SIGNATURE',
    signerIndex: 2,
));

// 4. Adicionar signatário 3 — assina por último
$session3 = $client->envelopes->addSession($envelope->envelopeId, new AddEnvelopeSessionRequest(
    signerName: 'Carlos Lima',
    signerCpf: '11122233344',
    signerEmail: 'carlos@example.com',
    signerUserExternalId: 'user-003',
    policyProfile: 'BIOMETRIC',
    purpose: 'DOCUMENT_SIGNATURE',
    signerIndex: 3,
));

echo "URL Signatário 1: " . $session1->url . "\n";
echo "URL Signatário 2: " . $session2->url . "\n";
echo "URL Signatário 3: " . $session3->url . "\n";
var pdfBase64 = Convert.ToBase64String(File.ReadAllBytes("contrato.pdf"));

// 1. Criar envelope sequencial
var envelope = await client.Envelopes.CreateAsync(new CreateEnvelopeRequest
{
    SigningMode = "SEQUENTIAL",
    TotalSigners = 3,
    Document = new EnvelopeDocument
    {
        Content = pdfBase64,
        Filename = "contrato.pdf",
    },
    ReturnUrl = "https://app.example.com/done",
    Locale = "pt-BR",
    ExpiresInMinutes = 4320,
});
Console.WriteLine($"Envelope ID: {envelope.EnvelopeId}");

// 2. Adicionar signatário 1 — assina primeiro
var session1 = await client.Envelopes.AddSessionAsync(envelope.EnvelopeId, new AddEnvelopeSessionRequest
{
    Signer = new Signer
    {
        UserExternalId = "user-001",
        Name = "João Silva",
        Cpf = "12345678900",
        Email = "joao@example.com",
    },
    Policy = new Policy { Profile = "BIOMETRIC" },
    SignerIndex = 1,
});

// 3. Adicionar signatário 2 — assina após signatário 1
var session2 = await client.Envelopes.AddSessionAsync(envelope.EnvelopeId, new AddEnvelopeSessionRequest
{
    Signer = new Signer
    {
        UserExternalId = "user-002",
        Name = "Maria Souza",
        Cpf = "98765432100",
        Email = "maria@example.com",
    },
    Policy = new Policy { Profile = "BIOMETRIC" },
    SignerIndex = 2,
});

// 4. Adicionar signatário 3 — assina por último
var session3 = await client.Envelopes.AddSessionAsync(envelope.EnvelopeId, new AddEnvelopeSessionRequest
{
    Signer = new Signer
    {
        UserExternalId = "user-003",
        Name = "Carlos Lima",
        Cpf = "11122233344",
        Email = "carlos@example.com",
    },
    Policy = new Policy { Profile = "BIOMETRIC" },
    SignerIndex = 3,
});

Console.WriteLine($"URL Signatário 1: {session1.Url}");
Console.WriteLine($"URL Signatário 2: {session2.Url}");
Console.WriteLine($"URL Signatário 3: {session3.Url}");

12. Receita: Acompanhar envelope e baixar carimbo combinado

Consulta o status de um envelope via polling e, quando todos os signatários concluírem, baixa o PDF com o carimbo combinado.

Backend: poll envelope status ──> completedSessions / totalSigners
                              │
    (loop até COMPLETED)      │
                              │
Backend: combined stamp ──> downloadUrl
                              │
Backend: download PDF ──> contrato-assinado-completo.pdf
import { writeFileSync } from 'fs';

const envelopeId = 'env_01J5X7K9M2N8P4Q6R3S1T0'; // ID do envelope criado anteriormente

// 1. Polling até conclusão
let detail = await client.envelopes.get(envelopeId);
while (detail.status !== 'COMPLETED') {
  console.log(`Progresso: ${detail.completedSessions}/${detail.totalSigners}`);
  for (const s of detail.sessions) {
    console.log(`  ${s.signerName} (${s.signerIndex}): ${s.status}`);
  }
  await new Promise(r => setTimeout(r, 5000)); // aguarda 5s
  detail = await client.envelopes.get(envelopeId);
}
console.log('Envelope concluído!');

// 2. Baixar carimbo combinado
const stamp = await client.envelopes.combinedStamp(envelopeId);
console.log('Download URL:', stamp.downloadUrl);
console.log('Total de assinaturas:', stamp.signerCount);

const res = await fetch(stamp.downloadUrl);
const pdf = Buffer.from(await res.arrayBuffer());
writeFileSync('contrato-assinado-completo.pdf', pdf);
console.log('PDF salvo: contrato-assinado-completo.pdf');
import time
from pathlib import Path
import httpx

envelope_id = 'env_01J5X7K9M2N8P4Q6R3S1T0'  # ID do envelope criado anteriormente

# 1. Polling até conclusão
detail = client.envelopes.get(envelope_id)
while detail.status != 'COMPLETED':
    print(f'Progresso: {detail.completed_sessions}/{detail.total_signers}')
    for s in detail.sessions:
        print(f'  {s.signer_name} ({s.signer_index}): {s.status}')
    time.sleep(5)  # aguarda 5s
    detail = client.envelopes.get(envelope_id)
print('Envelope concluído!')

# 2. Baixar carimbo combinado
stamp = client.envelopes.combined_stamp(envelope_id)
print('Download URL:', stamp.download_url)
print('Total de assinaturas:', stamp.signer_count)

pdf = httpx.get(stamp.download_url).content
Path('contrato-assinado-completo.pdf').write_bytes(pdf)
print('PDF salvo: contrato-assinado-completo.pdf')
envelopeID := "env_01J5X7K9M2N8P4Q6R3S1T0" // ID do envelope criado anteriormente

// 1. Polling até conclusão
detail, err := client.Envelopes.Get(ctx, envelopeID)
if err != nil { log.Fatal(err) }

for detail.Status != "COMPLETED" {
    fmt.Printf("Progresso: %d/%d\n", detail.CompletedSessions, detail.TotalSigners)
    for _, s := range detail.Sessions {
        fmt.Printf("  %s (%d): %s\n", s.SignerName, s.SignerIndex, s.Status)
    }
    time.Sleep(5 * time.Second) // aguarda 5s
    detail, err = client.Envelopes.Get(ctx, envelopeID)
    if err != nil { log.Fatal(err) }
}
fmt.Println("Envelope concluído!")

// 2. Baixar carimbo combinado
stamp, err := client.Envelopes.CombinedStamp(ctx, envelopeID)
if err != nil { log.Fatal(err) }
fmt.Println("Download URL:", stamp.DownloadURL)
fmt.Println("Total de assinaturas:", stamp.SignerCount)

resp, _ := http.Get(stamp.DownloadURL)
defer resp.Body.Close()
pdf, _ := io.ReadAll(resp.Body)
os.WriteFile("contrato-assinado-completo.pdf", pdf, 0644)
fmt.Println("PDF salvo: contrato-assinado-completo.pdf")
String envelopeId = "env_01J5X7K9M2N8P4Q6R3S1T0"; // ID do envelope criado anteriormente

// 1. Polling até conclusão
EnvelopeDetail detail = client.envelopes().get(envelopeId);
while (!"COMPLETED".equals(detail.status)) {
    System.out.println("Progresso: " + detail.completedSessions + "/" + detail.totalSigners);
    for (EnvelopeSessionSummary s : detail.sessions) {
        System.out.println("  " + s.signerName + " (" + s.signerIndex + "): " + s.status);
    }
    Thread.sleep(5000); // aguarda 5s
    detail = client.envelopes().get(envelopeId);
}
System.out.println("Envelope concluído!");

// 2. Baixar carimbo combinado
CombinedStampResponse stamp = client.envelopes().combinedStamp(envelopeId);
System.out.println("Download URL: " + stamp.downloadUrl);
System.out.println("Total de assinaturas: " + stamp.signerCount);

byte[] pdf = new URL(stamp.downloadUrl).openStream().readAllBytes();
Files.write(Path.of("contrato-assinado-completo.pdf"), pdf);
System.out.println("PDF salvo: contrato-assinado-completo.pdf");
$envelopeId = 'env_01J5X7K9M2N8P4Q6R3S1T0'; // ID do envelope criado anteriormente

// 1. Polling até conclusão
$detail = $client->envelopes->get($envelopeId);
while ($detail->status !== 'COMPLETED') {
    echo "Progresso: " . $detail->completedSessions . "/" . $detail->totalSigners . "\n";
    foreach ($detail->sessions as $s) {
        echo "  " . $s->signerName . " (" . $s->signerIndex . "): " . $s->status . "\n";
    }
    sleep(5); // aguarda 5s
    $detail = $client->envelopes->get($envelopeId);
}
echo "Envelope concluído!\n";

// 2. Baixar carimbo combinado
$stamp = $client->envelopes->combinedStamp($envelopeId);
echo "Download URL: " . $stamp->downloadUrl . "\n";
echo "Total de assinaturas: " . $stamp->signerCount . "\n";

$pdf = file_get_contents($stamp->downloadUrl);
file_put_contents('contrato-assinado-completo.pdf', $pdf);
echo "PDF salvo: contrato-assinado-completo.pdf\n";
var envelopeId = "env_01J5X7K9M2N8P4Q6R3S1T0"; // ID do envelope criado anteriormente

// 1. Polling até conclusão
var detail = await client.Envelopes.GetAsync(envelopeId);
while (detail.Status != "COMPLETED")
{
    Console.WriteLine($"Progresso: {detail.CompletedSessions}/{detail.TotalSigners}");
    foreach (var s in detail.Sessions)
    {
        Console.WriteLine($"  {s.SignerName} ({s.SignerIndex}): {s.Status}");
    }
    await Task.Delay(5000); // aguarda 5s
    detail = await client.Envelopes.GetAsync(envelopeId);
}
Console.WriteLine("Envelope concluído!");

// 2. Baixar carimbo combinado
var stamp = await client.Envelopes.CombinedStampAsync(envelopeId);
Console.WriteLine($"Download URL: {stamp.DownloadUrl}");
Console.WriteLine($"Total de assinaturas: {stamp.SignerCount}");

var pdf = await new HttpClient().GetByteArrayAsync(stamp.DownloadUrl);
File.WriteAllBytes("contrato-assinado-completo.pdf", pdf);
Console.WriteLine("PDF salvo: contrato-assinado-completo.pdf");