Guias de Desenvolvimento

Signing Sessions

Guias dos SDKs

Aceite + OTP — CLICK_PLUS_OTP

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

1. O que é

CLICK_PLUS_OTP adiciona uma etapa de verificação OTP após o aceite. O signatário clica "Aceitar", recebe um código de 6 dígitos por email (ou SMS) e o insere para confirmar.

Ideal para contratos que exigem dupla confirmação.

Signatário clica "Aceitar" ──> OTP enviado por email/SMS
                                   │
Signatário digita código ──> COMPLETED

2. Requisitos

Campo Obrigatório Notas
name Sim Nome completo
userExternalId Sim ID no seu sistema
cpf ou cnpj Sim (um) Sem pontuação
email Sim* *Obrigatório para OTP por email
phone Alternativo Para OTP via SMS, com otpChannel: 'sms'

3. Criar a sessão

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}");

4. Experiência do signatário

A página hospedada exibe o documento em um visualizador PDF. O signatário clica "Aceitar", e em seguida um campo para digitar o código OTP de 6 dígitos é exibido. Após digitar o código correto, a assinatura é confirmada e o signatário é redirecionado para a returnUrl.


5. Alternativa programática (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.


6. Detalhes do OTP

signer: {
  name: 'Maria Souza',
  phone: '+5511999998888',
  otpChannel: 'sms',
  userExternalId: 'user-002',
},

← Aceite Simples (CLICK_ONLY)
Próximo: Biometria →
Voltar para Visão Geral