Guias de Desenvolvimento

Política e Conformidade

Signing Sessions

Sessão de Confiança

Modelos de Solução

Guias dos SDKs

Onboarding e Homologação de Fornecedores com Assinatura Eletrônica — Modelo de Solução

Digitalize a homologação de fornecedores de ponta a ponta: verifique a identidade do representante legal (KYC), assine o contrato de fornecimento online com validade jurídica fundada na MP 2.200-2/2001 e arquive um pacote de evidências assinado com certificado ICP-Brasil — tudo por API, integrado ao seu ERP e em conformidade com a LGPD. O resultado: cadastro de fornecedores com assinatura eletrônica em horas, não semanas, com trilha de auditoria completa para o compliance de compras.

Público-alvo: times de compras/procurement e desenvolvedores que integram ERPs (SAP, TOTVS, Oracle, sistemas próprios) ao processo de qualificação e contratação de fornecedores.


⚡ Caminho de 20 minutos

  1. Obtenha credenciais HML no painel admin ou por contato@signdocs.com.br.
  2. Autentique com OAuth2 client_credentials em POST /oauth2/token.
  3. Crie uma sessão de confiança KYC (POST /v1/trust-sessions) para o representante legal do fornecedor, com metadata.supplierId, e envie o link de verificação.
  4. Ao receber o webhook TRANSACTION.COMPLETED, crie o envelope do contrato de fornecimento (POST /v1/envelopes) com fornecedor + comprador e entregue os links de assinatura.
  5. Ao receber ENVELOPE.ALL_SIGNED, baixe o PDF com carimbo combinado e o pacote de evidências — fornecedor homologado, contrato assinado e arquivado.

1. O problema de negócio

A homologação de fornecedores tradicional é um funil lento: o time de compras coleta contrato social, procurações e documentos do representante legal por e-mail, confere tudo manualmente, imprime o contrato de fornecimento, envia para assinatura física (ou coleta assinaturas escaneadas sem qualquer verificação de identidade) e só então libera o cadastro no ERP. Cada etapa manual adiciona dias ao lead time — e fornecedores estratégicos ficam parados na fila enquanto a operação precisa deles ativos.

Pior: o processo em papel raramente responde à pergunta que o compliance faz depois — quem assinou o contrato de fornecimento era mesmo o representante legal daquela empresa? Uma assinatura escaneada colada num PDF não prova identidade, não gera trilha de auditoria e não resiste a uma disputa contratual ou a uma auditoria de fraude em cadeia de suprimentos.

A homologação de fornecedores digital resolve as duas pontas com um fluxo em duas fases: primeiro uma verificação de identidade do representante legal (KYC via sessão de confiança, com biometria facial opcional e CPF/CNPJ validados no fluxo), depois o contrato de fornecimento online assinado eletronicamente por fornecedor e comprador em um envelope. As duas fases ficam correlacionadas pelo metadata (ex.: supplierId do seu ERP), e cada uma gera um pacote de evidências independente com hash SHA-256, timestamp ISO do servidor e trilha de auditoria, assinado com certificado ICP-Brasil.

2. Arquitetura do fluxo

ERP / Sistema de Compras
   │
   │ FASE 1 — verificação do representante legal
   │ POST /v1/trust-sessions  (metadata.supplierId = forn-8842)
   ▼
Representante legal ──verifica identidade──▶ webhook TRANSACTION.COMPLETED
   │                                              │
   │ FASE 2 — contrato de fornecimento            │ (seu backend cria o envelope)
   │ POST /v1/envelopes  (metadata.supplierId)    │
   │ POST /v1/envelopes/{id}/sessions  × 2        │
   ▼                                              ▼
Fornecedor assina ──▶ Comprador assina ──▶ webhook ENVELOPE.ALL_SIGNED
   │
   ▼
Fornecedor HOMOLOGADO no ERP
(carimbo combinado + pacotes de evidências arquivados)

Por que cada primitiva neste cenário:

Primitiva Papel no fluxo Por quê
Sessão de confiança (/v1/trust-sessions) Fase 1 — KYC do representante legal Autentica uma ação sem documento: prova quem é a pessoa antes de existir contrato. Veja Sessão de Confiança para KYC.
Envelope (/v1/envelopes) Fase 2 — contrato de fornecimento O contrato tem 2+ signatários (fornecedor e comprador) sobre o mesmo PDF, com carimbo combinado ao final.
Sessão de assinatura avulsa (/v1/signing-sessions) Não usada aqui Serve para 1 signatário sobre 1 documento — insuficiente para um contrato bilateral.

3. Pré-requisitos

🟠 HML vs Produção — desenvolva sempre em HML (https://api-hml.signdocs.com.br, com hífen). Credenciais HML não consomem cota do plano e os dados expiram em 7 dias. Produção: https://api.signdocs.com.br.

4. Autenticação OAuth2

Troque client_id + client_secret por um access_token de curta duração via client_credentials:

ACCESS_TOKEN=$(curl -s -X POST "https://api-hml.signdocs.com.br/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')
const BASE = process.env.SIGNDOCS_BASE_URL ?? 'https://api-hml.signdocs.com.br';

async function getAccessToken(): Promise<string> {
  const resp = await fetch(`${BASE}/oauth2/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: process.env.SIGNDOCS_CLIENT_ID!,
      client_secret: process.env.SIGNDOCS_CLIENT_SECRET!,
    }),
  });
  if (!resp.ok) throw new Error(`Token error: ${resp.status}`);
  const { access_token } = await resp.json();
  return access_token;
}
import os, requests

BASE = os.environ.get("SIGNDOCS_BASE_URL", "https://api-hml.signdocs.com.br")

def get_access_token() -> str:
    resp = requests.post(
        f"{BASE}/oauth2/token",
        data={
            "grant_type": "client_credentials",
            "client_id": os.environ["SIGNDOCS_CLIENT_ID"],
            "client_secret": os.environ["SIGNDOCS_CLIENT_SECRET"],
        },
        timeout=10,
    )
    resp.raise_for_status()
    return resp.json()["access_token"]

Cache o token até perto do expires_in e renove — não peça um token novo por requisição.

Antes de qualquer contrato, prove que a pessoa que representa o fornecedor é quem diz ser. A sessão de confiança autentica essa ação de verificação sem documento anexado: o representante recebe um link, passa pelo fluxo de identidade e o resultado vira um pacote de evidências auditável. O signer leva o CPF do representante; o CNPJ do fornecedor e o supplierId do seu ERP vão no metadata — é essa chave que encadeia a Fase 2.

O guia dedicado Sessão de Confiança para KYC cobre o fluxo em profundidade; para KYC de clientes finais (pessoa física em escala), veja o modelo KYC de Clientes.

curl -s -X POST "https://api-hml.signdocs.com.br/v1/trust-sessions" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: kyc-forn-8842-v1" \
  -d '{
    "action": {
      "type": "supplier_kyc_verification",
      "description": "Verificação de identidade do representante legal — homologação do fornecedor forn-8842 (Metalúrgica Aurora Ltda)"
    },
    "policy": { "profile": "BIOMETRIC" },
    "signer": {
      "name": "Carlos Pereira",
      "cpf": "12345678901",
      "email": "carlos@metalurgicaaurora.com.br",
      "userExternalId": "forn-8842-rep",
      "otpChannel": "email"
    },
    "metadata": {
      "supplierId": "forn-8842",
      "supplierCnpj": "12345678000199",
      "erpVendorRecord": "SAP-VENDOR-004421"
    },
    "returnUrl": "https://compras.suaempresa.com.br/fornecedores/forn-8842/verificado",
    "cancelUrl": "https://compras.suaempresa.com.br/fornecedores/forn-8842/cancelado",
    "locale": "pt-BR",
    "expiresInMinutes": 4320
  }'

A resposta traz sessionId, url e clientSecret (one-time). O link entregue ao representante é url + ?cs= + clientSecret.

async function iniciarVerificacaoFornecedor(fornecedor: {
  id: string;
  cnpj: string;
  representante: { nome: string; cpf: string; email: string };
}) {
  const token = await getAccessToken();
  const resp = await fetch(`${BASE}/v1/trust-sessions`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
      'X-Idempotency-Key': `kyc-${fornecedor.id}-v1`,
    },
    body: JSON.stringify({
      action: {
        type: 'supplier_kyc_verification',
        description: `Verificação de identidade do representante legal — homologação do fornecedor ${fornecedor.id}`,
      },
      policy: { profile: 'BIOMETRIC' },
      signer: {
        name: fornecedor.representante.nome,
        cpf: fornecedor.representante.cpf,
        email: fornecedor.representante.email,
        userExternalId: `${fornecedor.id}-rep`,
        otpChannel: 'email',
      },
      metadata: { supplierId: fornecedor.id, supplierCnpj: fornecedor.cnpj },
      returnUrl: `https://compras.suaempresa.com.br/fornecedores/${fornecedor.id}/verificado`,
      cancelUrl: `https://compras.suaempresa.com.br/fornecedores/${fornecedor.id}/cancelado`,
      locale: 'pt-BR',
      expiresInMinutes: 4320,
    }),
  });
  if (!resp.ok) throw new Error(`SignDocs ${resp.status}: ${await resp.text()}`);
  const { sessionId, url, clientSecret } = await resp.json();

  // Guarde a correlação sessionId -> supplierId para o webhook da Fase 2
  await db.fornecedores.update(fornecedor.id, {
    kycSessionId: sessionId,
    status: 'KYC_PENDENTE',
  });
  return `${url}?cs=${clientSecret}`; // entregue este link ao representante
}
def iniciar_verificacao_fornecedor(fornecedor: dict) -> str:
    token = get_access_token()
    resp = requests.post(
        f"{BASE}/v1/trust-sessions",
        headers={
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
            "X-Idempotency-Key": f"kyc-{fornecedor['id']}-v1",
        },
        json={
            "action": {
                "type": "supplier_kyc_verification",
                "description": (
                    "Verificação de identidade do representante legal — "
                    f"homologação do fornecedor {fornecedor['id']}"
                ),
            },
            "policy": {"profile": "BIOMETRIC"},
            "signer": {
                "name": fornecedor["representante"]["nome"],
                "cpf": fornecedor["representante"]["cpf"],
                "email": fornecedor["representante"]["email"],
                "userExternalId": f"{fornecedor['id']}-rep",
                "otpChannel": "email",
            },
            "metadata": {
                "supplierId": fornecedor["id"],
                "supplierCnpj": fornecedor["cnpj"],
            },
            "returnUrl": f"https://compras.suaempresa.com.br/fornecedores/{fornecedor['id']}/verificado",
            "cancelUrl": f"https://compras.suaempresa.com.br/fornecedores/{fornecedor['id']}/cancelado",
            "locale": "pt-BR",
            "expiresInMinutes": 4320,
        },
        timeout=10,
    )
    resp.raise_for_status()
    data = resp.json()

    # Guarde a correlação sessionId -> supplierId para o webhook da Fase 2
    db.fornecedores.update(
        fornecedor["id"],
        kyc_session_id=data["sessionId"],
        status="KYC_PENDENTE",
    )
    return f"{data['url']}?cs={data['clientSecret']}"  # link do representante

DicaBIOMETRIC na verificação exige habilitação de biometria no seu tenant. Se ainda não tiver, comece com policy.profile: "CLICK_PLUS_OTP" na Fase 1 e faça o upgrade depois: o restante do fluxo não muda.

⚠️ Idempotência — derive a X-Idempotency-Key de uma chave de negócio estável (ex.: kyc-{supplierId}-v1), não de um UUID aleatório. Assim, retries após timeout não criam sessões duplicadas para o mesmo fornecedor.

6. Fase 2 — Contrato de fornecimento em envelope (disparado pelo webhook)

Quando o representante conclui a verificação, o SignDocs envia TRANSACTION.COMPLETED ao seu webhook. Seu handler correlaciona o sessionId com o supplierId guardado na Fase 1 e cria o envelope do contrato de fornecimento — repetindo o mesmo metadata.supplierId (e anexando o kycEvidenceId) para que as duas fases fiquem amarradas de ponta a ponta na sua auditoria.

O contrato usa signingMode: "SEQUENTIAL": o fornecedor (signatário 1, assinando como pessoa jurídica via cnpj) assina primeiro; o representante de compras (signatário 2, via cpf) só consegue assinar depois. Documento inline em base64, máximo 10 MB.

PDF_BASE64=$(base64 -w0 contrato-fornecimento-forn-8842.pdf)

# 1. Criar o envelope
ENVELOPE_ID=$(curl -s -X POST "https://api-hml.signdocs.com.br/v1/envelopes" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: contrato-forn-8842-v1" \
  -d "{
    \"signingMode\": \"SEQUENTIAL\",
    \"totalSigners\": 2,
    \"document\": { \"content\": \"$PDF_BASE64\", \"filename\": \"contrato-fornecimento-forn-8842.pdf\" },
    \"metadata\": {
      \"supplierId\": \"forn-8842\",
      \"kycEvidenceId\": \"evd_01JKYC...\",
      \"contractType\": \"fornecimento\"
    },
    \"owner\": { \"email\": \"compras@suaempresa.com.br\", \"name\": \"Equipe de Compras\" },
    \"returnUrl\": \"https://compras.suaempresa.com.br/fornecedores/forn-8842/contrato\",
    \"locale\": \"pt-BR\",
    \"expiresInMinutes\": 10080
  }" | jq -r '.envelopeId')

# 2. Signatário 1 — fornecedor (assina como PJ, via CNPJ)
curl -s -X POST "https://api-hml.signdocs.com.br/v1/envelopes/$ENVELOPE_ID/sessions" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "signer": {
      "name": "Carlos Pereira",
      "email": "carlos@metalurgicaaurora.com.br",
      "cnpj": "12345678000199",
      "userExternalId": "forn-8842-rep"
    },
    "policy": { "profile": "CLICK_PLUS_OTP" },
    "purpose": "DOCUMENT_SIGNATURE",
    "signerIndex": 1
  }'

# 3. Signatário 2 — comprador
curl -s -X POST "https://api-hml.signdocs.com.br/v1/envelopes/$ENVELOPE_ID/sessions" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "signer": {
      "name": "Ana Ribeiro",
      "email": "ana.ribeiro@suaempresa.com.br",
      "cpf": "98765432100",
      "userExternalId": "compras-ana"
    },
    "policy": { "profile": "CLICK_PLUS_OTP" },
    "purpose": "DOCUMENT_SIGNATURE",
    "signerIndex": 2
  }'

Cada chamada de sessão retorna url + clientSecret — monte o link como url?cs={clientSecret}.

// Handler do webhook (Express) — valide a assinatura HMAC antes: ./webhooks.html
app.post('/webhooks/signdocs', async (req, res) => {
  const { event, data } = req.body;

  if (event === 'TRANSACTION.COMPLETED') {
    const fornecedor = await db.fornecedores.findByKycSession(data.sessionId);
    if (fornecedor && fornecedor.status === 'KYC_PENDENTE') {
      await criarEnvelopeContrato(fornecedor, data.evidenceId);
    }
  }
  res.status(200).end();
});

async function criarEnvelopeContrato(fornecedor: Fornecedor, kycEvidenceId: string) {
  const token = await getAccessToken();
  const pdfBase64 = (await gerarContratoPdf(fornecedor)).toString('base64');

  const envResp = await fetch(`${BASE}/v1/envelopes`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
      'X-Idempotency-Key': `contrato-${fornecedor.id}-v1`,
    },
    body: JSON.stringify({
      signingMode: 'SEQUENTIAL',
      totalSigners: 2,
      document: { content: pdfBase64, filename: `contrato-fornecimento-${fornecedor.id}.pdf` },
      metadata: { supplierId: fornecedor.id, kycEvidenceId, contractType: 'fornecimento' },
      owner: { email: 'compras@suaempresa.com.br', name: 'Equipe de Compras' },
      returnUrl: `https://compras.suaempresa.com.br/fornecedores/${fornecedor.id}/contrato`,
      locale: 'pt-BR',
      expiresInMinutes: 10080,
    }),
  });
  const { envelopeId } = await envResp.json();

  const addSession = (body: object) =>
    fetch(`${BASE}/v1/envelopes/${envelopeId}/sessions`, {
      method: 'POST',
      headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    }).then((r) => r.json());

  const s1 = await addSession({
    signer: {
      name: fornecedor.representante.nome,
      email: fornecedor.representante.email,
      cnpj: fornecedor.cnpj,
      userExternalId: `${fornecedor.id}-rep`,
    },
    policy: { profile: 'CLICK_PLUS_OTP' },
    purpose: 'DOCUMENT_SIGNATURE',
    signerIndex: 1,
  });
  const s2 = await addSession({
    signer: { name: 'Ana Ribeiro', email: 'ana.ribeiro@suaempresa.com.br', cpf: '98765432100', userExternalId: 'compras-ana' },
    policy: { profile: 'CLICK_PLUS_OTP' },
    purpose: 'DOCUMENT_SIGNATURE',
    signerIndex: 2,
  });

  await db.fornecedores.update(fornecedor.id, {
    status: 'CONTRATO_PENDENTE',
    envelopeId,
    linkFornecedor: `${s1.url}?cs=${s1.clientSecret}`,
    linkComprador: `${s2.url}?cs=${s2.clientSecret}`,
  });
}
# Handler do webhook (Flask) — valide a assinatura HMAC antes: ./webhooks.html
@app.post("/webhooks/signdocs")
def signdocs_webhook():
    body = request.get_json()
    event, data = body["event"], body["data"]

    if event == "TRANSACTION.COMPLETED":
        fornecedor = db.fornecedores.find_by_kyc_session(data["sessionId"])
        if fornecedor and fornecedor.status == "KYC_PENDENTE":
            criar_envelope_contrato(fornecedor, data["evidenceId"])
    return "", 200


def criar_envelope_contrato(fornecedor, kyc_evidence_id: str):
    token = get_access_token()
    pdf_base64 = gerar_contrato_pdf_base64(fornecedor)
    headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}

    env = requests.post(
        f"{BASE}/v1/envelopes",
        headers={**headers, "X-Idempotency-Key": f"contrato-{fornecedor.id}-v1"},
        json={
            "signingMode": "SEQUENTIAL",
            "totalSigners": 2,
            "document": {"content": pdf_base64, "filename": f"contrato-fornecimento-{fornecedor.id}.pdf"},
            "metadata": {"supplierId": fornecedor.id, "kycEvidenceId": kyc_evidence_id, "contractType": "fornecimento"},
            "owner": {"email": "compras@suaempresa.com.br", "name": "Equipe de Compras"},
            "returnUrl": f"https://compras.suaempresa.com.br/fornecedores/{fornecedor.id}/contrato",
            "locale": "pt-BR",
            "expiresInMinutes": 10080,
        },
        timeout=10,
    ).json()

    def add_session(payload):
        return requests.post(
            f"{BASE}/v1/envelopes/{env['envelopeId']}/sessions",
            headers=headers, json=payload, timeout=10,
        ).json()

    s1 = add_session({
        "signer": {
            "name": fornecedor.representante.nome,
            "email": fornecedor.representante.email,
            "cnpj": fornecedor.cnpj,
            "userExternalId": f"{fornecedor.id}-rep",
        },
        "policy": {"profile": "CLICK_PLUS_OTP"},
        "purpose": "DOCUMENT_SIGNATURE",
        "signerIndex": 1,
    })
    s2 = add_session({
        "signer": {"name": "Ana Ribeiro", "email": "ana.ribeiro@suaempresa.com.br",
                   "cpf": "98765432100", "userExternalId": "compras-ana"},
        "policy": {"profile": "CLICK_PLUS_OTP"},
        "purpose": "DOCUMENT_SIGNATURE",
        "signerIndex": 2,
    })

    db.fornecedores.update(
        fornecedor.id,
        status="CONTRATO_PENDENTE",
        envelope_id=env["envelopeId"],
        link_fornecedor=f"{s1['url']}?cs={s1['clientSecret']}",
        link_comprador=f"{s2['url']}?cs={s2['clientSecret']}",
    )

⚠️ O clientSecret é one-time e não é armazenado — guarde o link montado no seu banco no momento da criação. Se perder, cancele a sessão e crie outra.

8. Homologação: contrato assinado e evidências arquivadas

Quando o segundo signatário conclui, chega o webhook ENVELOPE.ALL_SIGNED. É o gatilho para marcar o fornecedor como homologado no ERP e arquivar os artefatos finais:

# PDF final com carimbo combinado de todas as assinaturas
curl -s -X POST "https://api-hml.signdocs.com.br/v1/envelopes/$ENVELOPE_ID/combined-stamp" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -o contrato-fornecimento-assinado.pdf

# Pacote de evidências (JSON assinado em PKCS#7/CMS, certificado ICP-Brasil A1)
curl -s "https://api-hml.signdocs.com.br/v1/evidence/$EVIDENCE_ID" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -o pacote-evidencias-forn-8842.p7m
// No mesmo handler de webhook:
if (event === 'ENVELOPE.ALL_SIGNED') {
  const fornecedor = await db.fornecedores.findByEnvelope(data.envelopeId);
  const token = await getAccessToken();

  const stamp = await fetch(`${BASE}/v1/envelopes/${data.envelopeId}/combined-stamp`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${token}` },
  });
  await salvarNoArquivo(fornecedor.id, 'contrato-assinado.pdf', await stamp.arrayBuffer());

  await db.fornecedores.update(fornecedor.id, { status: 'HOMOLOGADO' });
  await erp.liberarCadastroFornecedor(fornecedor.id); // libera pedidos de compra
}
# No mesmo handler de webhook:
if event == "ENVELOPE.ALL_SIGNED":
    fornecedor = db.fornecedores.find_by_envelope(data["envelopeId"])
    token = get_access_token()

    stamp = requests.post(
        f"{BASE}/v1/envelopes/{data['envelopeId']}/combined-stamp",
        headers={"Authorization": f"Bearer {token}"}, timeout=30,
    )
    salvar_no_arquivo(fornecedor.id, "contrato-assinado.pdf", stamp.content)

    db.fornecedores.update(fornecedor.id, status="HOMOLOGADO")
    erp.liberar_cadastro_fornecedor(fornecedor.id)  # libera pedidos de compra

A prova de integridade de cada fase = timestamp ISO do servidor + trilha de auditoria + hash SHA-256 do documento + pacote de evidências assinado com certificado ICP-Brasil. Qualquer parte pode verificar depois via GET /v1/verify/{evidenceId} ou reenviando o PDF em POST /v1/verify/document. Detalhes do pacote: evidências de sessão de confiança.

Webhooks: reagindo à conclusão

Registre um único endpoint para os eventos das duas fases:

curl -s -X POST "https://api-hml.signdocs.com.br/v1/webhooks" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://erp.suaempresa.com.br/webhooks/signdocs",
    "events": [
      "TRANSACTION.COMPLETED",
      "TRANSACTION.FAILED",
      "ENVELOPE.ALL_SIGNED",
      "SIGNING_SESSION.EXPIRED"
    ]
  }'

Papel de cada evento neste fluxo:

Evento O que significa aqui Ação do seu backend
TRANSACTION.COMPLETED Fase 1 concluída — representante verificado Criar o envelope do contrato (Fase 2)
TRANSACTION.FAILED Verificação reprovada ou falhou Marcar fornecedor para revisão manual
ENVELOPE.ALL_SIGNED Contrato assinado por ambos Homologar fornecedor, baixar carimbo + evidências
SIGNING_SESSION.EXPIRED Um signatário não assinou no prazo Notificar compras, reemitir sessão

Todo payload vem assinado com HMAC-SHA256 — valide a assinatura e o timestamp antes de processar, e trate entregas duplicadas (at-least-once) com idempotência no consumo. Especificação completa, headers e código de validação: guia de webhooks e webhooks com SDK.

Escolhendo o perfil de política

Perfil Como funciona Quando usar neste cenário
CLICK_PLUS_OTP Aceite + código OTP por e-mail/SMS Recomendado para o contrato de fornecimento — verificação de posse do canal com fricção baixa, adequado a um fluxo B2B onde a identidade já foi provada na Fase 1
BIOMETRIC Verificação facial na página hospedada Opcional na Fase 1 (KYC) — eleva a garantia de identidade do representante; requer biometria habilitada no tenant
CLICK_ONLY Apenas aceite com um clique Aditivos de baixo risco entre partes já homologadas; evite no contrato principal
DIGITAL_CERTIFICATE Assinatura com certificado ICP-Brasil (e-CPF/e-CNPJ) Quando a política de compras ou o contrato exigir assinatura com certificado digital

Default recomendado: BIOMETRIC (ou CLICK_PLUS_OTP) na verificação da Fase 1 e CLICK_PLUS_OTP no contrato da Fase 2 — a combinação prova identidade uma vez e mantém a assinatura do contrato fluida. Comparativo completo de níveis de garantia e enquadramento legal: perfis de assinatura e níveis legais.

Caminho sem código

O fluxo inteiro roda no Make.com sem escrever backend, ligando seu ERP/planilha de fornecedores ao SignDocs:

Passo a passo do app e da conexão OAuth2: integração Make.com. O mesmo desenho funciona em n8n e Zapier.

Erros comuns

1. Signer sem CPF nem CNPJ (400)

{ "status": 400, "title": "Validation Error", "detail": "signer: é obrigatório informar pelo menos um entre 'cpf' e 'cnpj'" }

Correção: envie cpf (11 dígitos, sem pontuação) para pessoa física ou cnpj (14 dígitos) para o fornecedor assinando como PJ.

2. DIGITAL_SIGN_A1 usado como perfil de política (400)

{ "status": 400, "title": "Validation Error", "detail": "policy.profile: valor inválido 'DIGITAL_SIGN_A1'" }

Correção: o perfil correto é DIGITAL_CERTIFICATE. DIGITAL_SIGN_A1 aparece apenas como tipo de step nas respostas, nunca como policy.profile na requisição.

3. Webhook recusado na borda (403 CloudFront)

{ "status": 403, "title": "Forbidden", "detail": "webhook url rejected at edge" }

Correção: a URL do webhook aponta para localhost ou IP privado — o registro exige HTTPS público. Em desenvolvimento, use um túnel (ngrok/cloudflared) ou webhook.site.

4. Token expirado (401)

{ "status": 401, "title": "Unauthorized", "detail": "access token inválido ou expirado" }

Correção: renove o token via POST /oauth2/token antes do expires_in e verifique se as credenciais correspondem ao ambiente da URL (HML vs produção).

5. Envelope não encontrado em HML (404)

{ "status": 404, "title": "Not Found", "detail": "envelope não encontrado" }

Correção: em HML os dados expiram em 7 dias. Se o teste ficou parado além disso, recrie os recursos — em produção o histórico permanece consultável.

Perguntas frequentes

Homologação de fornecedores digital tem validade jurídica?

Sim. O contrato de fornecimento assinado eletronicamente tem validade fundada na MP 2.200-2/2001, art. 10 §2º, pelo consentimento das partes quanto ao meio de assinatura — e o pacote de evidências (hash SHA-256, trilha de auditoria, timestamp do servidor) materializa essa prova. Para os níveis de garantia por perfil, veja perfis de assinatura e níveis legais.

O fornecedor precisa ter certificado digital ICP-Brasil?

Não. Com CLICK_PLUS_OTP o representante assina com aceite + código OTP, sem certificado. O perfil DIGITAL_CERTIFICATE fica disponível quando sua política de compras exigir assinatura com e-CPF/e-CNPJ — basta trocar o policy.profile da sessão do fornecedor.

Como faço o cadastro de fornecedores com assinatura eletrônica direto do meu ERP?

Integre por API (este guia) usando metadata.supplierId como chave de correlação entre o registro do fornecedor no ERP e os recursos SignDocs, ou monte o fluxo sem código no Make/n8n/Zapier a partir de um trigger do próprio ERP. Os webhooks devolvem os eventos para o ERP atualizar o status do fornecedor automaticamente.

Você recebe TRANSACTION.FAILED no webhook e o envelope da Fase 2 nunca é criado — o fornecedor fica retido para análise manual do time de compras. Nenhum contrato é enviado a um representante cuja identidade não foi confirmada.

Posso assinar o contrato de fornecimento online com mais de dois signatários?

Sim. O envelope aceita totalSigners ≥ 2 — inclua testemunhas ou um segundo aprovador de compras adicionando mais sessões com signerIndex sequencial. Em SEQUENTIAL, a ordem dos índices define a ordem de assinatura.

Como provo depois que o contrato não foi alterado?

Cada fase gera um pacote de evidências JSON assinado em PKCS#7/CMS com certificado ICP-Brasil A1, incluindo o hash SHA-256 do documento. Verifique a qualquer momento via GET /v1/verify/{evidenceId} ou reenviando o PDF em POST /v1/verify/document.

Próximos passos