Como Baixar o Documento Assinado e o Pacote de Evidências via API (e Guardar do Seu Lado)

A assinatura terminou, o webhook chegou e o contrato agora existe em duas formas: o PDF assinado e o pacote de evidências (.p7m) que prova como cada signatário foi autenticado. Este guia mostra como buscar os dois pela API, o que cada URL devolvida significa, por quanto tempo ela vale e, principalmente, como guardar tudo do seu lado, porque o seu arquivo, e não a plataforma, é a cópia primária do que você vai apresentar daqui a cinco anos.

O erro mais comum não é técnico, é de ordem: baixar antes da conclusão, ou guardar o link em vez do arquivo. As duas coisas funcionam no teste e falham em produção, semanas depois, quando alguém precisa do documento. O roteiro abaixo evita as duas.

Se você ainda está no envio, comece por como enviar um documento para assinatura; o fluxo transacional completo mostra onde o download entra na sequência.

Quando baixar: só depois da conclusão

O documento assinado só existe quando a transação chega a COMPLETED. Antes disso, o que a API tem é o original que você enviou e, no máximo, evidências parciais das etapas já cumpridas. O gatilho correto para o download é o evento de conclusão, nunca um temporizador nem o redirecionamento do signatário para o seu returnUrl:

  • Sessão única: SIGNING_SESSION.COMPLETED (a transação correspondente também emite TRANSACTION.COMPLETED).
  • Envelope com vários signatários: ENVELOPE.ALL_SIGNED. Cada signatário gera o seu SIGNING_SESSION.COMPLETED no caminho, mas o PDF consolidado com todas as assinaturas só está pronto no evento do envelope.
  • Sem webhook (polling): GET /v1/transactions/{id} até status ser COMPLETED, com espaçamento razoável. Os limites estão em rate limits e paginação.

O redirect não é prova. O signatário voltar ao seu site com session_id na URL significa apenas que o navegador dele seguiu o returnUrl. Confirme pelo webhook ou pelo status antes de tratar o documento como assinado, e só então baixe.

O que a API devolve

Duas chamadas cobrem tudo. As duas devolvem URLs pré-assinadas, e não os bytes: você recebe um endereço temporário no armazenamento da plataforma e faz um GET simples nele, sem token de API.

Chamada Campo Conteúdo Validade da URL
GET /v1/transactions/{id}/download signedUrl O PDF assinado, com o carimbo visual da plataforma e a assinatura embutida. É o arquivo que você entrega às partes. 1 hora
originalUrl O documento exatamente como você enviou, antes de qualquer assinatura. Útil para conferir o hash de origem.
signatureUrl Assinatura destacada (arquivo .p7s), quando o fluxo a produz. Nem toda transação devolve este campo.
GET /v1/transactions/{id}/evidence downloadUrl O pacote de evidências .p7m: trilha de auditoria, resultados de cada etapa de autenticação, IP, geolocalização, user-agent e timestamps UTC, selado pela plataforma. Temporária; trate como a anterior

As URLs expiram em uma hora. Isso não é um problema se você baixa no momento em que recebe a resposta; vira um problema quando alguém guarda a URL no banco "para baixar depois". A regra é simples: a resposta do /download é um cupom de uma hora, não um endereço permanente. Se expirou, chame o endpoint de novo e receba URLs novas. O que o pacote .p7m contém e como abri-lo está detalhado em evidence packs (.p7m).

Código: do webhook ao arquivo guardado

O exemplo abaixo, em Node.js, é o worker que consome o evento de conclusão da sua fila (o desenho da fila está em webhooks em fila e reprocessamento). Ele busca as URLs, transmite os bytes direto para o seu armazenamento sem carregar o PDF inteiro em memória e confere o hash antes de marcar o contrato como arquivado.

// worker: roda quando SIGNING_SESSION.COMPLETED ou ENVELOPE.ALL_SIGNED entra na fila import { createHash } from 'node:crypto'; import { pipeline } from 'node:stream/promises'; import { createWriteStream } from 'node:fs'; const API = 'https://api.signdocs.com.br'; // homologação: https://api-hml.signdocs.com.br async function api(path, token) { const r = await fetch(API + path, { headers: { Authorization: `Bearer ${token}` } }); if (!r.ok) throw new Error(`${path} -> ${r.status}`); return r.json(); } // baixa uma URL pré-assinada para disco e devolve o SHA-256 do que foi gravado async function fetchToFile(url, dest) { const r = await fetch(url); // sem Authorization: a URL já carrega a permissão if (!r.ok) throw new Error(`download -> ${r.status}`); const hash = createHash('sha256'); const tap = new TransformStream({ transform(chunk, c) { hash.update(chunk); c.enqueue(chunk); } }); await pipeline(r.body.pipeThrough(tap), createWriteStream(dest)); return hash.digest('hex'); } export async function archiveTransaction(transactionId, token, contract) { const tx = await api(`/v1/transactions/${transactionId}`, token); if (tx.status !== 'COMPLETED') return { deferred: true, status: tx.status }; // evento antigo ou fora de ordem const dl = await api(`/v1/transactions/${transactionId}/download`, token); const ev = await api(`/v1/transactions/${transactionId}/evidence`, token); const base = `archive/${contract.id}/${transactionId}`; const signedSha = await fetchToFile(dl.signedUrl, `${base}/assinado.pdf`); const origSha = await fetchToFile(dl.originalUrl, `${base}/original.pdf`); await fetchToFile(ev.downloadUrl, `${base}/evidencias.p7m`); // o hash do original tem de bater com o que você guardou no envio if (origSha !== contract.sha256AtSend) throw new Error('original divergente do enviado'); await db.contracts.update(contract.id, { archivedAt: new Date(), transactionId, signedSha256: signedSha, evidenceId: tx.evidenceId, verifierUrl: `https://verificador.signdocs.com.br/?id=${tx.evidenceId}` }); }

Três detalhes do código que costumam ser esquecidos:

  • O GET na URL pré-assinada não leva o token da API. Ela já carrega a autorização. Mandar Authorization junto pode inclusive fazer o armazenamento rejeitar a requisição.
  • Reconfirme o status antes de baixar. Eventos podem chegar fora de ordem ou ser reprocessados; se a transação não está COMPLETED, devolva o job para mais tarde em vez de arquivar um original como se fosse assinado.
  • Calcule o hash enquanto grava. O SHA-256 do original deve ser idêntico ao que você registrou no envio (a razão de guardá-lo antes está em gerar o PDF antes de enviar). O do PDF assinado é diferente por definição: ele contém a assinatura. Guarde os dois.

O que guardar junto, sempre

Um contrato arquivado é um conjunto, não um arquivo. Se um dos itens abaixo faltar, a prova enfraquece:

Item Para que serve Onde vem
PDF assinado O documento que as partes reconhecem; pode ser validado em leitores de PDF. signedUrl
Original enviado Prova que o conteúdo assinado é o que você gerou, comparando hashes. originalUrl ou a sua própria cópia de envio
Pacote .p7m Trilha de auditoria selada: quem, como, quando, de onde. /evidencedownloadUrl
evidenceId e link do verificador Permite que um terceiro confira a assinatura no verificador público sem falar com você. GET /v1/transactions/{id}
transactionId, sessionId, metadata Reencontrar o contrato no seu sistema e na plataforma. Resposta da criação e do GET
Hashes SHA-256 (original e assinado) Detectar qualquer alteração posterior no seu próprio arquivo. Calculados por você ao gravar

Guarde tudo isso em um armazenamento com versionamento ou imutabilidade (um bucket com object lock, por exemplo) e com o mesmo controle de acesso do contrato em papel. O PDF assinado sem o .p7m ainda tem a assinatura embutida, mas a história de como a identidade do signatário foi verificada está no pacote.

Retenção: o seu arquivo é a cópia primária

A plataforma guarda os documentos por prazos definidos e públicos, e eles são diferentes por ambiente:

  • Homologação (api-hml.signdocs.com.br): tudo é apagado em 7 dias. É um ambiente de teste; não há como recuperar nada depois disso, e o comportamento é o mesmo do sandbox descrito na documentação.
  • Produção: os prazos por tipo de dado estão na Tabela de Retenção pública, que é o documento que vale, e não este artigo.

A consequência prática: desenhe a integração assumindo que você é o arquivo. O download imediato após a conclusão não é otimização, é a única forma de garantir que o documento e as evidências estarão disponíveis pelo tempo que o seu negócio exige, seja por obrigação legal, contratual ou fiscal. Não crie processos que voltam à API meses depois "para pegar o PDF".

URL expirada: como reagir

Se um GET na URL pré-assinada devolve erro de acesso negado ou expirado, a causa quase sempre é o intervalo entre o /download e o uso ter passado de uma hora: um job que ficou preso na fila, uma tentativa de repetição tardia, um link copiado para outro sistema. A correção é chamar /download (ou /evidence) de novo, obter URLs frescas e baixar imediatamente, na mesma execução. Não faça a chamada da URL em um passo e o download em outro.

// padrão: obter e consumir na mesma função; nunca persistir a URL async function withFreshUrls(transactionId, token, fn) { for (let attempt = 0; attempt < 2; attempt++) { const dl = await api(`/v1/transactions/${transactionId}/download`, token); try { return await fn(dl); } catch (e) { if (!/403|expired/i.test(String(e)) || attempt === 1) throw e; } } }

Se, ao contrário, o /download em si devolve 404 ou 409, o problema é outro: a transação não existe mais (retenção vencida, sobretudo em homologação) ou ainda não foi concluída. Os significados de cada código estão em códigos de erro e tratamento de falhas.

Erros comuns

Erro Sintoma Correção
Guardar a URL em vez do arquivo Links "quebrados" no painel interno depois de uma hora. Persistir os bytes; a URL é descartável.
Baixar no redirect do signatário Arquiva o original como se fosse o assinado; às vezes a assinatura ainda estava sendo finalizada. Gatilho = webhook de conclusão + status COMPLETED.
Baixar por sessão em envelopes Várias cópias parciais, nenhuma com todas as assinaturas. Esperar ENVELOPE.ALL_SIGNED e baixar pela transação do envelope.
Misturar originalUrl e signedUrl Cliente recebe o PDF sem assinatura. Nomear arquivos pela origem; conferir que o assinado tem assinatura ao abrir (veja como validar a assinatura no PDF).
Ignorar o .p7m Em disputa, só há o PDF; a trilha de autenticação ficou na plataforma, sujeita à retenção. Baixar e arquivar o pacote junto, sempre.
Mandar Authorization para a URL pré-assinada Erro de acesso mesmo com URL válida. GET simples, sem cabeçalhos de autenticação.

Um teste de ponta a ponta no sandbox que cubra exatamente esse caminho (conclusão, download, hash, arquivo) evita quase todos eles; o roteiro está em testar a integração no sandbox e no CI.

Perguntas Frequentes

Quando posso baixar o documento assinado?

Somente depois que a transação chega a COMPLETED. O gatilho correto é o webhook de conclusão: SIGNING_SESSION.COMPLETED para uma sessão única e ENVELOPE.ALL_SIGNED para envelopes com vários signatários, que é quando o PDF consolidado com todas as assinaturas existe. O redirecionamento do signatário para o seu returnUrl não serve como gatilho: ele só indica que o navegador seguiu o link. Sem webhooks, consulte GET /v1/transactions/{id} com espaçamento até o status ser COMPLETED e só então chame o download.

O que o GET /v1/transactions/{id}/download devolve?

URLs pré-assinadas, não os bytes do arquivo. O campo signedUrl aponta para o PDF assinado, originalUrl para o documento exatamente como você enviou e signatureUrl, quando presente, para a assinatura destacada (.p7s). Você faz um GET simples em cada URL, sem o token da API, porque a autorização já está embutida no endereço. O pacote de evidências vem de outra chamada, GET /v1/transactions/{id}/evidence, no campo downloadUrl.

Por quanto tempo a URL de download vale?

Uma hora. Depois disso o GET na URL devolve erro de acesso. Por isso a URL nunca deve ser guardada no banco: obtenha e consuma na mesma execução, transmitindo os bytes para o seu armazenamento. Se um job ficou preso e a URL venceu, chame /download (ou /evidence) de novo para receber endereços novos e baixe imediatamente. Se o próprio /download devolve 404, a transação não existe mais, o que em homologação acontece após 7 dias.

O que é o pacote de evidências .p7m e por que baixá-lo?

É a trilha de auditoria da transação selada pela plataforma: o resultado de cada etapa de autenticação (clique, OTP, prova de vida, match facial, certificado), IP, geolocalização, user-agent e timestamps UTC de cada ação. O PDF assinado carrega a assinatura, mas a história de como a identidade do signatário foi verificada está no pacote. Em uma contestação, você apresenta os dois. Baixe o .p7m junto com o PDF, no mesmo job, e arquive-os lado a lado com o evidenceId e o link do verificador público.

Por quanto tempo a SignDocs guarda o documento?

Em homologação (api-hml.signdocs.com.br), tudo é apagado em 7 dias, sem recuperação. Em produção, os prazos por tipo de dado estão na Tabela de Retenção pública da SignDocs, que é o documento que vale. A consequência prática é desenhar a integração assumindo que o seu arquivo é a cópia primária: baixe o PDF assinado e o .p7m assim que a conclusão chegar e guarde-os pelo prazo que o seu negócio exige, em vez de voltar à API meses depois para buscar o documento.

Como confiro que o arquivo baixado é o que foi assinado?

Calcule o SHA-256 enquanto grava. O hash do original devolvido pela API deve ser idêntico ao que você registrou no momento do envio; qualquer diferença indica que você está olhando para outra transação ou para um arquivo trocado. O hash do PDF assinado é naturalmente diferente do original, porque contém a assinatura; guarde-o também para detectar alterações posteriores no seu próprio arquivo. Um terceiro pode conferir a assinatura de forma independente no verificador público, informando o evidenceId.

Teste o ciclo completo no sandbox

Envio, assinatura simulada, webhook de conclusão, download do PDF e do .p7m: tudo funciona no ambiente de homologação gratuito, sem cartão. Em produção, o plano é dimensionado com o time comercial.

Criar credenciais de homologação Fale com o time comercial