Guias de Desenvolvimento

Política e Conformidade

Signing Sessions

Sessão de Confiança

Modelos de Solução

Guias dos SDKs

Aprovação e Aceite de Proposta Comercial com Assinatura Eletrônica — Modelo de Solução

Transforme "proposta enviada" em "contrato fechado" no mesmo dia: o cliente recebe a proposta, aceita online com um clique e o seu CRM marca o deal como ganho automaticamente. O aceite eletrônico tem validade jurídica fundada na MP 2.200-2/2001 (art. 10, §2º — validade pelo consentimento das partes), com trilha de auditoria completa, hash SHA-256 do documento e pacote de evidências assinado com certificado ICP-Brasil, em conformidade com a LGPD. Menos fricção no fechamento significa ciclo de vendas mais curto e menos propostas perdidas por follow-up manual.

Público-alvo: times de vendas e RevOps que querem fechar contrato mais rápido, e desenvolvedores que integram CRM (HubSpot, Pipedrive, RD Station, Salesforce ou CRM próprio) a um fluxo de aceite de proposta online.


⚡ Caminho de 15 minutos

  1. Solicite credenciais HML no painel admin e obtenha um access_token via POST /oauth2/token.
  2. Crie uma sessão de assinatura (POST /v1/signing-sessions) com o PDF da proposta, perfil CLICK_ONLY e metadata contendo dealId e crmUrl.
  3. Informe owner com o e-mail do vendedor — a SignDocs envia automaticamente o convite de aceite ao cliente.
  4. Registre um webhook para SIGNING_SESSION.COMPLETED e, ao recebê-lo, atualize o deal no CRM usando a metadata da transação.
  5. Baixe o documento aceito e o pacote de evidências (GET /v1/transactions/{transactionId}/download e /evidence) e anexe ao deal.

Resultado: proposta aceita com registro probatório completo e CRM atualizado sem nenhum passo manual.


1. O problema de negócio

O aceite de proposta comercial é, na maioria das empresas, o elo mais lento do funil: o vendedor exporta o PDF, envia por e-mail, pede "um de acordo por escrito" e fica refém da caixa de entrada do cliente. Quando o aceite chega — às vezes como uma resposta vaga de e-mail, às vezes como um PDF impresso, assinado e fotografado — alguém ainda precisa atualizar o CRM à mão. Cada dia de espera é chance de o concorrente entrar, de o orçamento congelar ou de o decisor mudar.

Um fluxo de aceite de proposta online resolve as duas pontas. Para o cliente, aceitar a proposta vira um clique em uma página hospedada, no celular ou no desktop, sem cadastro e sem imprimir nada. Para o time de vendas, a assinatura eletrônica da proposta comercial gera evidência jurídica real (quem aceitou, quando, de qual IP, sobre qual versão exata do documento) — muito mais defensável que um "de acordo" por e-mail. E para o RevOps, a integração CRM + assinatura digital fecha o ciclo: o webhook de conclusão carrega os identificadores necessários para marcar o deal como fechado-ganho no segundo em que o cliente aceita.

Este guia monta esse fluxo de ponta a ponta com a API da SignDocs Brasil: deal ganho no CRM → sessão de assinatura criada com os dados do deal em metadata → cliente aceita → webhook devolve o controle ao seu sistema → CRM atualizado e evidência arquivada. Como bônus, mostramos como tratar alçada interna (desconto acima do limite precisa de aprovação do diretor) com uma sessão de confiança, sem gerar PDF nenhum.

2. Arquitetura do fluxo

CRM (deal em "Proposta")              SignDocs                          Cliente
     │                                    │                                │
     │ 1. POST /v1/signing-sessions       │                                │
     │    metadata: dealId, crmUrl  ─────>│                                │
     │                                    │ 2. e-mail de convite ─────────>│
     │                                    │    (owner informado)           │
     │                                    │<─ 3. abre o link e aceita ─────│
     │                                    │      (CLICK_ONLY)              │
     │<─ 4. webhook                       │                                │
     │    SIGNING_SESSION.COMPLETED       │                                │
     │                                    │                                │
     │ 5. GET /v1/transactions/{id} ─────>│                                │
     │    lê metadata.dealId              │                                │
     │ 6. marca deal "Fechado-ganho"      │                                │
     │ 7. baixa PDF aceito + evidência ──>│                                │

Por que cada primitiva neste cenário:

Primitiva Papel neste fluxo Quando usar
Sessão de assinatura (POST /v1/signing-sessions) Aceite da proposta pelo cliente 1 signatário sobre 1 documento — o caso típico de proposta: quem aceita é o decisor do cliente
Envelope (POST /v1/envelopes) Contrato definitivo pós-aceite 2+ signatários no mesmo documento (cliente + representante legal da sua empresa); veja envelopes
Sessão de confiança (POST /v1/trust-sessions) Alçada interna: aprovação do desconto pelo diretor Autorizar uma ação sem documento — não faz sentido gerar PDF para um "aprovo o desconto de 25%"; veja aprovação

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

A API usa OAuth2 client_credentials (servidor-a-servidor). Troque client_id + client_secret por um access_token de curta duração:

export SIGNDOCS_BASE_URL="https://api-hml.signdocs.com.br"

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')
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, // https://api-hml.signdocs.com.br
});
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'),
))

O SDK renova o token automaticamente. Em integrações HTTP puras, faça cache do token até perto do expires_in.

5. Criar a sessão de aceite com os dados do deal

Este é o coração da integração. Quando o deal chega ao estágio "Proposta enviada" (ou quando o vendedor clica "Enviar para aceite"), seu sistema cria a sessão de assinatura. Dois campos fazem a ponte com o CRM:

Informar owner com os dados do vendedor faz a SignDocs enviar automaticamente o e-mail de convite ao cliente (quando signer.email for diferente de owner.email) e notificar o vendedor na conclusão. Se preferir entregar o link pelo seu próprio canal (e-mail transacional, WhatsApp Business do cliente via automação), omita owner.

PDF_BASE64=$(base64 -w0 proposta-8842.pdf)

curl -s -X POST "$SIGNDOCS_BASE_URL/v1/signing-sessions" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "purpose": "DOCUMENT_SIGNATURE",
    "policy": { "profile": "CLICK_ONLY" },
    "signer": {
      "name": "João Andrade",
      "email": "joao.andrade@clienteexemplo.com.br",
      "cpf": "12345678901",
      "userExternalId": "crm-contact-4415"
    },
    "owner": { "name": "Ana Vendas", "email": "ana@suaempresa.com.br" },
    "document": { "content": "'"$PDF_BASE64"'", "filename": "proposta-8842-v3.pdf" },
    "metadata": {
      "dealId": "8842",
      "crmUrl": "https://crm.suaempresa.com.br/deals/8842",
      "proposalVersion": "v3",
      "amountBRL": "48500.00"
    },
    "returnUrl": "https://suaempresa.com.br/proposta/obrigado",
    "locale": "pt-BR",
    "expiresInMinutes": 1440
  }'
import { readFileSync } from 'fs';

const pdfBase64 = readFileSync('proposta-8842-v3.pdf').toString('base64');

const session = await client.signingSessions.create({
  purpose: 'DOCUMENT_SIGNATURE',
  policy: { profile: 'CLICK_ONLY' },
  signer: {
    name: 'João Andrade',
    email: 'joao.andrade@clienteexemplo.com.br',
    cpf: '12345678901',
    userExternalId: 'crm-contact-4415',
  },
  owner: { name: 'Ana Vendas', email: 'ana@suaempresa.com.br' },
  document: { content: pdfBase64, filename: 'proposta-8842-v3.pdf' },
  metadata: {
    dealId: '8842',
    crmUrl: 'https://crm.suaempresa.com.br/deals/8842',
    proposalVersion: 'v3',
    amountBRL: '48500.00',
  },
  returnUrl: 'https://suaempresa.com.br/proposta/obrigado',
  locale: 'pt-BR',
  expiresInMinutes: 1440, // proposta válida por 24h
});

console.log('Session ID:', session.sessionId);
console.log('Convite enviado pela SignDocs?', session.inviteSent);
import base64

with open('proposta-8842-v3.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 Andrade',
        email='joao.andrade@clienteexemplo.com.br',
        cpf='12345678901',
        user_external_id='crm-contact-4415',
    ),
    owner=Owner(name='Ana Vendas', email='ana@suaempresa.com.br'),
    document=InlineDocument(content=pdf_base64, filename='proposta-8842-v3.pdf'),
    metadata={
        'dealId': '8842',
        'crmUrl': 'https://crm.suaempresa.com.br/deals/8842',
        'proposalVersion': 'v3',
        'amountBRL': '48500.00',
    },
    return_url='https://suaempresa.com.br/proposta/obrigado',
    locale='pt-BR',
    expires_in_minutes=1440,  # proposta válida por 24h
))

print('Session ID:', session.session_id)

⚠️ Proposta válida por mais de 24 horas?expiresInMinutes aceita no máximo 1440 (24h). Para propostas com validade de vários dias, trate o evento SIGNING_SESSION.EXPIRED no webhook: enquanto a data de validade da proposta não passou, crie uma nova sessão com o mesmo PDF e a mesma metadata e reenvie o link. Você mantém a validade comercial no seu sistema; a sessão é só a janela de aceite ativa.

Dica — grave sessionId e transactionId da resposta como campos customizados do deal no CRM. Junto com a metadata, isso dá rastreabilidade nos dois sentidos: do CRM para a SignDocs e da SignDocs para o CRM.

A resposta da criação traz url e clientSecret. O link que o cliente abre é a combinação dos dois — url sozinha não funciona:

# Da resposta do passo anterior:
# "url": "https://sign-hml.signdocs.com.br/s/01JXYZ...",
# "clientSecret": "ss_secret_eyJhbGciOi..."

SIGNING_LINK="${SESSION_URL}?cs=${CLIENT_SECRET}"
echo "$SIGNING_LINK"
const signingLink = `${session.url}?cs=${session.clientSecret}`;

// Se owner foi informado e session.inviteSent === true, a SignDocs já
// enviou o convite por e-mail. Use o link direto para canais adicionais:
// e-mail transacional próprio, SMS ou WhatsApp Business (via automação sua).
await crm.updateDeal('8842', { proposal_signing_link: signingLink });
signing_link = f"{session.url}?cs={session.client_secret}"

# Se owner foi informado e session.invite_sent for True, o convite por
# e-mail já saiu. Guarde o link no deal para reenvio manual pelo vendedor.
crm.update_deal('8842', {'proposal_signing_link': signing_link})

⚠️ O clientSecret é sensível — quem tem o link completo consegue abrir a sessão de aceite. Não registre o link em logs nem o exponha em listagens do CRM visíveis a quem não é dono do deal.

7. Alçada interna: aprovação de desconto antes do envio

Desconto acima de X% precisa do "de acordo" do diretor comercial — mas gerar um PDF só para isso é burocracia. Use uma sessão de confiança (POST /v1/trust-sessions): mesma mecânica de link hospedado e evidência, porém autenticando uma ação em vez de um documento. Só depois da aprovação interna o seu sistema cria a sessão de aceite do passo 5.

curl -s -X POST "$SIGNDOCS_BASE_URL/v1/trust-sessions" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": {
      "type": "approve_discount",
      "description": "Aprovar desconto de 25% na proposta #8842 (Cliente Exemplo Ltda, R$ 48.500,00)"
    },
    "policy": { "profile": "CLICK_PLUS_OTP" },
    "signer": {
      "name": "Ricardo Diretor",
      "email": "ricardo@suaempresa.com.br",
      "cpf": "98765432100",
      "userExternalId": "employee-0007"
    },
    "metadata": {
      "dealId": "8842",
      "discountPercent": "25",
      "requestedBy": "ana@suaempresa.com.br"
    },
    "expiresInMinutes": 240
  }'
const approval = await fetch(`${process.env.SIGNDOCS_BASE_URL}/v1/trust-sessions`, {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    action: {
      type: 'approve_discount',
      description: 'Aprovar desconto de 25% na proposta #8842 (Cliente Exemplo Ltda, R$ 48.500,00)',
    },
    policy: { profile: 'CLICK_PLUS_OTP' },
    signer: {
      name: 'Ricardo Diretor',
      email: 'ricardo@suaempresa.com.br',
      cpf: '98765432100',
      userExternalId: 'employee-0007',
    },
    metadata: {
      dealId: '8842',
      discountPercent: '25',
      requestedBy: 'ana@suaempresa.com.br',
    },
    expiresInMinutes: 240,
  }),
}).then((r) => r.json());
// Entregue approval.url + '?cs=' + approval.clientSecret ao diretor
approval = requests.post(
    f"{os.environ['SIGNDOCS_BASE_URL']}/v1/trust-sessions",
    headers={'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json'},
    json={
        'action': {
            'type': 'approve_discount',
            'description': 'Aprovar desconto de 25% na proposta #8842 (Cliente Exemplo Ltda, R$ 48.500,00)',
        },
        'policy': {'profile': 'CLICK_PLUS_OTP'},
        'signer': {
            'name': 'Ricardo Diretor',
            'email': 'ricardo@suaempresa.com.br',
            'cpf': '98765432100',
            'userExternalId': 'employee-0007',
        },
        'metadata': {
            'dealId': '8842',
            'discountPercent': '25',
            'requestedBy': 'ana@suaempresa.com.br',
        },
        'expiresInMinutes': 240,
    },
).json()
# Entregue approval['url'] + '?cs=' + approval['clientSecret'] ao diretor

O diretor recebe o link, revisa a descrição da ação e confirma com clique + código OTP. O evento de conclusão libera o envio da proposta. O padrão completo (idempotência, consumo único da evidência, cancelamento) está no guia Sessão de Confiança — Aprovação.

8. Acompanhar o aceite

Webhook é o caminho recomendado (próxima seção), mas o endpoint leve de status resolve consultas pontuais — por exemplo, um botão "Atualizar status" no card do deal:

curl -s "$SIGNDOCS_BASE_URL/v1/signing-sessions/$SESSION_ID/status" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
# {"sessionId":"...","transactionId":"...","status":"COMPLETED","completedAt":"...","evidenceId":"ev_..."}
const status = await client.signingSessions.getStatus(session.sessionId);
console.log(status.status); // PENDING | COMPLETED | EXPIRED | CANCELLED

// Ou bloqueie até concluir (útil em scripts e testes):
const result = await client.signingSessions.waitForCompletion(session.sessionId);
console.log('Evidence ID:', result.evidenceId);
status = client.signing_sessions.get_status(session.session_id)
print(status.status)  # PENDING | COMPLETED | EXPIRED | CANCELLED

# Ou bloqueie até concluir (útil em scripts e testes):
result = client.signing_sessions.wait_for_completion(session.session_id)
print('Evidence ID:', result.evidence_id)

Se a proposta for retirada (deal perdido, condições alteradas), cancele a sessão com POST /v1/signing-sessions/{sessionId}/cancel para invalidar o link imediatamente.

9. Baixar o documento aceito e a evidência

Após a conclusão, anexe ao deal duas coisas: o PDF aceito e o pacote de evidências — um JSON assinado em PKCS#7/CMS (.p7m) com certificado ICP-Brasil A1, contendo a trilha de auditoria, o hash SHA-256 do documento e os timestamps ISO do servidor.

# URLs pré-assinadas (expiram em 1 hora) do documento original e do assinado
curl -s "$SIGNDOCS_BASE_URL/v1/transactions/$TRANSACTION_ID/download" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# Metadados da evidência + URL de download do .p7m
curl -s "$SIGNDOCS_BASE_URL/v1/transactions/$TRANSACTION_ID/evidence" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
const downloads = await client.transactions.getDownloadUrls(transactionId);
const evidence = await client.transactions.getEvidence(transactionId);

// Arquive junto ao deal: PDF aceito + evidence pack (.p7m)
await crm.attachFile('8842', downloads.signedDocumentUrl, 'proposta-aceita.pdf');
await crm.attachFile('8842', evidence.downloadUrl, 'evidencia-aceite.p7m');
downloads = client.transactions.get_download_urls(transaction_id)
evidence = client.transactions.get_evidence(transaction_id)

crm.attach_file('8842', downloads.signed_document_url, 'proposta-aceita.pdf')
crm.attach_file('8842', evidence.download_url, 'evidencia-aceite.p7m')

Qualquer pessoa pode conferir a autenticidade depois via GET /v1/verify/{evidenceId} — útil para auditoria ou disputa. Em HML, lembre que os dados expiram em 7 dias; em produção a evidência fica disponível de forma durável.

Webhooks: reagindo à conclusão

Registre um webhook para os eventos do ciclo de vida da proposta:

curl -s -X POST "$SIGNDOCS_BASE_URL/v1/webhooks" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://integracao.suaempresa.com.br/webhooks/signdocs",
    "events": ["SIGNING_SESSION.COMPLETED", "SIGNING_SESSION.EXPIRED", "SIGNING_SESSION.CANCELLED"]
  }'

O payload de SIGNING_SESSION.COMPLETED traz os identificadores da sessão e da transação:

{
  "id": "del_01HX9Z7RABCDE4567890123",
  "eventType": "SIGNING_SESSION.COMPLETED",
  "tenantId": "tn_01HX9Z0ABCDEF1234567890",
  "transactionId": "tx_01HX9Z3KQWERTY1234567890",
  "timestamp": "2026-07-10T14:35:00.000Z",
  "data": {
    "sessionId": "ss_01HX9Z3KQWERTY1234567890",
    "transactionId": "tx_01HX9Z3KQWERTY1234567890",
    "status": "COMPLETED",
    "evidenceId": "ev_01HX9Z4NFGHIJ0987654321",
    "evidenceHash": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
    "completedAt": "2026-07-10T14:35:00.000Z"
  }
}

Para recuperar a metadata com o dealId, consulte a transação a partir do transactionId do evento — é assim que o webhook "devolve" os dados do deal ao seu handler:

app.post('/webhooks/signdocs', async (req, res) => {
  // 1. Valide a assinatura HMAC-SHA256 (headers X-SignDocs-Signature +
  //    X-SignDocs-Timestamp) ANTES de processar — veja ./webhooks.html
  const event = req.body;

  if (event.eventType === 'SIGNING_SESSION.COMPLETED') {
    // 2. A metadata vive na transação — busque-a pelo transactionId do evento
    const tx = await client.transactions.get(event.data.transactionId);
    const { dealId, crmUrl } = tx.metadata;

    // 3. Feche o ciclo no CRM
    await crm.updateDeal(dealId, {
      stage: 'closed_won',
      accepted_at: event.data.completedAt,
      evidence_id: event.data.evidenceId,
    });
  }

  if (event.eventType === 'SIGNING_SESSION.EXPIRED') {
    const tx = await client.transactions.get(event.data.transactionId);
    await crm.addDealNote(tx.metadata.dealId, 'Link de aceite expirou — reemitir se a proposta ainda vale.');
  }

  res.status(200).end(); // responda 2xx rápido; processe pesado de forma assíncrona
});
@app.post('/webhooks/signdocs')
def signdocs_webhook():
    # 1. Valide a assinatura HMAC-SHA256 (X-SignDocs-Signature +
    #    X-SignDocs-Timestamp) ANTES de processar — veja ./webhooks.html
    event = request.get_json()

    if event['eventType'] == 'SIGNING_SESSION.COMPLETED':
        # 2. A metadata vive na transação — busque-a pelo transactionId do evento
        tx = client.transactions.get(event['data']['transactionId'])
        deal_id = tx.metadata['dealId']

        # 3. Feche o ciclo no CRM
        crm.update_deal(deal_id, {
            'stage': 'closed_won',
            'accepted_at': event['data']['completedAt'],
            'evidence_id': event['data']['evidenceId'],
        })

    return '', 200  # responda 2xx rápido; processe pesado de forma assíncrona

A entrega é at-least-once: torne o handler idempotente (marcar um deal já fechado como fechado deve ser inofensivo). A especificação completa da validação HMAC, retries e replay está em Webhooks.

Escolhendo o perfil de política

Perfil Fricção para o cliente Quando usar em propostas
CLICK_ONLY Mínima — um clique Default recomendado. Aceite de proposta entre partes que já se conhecem e negociaram; validade pela manifestação de vontade (MP 2.200-2, art. 10, §2º)
CLICK_PLUS_OTP Baixa — clique + código por e-mail/SMS Propostas de alto valor ou cliente novo: o OTP amarra o aceite à posse do e-mail/telefone do decisor, fortalecendo a prova de autoria
BIOMETRIC Média — verificação facial Setores com risco elevado de repúdio ou exigência interna de identificação forte (requer signer.cpf)
DIGITAL_CERTIFICATE Alta — exige certificado ICP-Brasil Quando o contrato definitivo (não a proposta) exigir assinatura com certificado ICP-Brasil; raro na fase de aceite

Comece com CLICK_ONLY: no aceite comercial, cada camada extra de autenticação custa conversão, e a evidência gerada (IP, user-agent, timestamps do servidor, hash do documento, trilha de eventos) já sustenta o consentimento. Suba para CLICK_PLUS_OTP quando o valor do deal justificar a fricção — é uma troca de um campo no payload, sem mudança de integração. Detalhes jurídicos de cada nível: Perfis de assinatura e níveis legais.

Caminho sem código

Seu time de RevOps consegue montar esse fluxo sem backend próprio:

Erros comuns

Todos os erros seguem RFC 7807 (application/problem+json).

1. Perfil de política inválido (400)DIGITAL_SIGN_A1 é um tipo de etapa, não um perfil.

{ "type": ".../errors/bad-request", "title": "Bad Request", "status": 400,
  "detail": "policy.profile must be one of CLICK_ONLY, CLICK_PLUS_OTP, BIOMETRIC, BIOMETRIC_PLUS_OTP, DIGITAL_CERTIFICATE, CUSTOM" }

Correção: use { "policy": { "profile": "CLICK_ONLY" } }.

2. Signatário sem CPF/CNPJ (400) — o signer exige name, userExternalId e pelo menos um entre cpf e cnpj.

{ "title": "Bad Request", "status": 400,
  "detail": "signer must include at least one of: cpf, cnpj" }

Correção: para aceite por pessoa jurídica, envie o cnpj do cliente; para o decisor pessoa física, o cpf (11 dígitos, sem pontuação).

3. Expiração fora do intervalo (400) — tentou expiresInMinutes: 10080 para uma proposta de 7 dias.

{ "title": "Bad Request", "status": 400,
  "detail": "expiresInMinutes must be between 5 and 1440" }

Correção: use até 1440 e reemita a sessão via handler de SIGNING_SESSION.EXPIRED (callout do passo 5).

4. Metadata estourando limites (400) — colocou o JSON inteiro da proposta em um valor de metadata.

{ "title": "Bad Request", "status": 400,
  "detail": "metadata values must be strings of at most 1024 characters" }

Correção: grave apenas identificadores e referências (dealId, crmUrl, versão, valor); o documento em si já viaja em document.

5. Link "sessão inválida" para o cliente — você entregou session.url sem o clientSecret. O link correto é url + "?cs=" + clientSecret. Se o secret se perdeu (é retornado uma única vez), cancele a sessão e crie outra.

Perguntas frequentes

Proposta comercial aceita com um clique tem validade jurídica?

Sim. A MP 2.200-2/2001 (art. 10, §2º) reconhece assinaturas eletrônicas quando as partes admitem o meio como válido — e o aceite de proposta é exatamente uma manifestação de vontade entre partes que negociaram. A SignDocs registra IP, user-agent, timestamps ISO do servidor, hash SHA-256 do documento e a trilha completa de eventos em um pacote de evidências assinado com certificado ICP-Brasil, o que sustenta a prova em caso de disputa.

Como o CRM sabe que a proposta foi aceita?

Pelo webhook SIGNING_SESSION.COMPLETED. O evento traz o transactionId; seu handler consulta GET /v1/transactions/{transactionId}, lê metadata.dealId (que você gravou na criação) e atualiza o deal. Nada de planilha nem conferência manual.

A SignDocs envia o e-mail com a proposta ou eu envio?

Você escolhe. Informando owner na criação (com e-mail diferente do signatário), a SignDocs envia o convite automaticamente e o campo inviteSent da resposta confirma. Sem owner, você entrega o link (url + ?cs= + clientSecret) pelo seu canal — e-mail transacional próprio, SMS ou WhatsApp via sua automação.

O que acontece se a proposta expirar antes de o cliente aceitar?

A sessão vira EXPIRED e o link deixa de funcionar; você recebe o webhook SIGNING_SESSION.EXPIRED. Se a validade comercial da proposta ainda não passou, crie uma nova sessão com o mesmo PDF e a mesma metadata e reenvie — a validade da proposta é regra sua, a sessão é só a janela de aceite ativa (máx. 24h cada).

Posso usar o mesmo fluxo para o contrato definitivo depois do aceite?

Sim — e é o próximo passo natural. Se o contrato exige múltiplos signatários (cliente + representante legal da sua empresa), use um envelope (POST /v1/envelopes) com modo sequencial ou paralelo e baixe o PDF final com o carimbo combinado. Veja Envelopes de assinatura expressa.

Preciso de aprovação interna antes de enviar desconto fora da alçada?

Use uma sessão de confiança: o diretor aprova a ação "conceder 25% de desconto no deal 8842" com clique + OTP, sem PDF, e a evidência fica arquivada. Só depois disso seu sistema dispara a sessão de aceite ao cliente. Guia completo: Sessão de Confiança — Aprovação.

Próximos passos