Guias de Desenvolvimento

Signing Sessions

Guias dos SDKs

Guia de Assinatura com Certificado Digital — SignDocs Brasil (Certificados A1)

Sumário

  1. Visão Geral
  2. Extraindo Seu Certificado
  3. Assinando um Hash
  4. Fluxo Completo da API
  5. Múltiplos Signatários
  6. Resolução de Cadeia
  7. Tratamento de Erros

1. Visão Geral

O SignDocs Brasil suporta assinaturas digitais utilizando certificados ICP-Brasil A1 (arquivos PKCS#12/PFX baseados em software). Diferentemente de assinaturas eletrônicas simples (aceitar com clique, OTP), assinaturas com certificado digital produzem um PDF com um contêiner PKCS#7/CMS SignedData embutido, criptograficamente vinculado ao certificado ICP-Brasil do signatário. O PDF resultante é verificável pelo Validador do ITI e por qualquer leitor compatível com PAdES.

Como Funciona

O protocolo de assinatura é uma troca em duas fases entre sua aplicação (o cliente) e a API do SignDocs Brasil (o servidor):

  1. Fase de preparação -- Você envia seu certificado folha (apenas a chave pública). O servidor prepara o PDF com um espaço reservado para a assinatura, constrói os atributos autenticados do PKCS#7 e retorna um hash SHA-256 codificado em hexadecimal para você assinar.
  2. Fase de conclusão -- Você assina o hash localmente com sua chave privada usando RSASSA-PKCS1-v1_5 e envia a assinatura bruta de volta. O servidor monta o contêiner PKCS#7 completo, incorpora-o no PDF e armazena o documento assinado digitalmente.

Você só precisa nos enviar seu certificado folha -- nós resolvemos a cadeia completa ICP-Brasil automaticamente.

Sua chave privada nunca sai da sua infraestrutura. O servidor nunca a vê. O único material criptográfico transmitido é:

Padrão de Assinatura

Propriedade Valor
Algoritmo de hash SHA-256
Algoritmo de assinatura RSASSA-PKCS1-v1_5 (SHA-256 com RSA)
Formato do contêiner PKCS#7 / CMS SignedData (RFC 5652)
Tipo de assinatura no PDF PAdES (CMS destacado no PDF /ByteRange)
Atributos autenticados contentType, messageDigest, signingTime
Cadeia de certificados Resolvida no servidor a partir das CAs ICP-Brasil incluídas

Perfil de Política

O perfil de política DIGITAL_CERTIFICATE produz duas etapas ordenadas:

  1. CLICK_ACCEPT -- O signatário reconhece o documento.
  2. DIGITAL_SIGN_A1 -- O fluxo de assinatura criptográfica descrito neste guia.

2. Extraindo Seu Certificado

Um certificado ICP-Brasil A1 é entregue como um arquivo .pfx (PKCS#12) contendo sua chave privada, seu certificado folha e possivelmente certificados de CAs intermediárias. Para a fase de preparação, você precisa extrair apenas o certificado folha em formato PEM. A chave privada permanece com você.

Python

from cryptography import x509
from cryptography.hazmat.primitives.serialization import pkcs12, Encoding

# Load PFX
with open("certificate.pfx", "rb") as f:
    pfx_data = f.read()

private_key, certificate, additional_certs = pkcs12.load_key_and_certificates(
    pfx_data, b"your-pfx-password"
)

# Export leaf certificate as PEM
leaf_pem = certificate.public_bytes(Encoding.PEM).decode("utf-8")
print(leaf_pem)
# -----BEGIN CERTIFICATE-----
# MIIGxTCCBK2gAwIBAgIIU+...
# -----END CERTIFICATE-----

Node.js

const forge = require("node-forge");
const fs = require("fs");

// Load PFX
const pfxDer = fs.readFileSync("certificate.pfx", "binary");
const pfxAsn1 = forge.asn1.fromDer(pfxDer);
const pfx = forge.pkcs12.pkcs12FromAsn1(pfxAsn1, "your-pfx-password");

// Extract leaf certificate
const certBags = pfx.getBags({ bagType: forge.pki.oids.certBag });
const certBag = certBags[forge.pki.oids.certBag];

// The leaf is the non-CA certificate
const leafBag = certBag.find((bag) => {
  const cert = bag.cert;
  const bc = cert.getExtension("basicConstraints");
  return !bc || !bc.cA;
});

const leafPem = forge.pki.certificateToPem(leafBag.cert);
console.log(leafPem);

Java

import java.io.FileInputStream;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Enumeration;

KeyStore ks = KeyStore.getInstance("PKCS12");
try (FileInputStream fis = new FileInputStream("certificate.pfx")) {
    ks.load(fis, "your-pfx-password".toCharArray());
}

Enumeration<String> aliases = ks.aliases();
String alias = aliases.nextElement();

X509Certificate cert = (X509Certificate) ks.getCertificate(alias);

// Convert to PEM
String base64 = Base64.getEncoder().encodeToString(cert.getEncoded());
String leafPem = "-----BEGIN CERTIFICATE-----\n"
    + base64.replaceAll("(.{64})", "$1\n")
    + "\n-----END CERTIFICATE-----";

System.out.println(leafPem);

C#

using System.Security.Cryptography.X509Certificates;
using System.Text;

var collection = new X509Certificate2Collection();
collection.Import("certificate.pfx", "your-pfx-password",
    X509KeyStorageFlags.Exportable);

// Find the leaf (end-entity) certificate
X509Certificate2 leaf = null;
foreach (var cert in collection)
{
    // Leaf certs do not have the CA basic constraint
    foreach (var ext in cert.Extensions)
    {
        if (ext is X509BasicConstraintsExtension bc && bc.CertificateAuthority)
            goto next;
    }
    leaf = cert;
    break;
    next:;
}

// Export as PEM
byte[] derBytes = leaf.Export(X509ContentType.Cert);
string base64 = Convert.ToBase64String(derBytes, Base64FormattingOptions.InsertLineBreaks);
string leafPem = $"-----BEGIN CERTIFICATE-----\n{base64}\n-----END CERTIFICATE-----";

Console.WriteLine(leafPem);

3. Assinando um Hash

O servidor retorna um valor hashToSign (digest SHA-256 codificado em hexadecimal). Sua aplicação deve assinar este hash usando RSASSA-PKCS1-v1_5 com SHA-256 e retornar os bytes brutos da assinatura como base64.

Importante: O hash já foi calculado pelo servidor. Você deve executar a operação de assinatura RSA PKCS#1 v1.5 diretamente sobre este hash. NÃO calcule o hash novamente. A operação de assinatura aplica internamente o encapsulamento DigestInfo (AlgorithmIdentifier codificado em DER + hash) conforme especificado pelo PKCS#1 v1.5.

Python

from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import pkcs12
import base64

# Load private key from PFX
with open("certificate.pfx", "rb") as f:
    private_key, _, _ = pkcs12.load_key_and_certificates(f.read(), b"your-pfx-password")

# hashToSign from the /signing/prepare response (hex string)
hash_to_sign_hex = "a1b2c3d4..."  # from API response
hash_bytes = bytes.fromhex(hash_to_sign_hex)

# Sign with RSASSA-PKCS1-v1_5 SHA-256
# Use Prehashed because the server already computed the digest
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
signature = private_key.sign(
    hash_bytes,
    padding.PKCS1v15(),
    Prehashed(hashes.SHA256())
)

raw_signature_base64 = base64.b64encode(signature).decode("utf-8")
print(raw_signature_base64)

Node.js

const crypto = require("crypto");
const forge = require("node-forge");
const fs = require("fs");

// Load private key from PFX
const pfxDer = fs.readFileSync("certificate.pfx", "binary");
const pfxAsn1 = forge.asn1.fromDer(pfxDer);
const pfx = forge.pkcs12.pkcs12FromAsn1(pfxAsn1, "your-pfx-password");

const keyBags = pfx.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag });
const keyBag = keyBags[forge.pki.oids.pkcs8ShroudedKeyBag];
const forgePrivateKey = keyBag[0].key;

// Convert to Node.js crypto key
const privateKeyPem = forge.pki.privateKeyToPem(forgePrivateKey);

// hashToSign from the /signing/prepare response (hex string)
const hashToSignHex = "a1b2c3d4..."; // from API response

// Sign: crypto.sign with SHA-256 will apply DigestInfo + RSA PKCS#1 v1.5
// Since we already have the hash, we create a signer in raw mode
const sign = crypto.createSign("RSA-SHA256");
// We need to feed the original data that hashes to hashToSign,
// but since we only have the hash, use the lower-level approach:
const hashBuffer = Buffer.from(hashToSignHex, "hex");

// Build DigestInfo manually for pre-hashed data
const DER_SHA256_PREFIX = Buffer.from(
  "3031300d060960864801650304020105000420",
  "hex"
);
const digestInfo = Buffer.concat([DER_SHA256_PREFIX, hashBuffer]);

// RSA private encrypt (raw PKCS#1 v1.5 signature)
const key = crypto.createPrivateKey(privateKeyPem);
const signature = crypto.privateEncrypt(
  { key, padding: crypto.constants.RSA_PKCS1_PADDING },
  digestInfo
);

const rawSignatureBase64 = signature.toString("base64");
console.log(rawSignatureBase64);

Java

import java.io.FileInputStream;
import java.security.*;
import java.util.Base64;

// Load private key from PFX
KeyStore ks = KeyStore.getInstance("PKCS12");
try (FileInputStream fis = new FileInputStream("certificate.pfx")) {
    ks.load(fis, "your-pfx-password".toCharArray());
}

String alias = ks.aliases().nextElement();
PrivateKey privateKey = (PrivateKey) ks.getKey(alias, "your-pfx-password".toCharArray());

// hashToSign from the /signing/prepare response (hex string)
String hashToSignHex = "a1b2c3d4..."; // from API response
byte[] hashBytes = hexStringToByteArray(hashToSignHex);

// Build DigestInfo (DER-encoded AlgorithmIdentifier + hash)
// SHA-256 DigestInfo prefix per RFC 3447 Section 9.2 Note 1
byte[] sha256Prefix = new byte[] {
    0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, (byte)0x86, 0x48,
    0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20
};

byte[] digestInfo = new byte[sha256Prefix.length + hashBytes.length];
System.arraycopy(sha256Prefix, 0, digestInfo, 0, sha256Prefix.length);
System.arraycopy(hashBytes, 0, digestInfo, sha256Prefix.length, hashBytes.length);

// Sign with NONEwithRSA (raw PKCS#1 v1.5 on pre-built DigestInfo)
Signature sig = Signature.getInstance("NONEwithRSA");
sig.initSign(privateKey);
sig.update(digestInfo);
byte[] signature = sig.sign();

String rawSignatureBase64 = Base64.getEncoder().encodeToString(signature);
System.out.println(rawSignatureBase64);

// Helper
static byte[] hexStringToByteArray(String s) {
    int len = s.length();
    byte[] data = new byte[len / 2];
    for (int i = 0; i < len; i += 2) {
        data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                              + Character.digit(s.charAt(i + 1), 16));
    }
    return data;
}

C#

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

// Load private key from PFX
var collection = new X509Certificate2Collection();
collection.Import("certificate.pfx", "your-pfx-password",
    X509KeyStorageFlags.Exportable);

X509Certificate2 cert = collection[0]; // leaf
RSA rsa = cert.GetRSAPrivateKey();

// hashToSign from the /signing/prepare response (hex string)
string hashToSignHex = "a1b2c3d4..."; // from API response
byte[] hashBytes = Convert.FromHexString(hashToSignHex);

// Sign the pre-computed hash with PKCS#1 v1.5
byte[] signature = rsa.SignHash(
    hashBytes,
    HashAlgorithmName.SHA256,
    RSASignaturePadding.Pkcs1
);

string rawSignatureBase64 = Convert.ToBase64String(signature);
Console.WriteLine(rawSignatureBase64);

4. Fluxo Completo da API

Esta seção percorre o fluxo completo de ponta a ponta para assinar digitalmente um documento PDF com um certificado ICP-Brasil A1.

Pré-requisitos

Passo 1: Criar uma Transação

curl -X POST https://api.signdocs.com.br/v1/transactions \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "purpose": "DOCUMENT_SIGNATURE",
    "policy": {
      "profile": "DIGITAL_CERTIFICATE"
    },
    "signer": {
      "name": "Maria da Silva",
      "email": "maria@example.com",
      "userExternalId": "user_12345",
      "cpf": "12345678901"
    },
    "digitalSignature": {
      "reason": "Concordo com os termos deste contrato",
      "location": "Sao Paulo, SP",
      "contactInfo": "maria@example.com"
    },
    "expiresInMinutes": 1440
  }'

Resposta (201 Created):

{
  "transactionId": "tx_01HQRS...",
  "status": "CREATED",
  "purpose": "DOCUMENT_SIGNATURE",
  "policy": {
    "profile": "DIGITAL_CERTIFICATE"
  },
  "steps": [
    {
      "stepId": "stp_01HQRS...",
      "type": "CLICK_ACCEPT",
      "status": "PENDING",
      "order": 0
    },
    {
      "stepId": "stp_01HQRT...",
      "type": "DIGITAL_SIGN_A1",
      "status": "PENDING",
      "order": 1
    }
  ],
  "userExternalId": "user_12345",
  "createdAt": "2026-02-15T14:00:00.000Z",
  "expiresAt": "2026-02-16T14:00:00.000Z"
}

Guarde o transactionId, steps[0].stepId (CLICK_ACCEPT) e steps[1].stepId (DIGITAL_SIGN_A1).

Passo 2: Fazer Upload do Documento PDF

curl -X POST https://api.signdocs.com.br/v1/transactions/${TX_ID}/document \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "'$(base64 -w0 contract.pdf)'"
  }'

Resposta (200 OK):

{
  "transactionId": "tx_01HQRS...",
  "documentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "status": "DOCUMENT_UPLOADED"
}

Passo 3: Concluir a Etapa CLICK_ACCEPT

Iniciar a etapa:

curl -X POST https://api.signdocs.com.br/v1/transactions/${TX_ID}/steps/${CLICK_STEP_ID}/start \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json"

Resposta (200 OK):

{
  "stepId": "stp_01HQRS...",
  "type": "CLICK_ACCEPT",
  "status": "STARTED",
  "message": "Click accept step started"
}

Concluir a etapa:

curl -X POST https://api.signdocs.com.br/v1/transactions/${TX_ID}/steps/${CLICK_STEP_ID}/complete \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "accepted": true,
    "textVersion": "1.0"
  }'

Resposta (200 OK):

{
  "stepId": "stp_01HQRS...",
  "type": "CLICK_ACCEPT",
  "status": "COMPLETED",
  "attempts": 1,
  "result": {
    "click": {
      "accepted": true,
      "textVersion": "1.0"
    }
  }
}

Passo 4: Iniciar a Etapa DIGITAL_SIGN_A1

curl -X POST https://api.signdocs.com.br/v1/transactions/${TX_ID}/steps/${SIGN_STEP_ID}/start \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json"

Resposta (200 OK):

{
  "stepId": "stp_01HQRT...",
  "type": "DIGITAL_SIGN_A1",
  "status": "STARTED",
  "message": "Digital certificate signing step started. Use /signing/prepare to continue."
}

Passo 5: Preparar Assinatura (Enviar Certificado Folha)

Envie seu certificado folha em formato PEM. Você só precisa enviar o certificado folha -- o servidor resolve a cadeia completa.

curl -X POST https://api.signdocs.com.br/v1/transactions/${TX_ID}/signing/prepare \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "certificateChainPems": [
      "-----BEGIN CERTIFICATE-----\nMIIGxTCCBK2gAwIBAgIIU+w77w...(your leaf cert)...\n-----END CERTIFICATE-----"
    ]
  }'

Corpo da requisição:

Campo Tipo Obrigatório Descrição
certificateChainPems string[] Sim Array de certificados X.509 codificados em PEM. No mínimo, inclua o certificado folha. Intermediários adicionais são aceitos, mas não obrigatórios.

Resposta (200 OK):

{
  "signatureRequestId": "sigreq_01HQRU...",
  "hashToSign": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "hashAlgorithm": "SHA-256",
  "signatureAlgorithm": "RSASSA-PKCS1-v1_5"
}

O hashToSign é o digest SHA-256 dos atributos autenticados codificados em DER (contentType + messageDigest + signingTime). Isto é o que sua chave privada deve assinar.

O signatureRequestId identifica esta sessão de assinatura e deve ser enviado de volta na chamada de conclusão. Ele expira após 15 minutos.

Passo 6: Assinar o Hash Localmente

Utilizando os exemplos de código da Seção 3, assine o valor hashToSign com sua chave privada. O resultado é uma assinatura RSA bruta codificada em base64.

# Example using OpenSSL CLI for illustration:
echo -n "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" \
  | xxd -r -p \
  | openssl dgst -sha256 -sign private_key.pem -pkeyopt rsa_padding_mode:pkcs1 \
  | base64 -w0

Nota: Em produção, utilize os exemplos específicos por linguagem da Seção 3, e não o OpenSSL CLI.

Passo 7: Concluir Assinatura (Enviar Assinatura Bruta)

curl -X POST https://api.signdocs.com.br/v1/transactions/${TX_ID}/signing/complete \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "signatureRequestId": "sigreq_01HQRU...",
    "rawSignatureBase64": "MEUCIQC7...base64-encoded-signature..."
  }'

Corpo da requisição:

Campo Tipo Obrigatório Descrição
signatureRequestId string Sim O ID retornado por /signing/prepare.
rawSignatureBase64 string Sim Assinatura RSA PKCS#1 v1.5 bruta codificada em base64.

Resposta (200 OK):

{
  "stepId": "stp_01HQRT...",
  "status": "COMPLETED",
  "result": {
    "digitalSignature": {
      "certificateSubject": "CN=Maria da Silva, OU=AR Certisign, O=ICP-Brasil, C=BR",
      "certificateSerial": "53ECFBEF...",
      "certificateIssuer": "CN=AC Certisign Multipla G7, OU=AC Certisign Multipla G7, O=ICP-Brasil, C=BR",
      "algorithm": "SHA256withRSA",
      "signedAt": "2026-02-15T14:05:30.000Z",
      "signedPdfHash": "b5bb9d8014a0f9b1d61e21e796d78dcc...",
      "signatureFieldName": "Signature1"
    }
  }
}

Neste ponto, a etapa DIGITAL_SIGN_A1 está marcada como COMPLETED. O PDF assinado está armazenado no servidor.

Passo 8: Finalizar a Transação

curl -X POST https://api.signdocs.com.br/v1/transactions/${TX_ID}/finalize \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json"

Resposta (200 OK):

{
  "transactionId": "tx_01HQRS...",
  "status": "COMPLETED",
  "evidenceId": "evi_01HQRV...",
  "evidenceHash": "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592",
  "completedAt": "2026-02-15T14:06:00.000Z"
}

A finalização verifica que todas as etapas foram concluídas, constrói o Pacote de Evidências criptográfico (.p7m) e copia o PDF assinado digitalmente para sua localização final. Para PDFs assinados digitalmente, o servidor NÃO aplica carimbo ou modifica o documento (fazê-lo invalidaria a assinatura PKCS#7 embutida).

Passo 9: Baixar o PDF Assinado

curl -X GET https://api.signdocs.com.br/v1/transactions/${TX_ID}/download \
  -H "Authorization: Bearer ${TOKEN}"

Resposta (200 OK):

{
  "transactionId": "tx_01HQRS...",
  "documentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "originalUrl": "https://signdocs-evidence-bucket.s3.amazonaws.com/tenants/.../document.pdf?X-Amz-...",
  "signedUrl": "https://signdocs-evidence-bucket.s3.amazonaws.com/tenants/.../document-signed.pdf?X-Amz-...",
  "expiresIn": 3600
}

A signedUrl é uma URL pré-assinada do S3 válida por 1 hora. O PDF baixado contém uma assinatura PAdES embutida verificável pelo Adobe Acrobat, Validador do ITI e outras ferramentas compatíveis.


5. Múltiplos Signatários

O SignDocs Brasil suporta múltiplos signatários assinando o mesmo documento PDF sequencialmente. Cada signatário recebe sua própria transação, mas todas as transações são vinculadas por meio de um grupo de documentos.

Como os Grupos de Documentos Funcionam

  1. Todas as transações em um grupo compartilham o mesmo documentGroupId.
  2. Cada signatário recebe um signerIndex (base 1) e o grupo possui um totalSigners fixo.
  3. O signatário 1 assina o PDF original. O signatário 2 assina o PDF que o signatário 1 já assinou. O signatário N assina o PDF que o signatário N-1 assinou.
  4. A assinatura digital de cada signatário é embutida em um campo de assinatura separado no PDF (Signature1, Signature2, etc.).
  5. O PDF final contém todas as assinaturas empilhadas incrementalmente, cada uma cobrindo os intervalos de bytes da versão anterior.

Criando Transações com Múltiplos Signatários

Crie uma transação por signatário, todas referenciando o mesmo documentGroupId:

Signatário 1 (primeiro a assinar):

curl -X POST https://api.signdocs.com.br/v1/transactions \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "purpose": "DOCUMENT_SIGNATURE",
    "policy": { "profile": "DIGITAL_CERTIFICATE" },
    "signer": {
      "name": "Maria da Silva",
      "email": "maria@example.com",
      "userExternalId": "user_maria",
      "cpf": "12345678901"
    },
    "documentGroupId": "group_contract_2026_001",
    "signerIndex": 1,
    "totalSigners": 3
  }'

Signatário 2 (assina após o signatário 1 concluir):

curl -X POST https://api.signdocs.com.br/v1/transactions \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "purpose": "DOCUMENT_SIGNATURE",
    "policy": { "profile": "DIGITAL_CERTIFICATE" },
    "signer": {
      "name": "Joao Santos",
      "email": "joao@example.com",
      "userExternalId": "user_joao",
      "cpf": "98765432100"
    },
    "documentGroupId": "group_contract_2026_001",
    "signerIndex": 2,
    "totalSigners": 3
  }'

Signatário 3 (assina por último):

curl -X POST https://api.signdocs.com.br/v1/transactions \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "purpose": "DOCUMENT_SIGNATURE",
    "policy": { "profile": "DIGITAL_CERTIFICATE" },
    "signer": {
      "name": "Carlos Oliveira",
      "email": "carlos@example.com",
      "userExternalId": "user_carlos",
      "cpf": "11122233344"
    },
    "documentGroupId": "group_contract_2026_001",
    "signerIndex": 3,
    "totalSigners": 3
  }'

Ordem de Execução

Os signatários devem concluir suas assinaturas na ordem do signerIndex:

  1. Faça upload do PDF na transação do signatário 1.
  2. Conclua o fluxo completo do signatário 1 (CLICK_ACCEPT, DIGITAL_SIGN_A1, finalizar).
  3. Quando o /signing/prepare do signatário 2 for chamado, o servidor carrega automaticamente o PDF assinado pelo signatário 1 (armazenado como document-signed-1.pdf).
  4. Conclua o fluxo completo do signatário 2.
  5. Quando o /signing/prepare do signatário 3 for chamado, o servidor carrega o PDF assinado pelo signatário 2 (armazenado como document-signed-2.pdf).
  6. Conclua o fluxo completo do signatário 3.

Se um signatário posterior chamar /signing/prepare antes que o signatário anterior conclua, o servidor retorna um erro 422 Unprocessable Entity:

{
  "type": "https://api.signdocs.com.br/errors/unprocessable-entity",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "Previous signer (index 1) must complete signing first"
}

Consultando um Grupo de Documentos

Utilize o filtro documentGroupId no endpoint de listagem para recuperar todas as transações de um grupo:

curl -X GET "https://api.signdocs.com.br/v1/transactions?documentGroupId=group_contract_2026_001" \
  -H "Authorization: Bearer ${TOKEN}"

6. Resolução de Cadeia

Construção de Cadeia no Servidor

O SignDocs Brasil inclui um conjunto abrangente de certificados de CAs intermediárias e raiz ICP-Brasil. Quando você envia seu certificado folha na requisição /signing/prepare, o servidor resolve automaticamente a cadeia completa de certificados usando o seguinte algoritmo:

  1. Analisar o certificado folha e extrair o Distinguished Name (DN) do Emissor.
  2. Pesquisar no armazenamento de intermediários incluído por um certificado cujo DN do Sujeito corresponda ao DN do Emissor do certificado folha. O armazenamento é indexado por DN para busca em O(1).
  3. Repetir para cada intermediário: extrair seu DN do Emissor e pesquisar o próximo emissor.
  4. Encerrar quando uma CA raiz ICP-Brasil confiável for encontrada (certificado autoassinado no pacote de raízes confiáveis) ou a profundidade máxima de cadeia de 10 for atingida.

Correspondência de DN

O construtor de cadeia utiliza uma abordagem de correspondência de DN com múltiplas estratégias para lidar com as variações encontradas entre as CAs ICP-Brasil:

Fallback por AIA

Se um certificado intermediário não for encontrado no armazenamento incluído, o servidor tenta obtê-lo por meio da extensão Authority Information Access (AIA):

  1. O método de acesso caIssuers da extensão AIA (OID 1.3.6.1.5.5.7.48.2) é extraído do certificado.
  2. A URL é acessada via HTTP com um timeout de 15 segundos.
  3. Os formatos de resposta PEM e DER são suportados.
  4. O certificado obtido é adicionado à cadeia.

Isso garante que mesmo CAs intermediárias recém-emitidas que ainda não estão no armazenamento incluído possam ser resolvidas.

O Que é Embutido no PDF

O contêiner PKCS#7 SignedData completo embutido no PDF inclui:

Isso significa que o PDF assinado é totalmente autocontido. Um verificador não precisa buscar certificados externos para validar a cadeia de assinatura.

Atualizações do Armazenamento de CAs Incluído

Os pacotes de CAs intermediárias e raiz ICP-Brasil são atualizados periodicamente conforme o ITI publica novas CAs. Os pacotes são implantados como parte do pacote da função Lambda. Se você encontrar uma falha na resolução de cadeia para um certificado recém-emitido, o fallback por AIA deve tratá-la de forma transparente. Se os problemas persistirem, entre em contato com o suporte.


7. Tratamento de Erros

Todos os erros seguem o formato Problem Details da RFC 7807 com tipo de conteúdo application/problem+json.

Formato da Resposta de Erro

{
  "type": "https://api.signdocs.com.br/errors/bad-request",
  "title": "Bad Request",
  "status": 400,
  "detail": "Human-readable explanation of what went wrong",
  "instance": "/v1/transactions/tx_01HQRS.../signing/prepare"
}

Erros Comuns

Formato de Certificado Inválido (400)

Retornado quando o PEM em certificateChainPems é malformado ou não é um certificado X.509 válido.

{
  "type": "https://api.signdocs.com.br/errors/bad-request",
  "title": "Bad Request",
  "status": 400,
  "detail": "Invalid X.509 certificate in certificateChainPems"
}

Causa: A string PEM está sem o cabeçalho -----BEGIN CERTIFICATE-----, contém base64 corrompido, ou a estrutura ASN.1 não é um certificado X.509 válido.

Correção: Verifique se o PEM foi extraído corretamente do arquivo PFX. Certifique-se de que nenhum espaço em branco extra ou truncamento ocorreu durante o transporte.

Certificado Ausente (400)

{
  "type": "https://api.signdocs.com.br/errors/bad-request",
  "title": "Bad Request",
  "status": 400,
  "detail": "certificateChainPems array is required with at least one certificate"
}

Causa: O campo certificateChainPems está ausente, é nulo, está vazio ou não é um array.

Correção: Forneça pelo menos o PEM do certificado folha no array.

Certificado Expirado

O servidor não rejeita certificados expirados na etapa /signing/prepare -- a política ICP-Brasil permite assinar com um certificado que era válido no momento da emissão. No entanto, verificadores podem sinalizar a assinatura durante a validação. Certifique-se de que seu certificado está vigente.

Cadeia Incompleta (Aviso)

Se o construtor de cadeia não conseguir conectar o certificado folha a uma raiz ICP-Brasil confiável, o servidor registra um aviso, mas prossegue com a assinatura. O PKCS#7 resultante conterá a cadeia que foi possível resolver. Verificadores como o Validador do ITI podem rejeitar a assinatura se a cadeia estiver incompleta.

Para evitar isso, certifique-se de que seu certificado A1 foi emitido por uma CA ICP-Brasil reconhecida. O fallback por AIA trata a maioria dos casos de intermediários ausentes automaticamente.

Requisição de Assinatura Expirada (409)

{
  "type": "https://api.signdocs.com.br/errors/conflict",
  "title": "Conflict",
  "status": 409,
  "detail": "Signature request has expired"
}

Causa: Mais de 15 minutos se passaram entre a chamada /signing/prepare e a chamada /signing/complete.

Correção: Chame /signing/prepare novamente para obter um novo signatureRequestId e hashToSign, depois assine e envie dentro de 15 minutos.

Requisição de Assinatura Já Concluída (409)

{
  "type": "https://api.signdocs.com.br/errors/conflict",
  "title": "Conflict",
  "status": 409,
  "detail": "Signature request is COMPLETED"
}

Causa: O endpoint /signing/complete foi chamado duas vezes com o mesmo signatureRequestId.

Correção: Cada requisição de assinatura só pode ser concluída uma vez. Se precisar assinar novamente, chame /signing/prepare outra vez.

Assinatura Bruta Inválida (400)

{
  "type": "https://api.signdocs.com.br/errors/bad-request",
  "title": "Bad Request",
  "status": 400,
  "detail": "Invalid base64 in rawSignatureBase64"
}

Causa: O campo rawSignatureBase64 não é base64 válido ou decodifica para um buffer vazio.

Correção: Certifique-se de que a assinatura está codificada corretamente em base64. Para uma chave RSA de 2048 bits, a assinatura bruta deve ter exatamente 256 bytes (344 caracteres em base64). Para uma chave de 4096 bits, 512 bytes (684 caracteres em base64).

Etapa DIGITAL_SIGN_A1 Não Iniciada (409)

{
  "type": "https://api.signdocs.com.br/errors/conflict",
  "title": "Conflict",
  "status": 409,
  "detail": "DIGITAL_SIGN_A1 step must be in STARTED status, current: PENDING"
}

Causa: O endpoint /signing/prepare foi chamado antes de iniciar a etapa DIGITAL_SIGN_A1.

Correção: Chame POST /v1/transactions/{id}/steps/{stepId}/start na etapa DIGITAL_SIGN_A1 primeiro.

Violação de Ordem das Etapas (409)

{
  "type": "https://api.signdocs.com.br/errors/conflict",
  "title": "Conflict",
  "status": 409,
  "detail": "Previous step CLICK_ACCEPT (order 0) must be completed first"
}

Causa: Tentou iniciar a etapa DIGITAL_SIGN_A1 antes de concluir a etapa CLICK_ACCEPT.

Correção: As etapas devem ser concluídas em ordem. Para o perfil DIGITAL_CERTIFICATE, conclua CLICK_ACCEPT antes de iniciar DIGITAL_SIGN_A1.

Conclusão Direta de DIGITAL_SIGN_A1 (400)

{
  "type": "https://api.signdocs.com.br/errors/bad-request",
  "title": "Bad Request",
  "status": 400,
  "detail": "DIGITAL_SIGN_A1 steps cannot be completed directly. Use POST /v1/transactions/{id}/signing/prepare and POST /v1/transactions/{id}/signing/complete instead."
}

Causa: Chamou POST /v1/transactions/{id}/steps/{stepId}/complete em uma etapa DIGITAL_SIGN_A1. Este tipo de etapa utiliza um protocolo dedicado de assinatura em duas fases.

Correção: Utilize os endpoints /signing/prepare e /signing/complete em vez do endpoint genérico de conclusão de etapa.

Documento Não Enviado (422)

{
  "type": "https://api.signdocs.com.br/errors/unprocessable-entity",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "Document must be uploaded before signing"
}

Causa: Chamou /signing/prepare em uma transação que não possui documento PDF enviado.

Correção: Faça upload do documento PDF antes de iniciar o fluxo de assinatura.

PKCS#7 Muito Grande

Se o contêiner PKCS#7 montado (certificados + assinatura + atributos autenticados) exceder o tamanho do espaço reservado no PDF, a incorporação da assinatura falhará. Isso é extremamente raro com certificados ICP-Brasil A1, pois o espaço reservado é dimensionado para acomodar cadeias completas. Se encontrado, entre em contato com o suporte.

Transação Já Finalizada (409)

{
  "type": "https://api.signdocs.com.br/errors/conflict",
  "title": "Conflict",
  "status": 409,
  "detail": "Transaction is already finalized"
}

Causa: Chamou /finalize em uma transação que já está no status COMPLETED.

Etapas Incompletas na Finalização (422)

{
  "type": "https://api.signdocs.com.br/errors/unprocessable-entity",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "Not all steps are completed. Pending: DIGITAL_SIGN_A1(STARTED)"
}

Causa: Chamou /finalize antes de todas as etapas serem concluídas.

Correção: Conclua todas as etapas (CLICK_ACCEPT e DIGITAL_SIGN_A1) antes de finalizar.


Apêndice: Referência Rápida

Resumo dos Endpoints

Passo Método Endpoint Finalidade
1 POST /v1/transactions Criar transação com perfil DIGITAL_CERTIFICATE
2 POST /v1/transactions/{txId}/document Fazer upload do documento PDF
3a POST /v1/transactions/{txId}/steps/{stepId}/start Iniciar etapa CLICK_ACCEPT
3b POST /v1/transactions/{txId}/steps/{stepId}/complete Concluir etapa CLICK_ACCEPT
4 POST /v1/transactions/{txId}/steps/{stepId}/start Iniciar etapa DIGITAL_SIGN_A1
5 POST /v1/transactions/{txId}/signing/prepare Enviar certificado folha, receber hash
6 -- (lado do cliente) Assinar hash com chave privada
7 POST /v1/transactions/{txId}/signing/complete Enviar assinatura bruta
8 POST /v1/transactions/{txId}/finalize Finalizar e construir evidências
9 GET /v1/transactions/{txId}/download Obter URLs pré-assinadas para download

Scopes OAuth2 Necessários

Endpoint Scope
Criar transação transactions:write
Fazer upload de documento transactions:write
Iniciar / concluir etapas steps:write
Preparar / concluir assinatura steps:write
Finalizar transação transactions:write
Baixar documento transactions:read