Documentos grandes na API: upload por URL pré-assinada em vez de base64

O jeito mais simples de mandar um documento para a API é embutir o arquivo em base64 no corpo da requisição — e é o jeito certo até certo tamanho. Passado esse ponto, ele para de funcionar de um jeito particularmente irritante: o teste passou com o contrato de exemplo de 200 KB e a integração quebra em produção, no primeiro cliente que envia um PDF escaneado de 40 páginas. A causa não é o seu código. É que base64 cresce cerca de um terço sobre o binário e o corpo de uma requisição tem teto. A API oferece um segundo caminho para esse caso: pedir uma URL pré-assinada, enviar o arquivo direto ao armazenamento e depois confirmar. Três chamadas em vez de uma, e o arquivo nunca trafega pela API.

Regra prática: abaixo de 5 MB, mande em base64 e siga a vida. A partir daí, use presignPUT direto no armazenamento → confirm. Se o tamanho do documento vem de upload do usuário final e você não controla, implemente o segundo caminho e use-o sempre.

Por que o base64 quebra, e quando

Base64 representa 3 bytes binários com 4 caracteres. A conta é direta: todo arquivo cresce cerca de 33% ao ser codificado. Um PDF de 6 MB vira quase 8 MB de texto no corpo da requisição, antes de qualquer outro campo do JSON.

Isso importa porque o corpo de uma requisição tem limite, e o limite vale para o payload codificado, não para o arquivo original. O resultado é a falha mais frustrante que existe: o desenvolvedor testou com o contrato de exemplo, funcionou, foi para produção — e a primeira pessoa que anexa um PDF escaneado de 40 páginas recebe um erro que não fala de tamanho de arquivo.

Vale notar de onde vem a percepção errada de folga: o arquivo no disco tem 6 MB, o desenvolvedor sabe que o limite é maior que isso, e a conclusão parece segura. O terço que o base64 acrescenta é invisível até você medir o corpo montado.

Os dois caminhos, lado a lado

Base64 inline URL pré-assinada
Chamadas 1 3 (presign, PUT, confirm)
Por onde os bytes passam Corpo da requisição, pela API Direto ao armazenamento, sem tocar a API
Overhead de codificação +33% Nenhum — vai binário
Recomendado para Até 5 MB Acima de 5 MB, ou tamanho imprevisível
Validação do documento Sim Sim, no confirm
Devolve documentHash Sim Sim

As duas últimas linhas são o ponto que costuma preocupar quem avalia a mudança: o caminho pré-assinado não é uma porta dos fundos com menos garantias. A validação é a mesma e o hash resultante é o mesmo. O que muda é exclusivamente por onde os bytes trafegam.

O caminho simples, para referência

// Documento pequeno: uma chamada POST https://api-hml.signdocs.com.br/v1/transactions/{transactionId}/document Authorization: Bearer <access_token> Content-Type: application/json { "content": "JVBERi0xLjQKJcfsj6IK...", // base64 "filename": "contrato.pdf" } // 200 { "transactionId": "tx_01J...", "documentHash": "4b6a...11f0", "documentFormat": "pdf", "status": "DOCUMENT_UPLOADED", "uploadedAt": "2026-09-05T10:14:02Z" }

Um detalhe que gera 400 desnecessário: para qualquer arquivo que não seja PDF, filename com extensão é obrigatório. É por ela que o formato é determinado — não pelo conteúdo dos bytes.

O caminho pré-assinado, passo a passo

1. Pedir a URL

POST https://api-hml.signdocs.com.br/v1/transactions/{transactionId}/document/presign Authorization: Bearer <access_token> Content-Type: application/json { "contentType": "application/pdf", "filename": "contrato-escaneado.pdf" } // 200 { "uploadUrl": "https://...s3...?X-Amz-Signature=...", "uploadToken": "upt_01J...", "s3Key": "tenants/.../contrato-escaneado.pdf", "expiresIn": 600, "contentType": "application/pdf", "instructions": "..." }

Guarde o uploadToken: é ele, e não o s3Key, que fecha o ciclo no passo 3.

2. Enviar o arquivo direto

// PUT do binário na URL devolvida — SEM Authorization PUT <uploadUrl> Content-Type: application/pdf <bytes do arquivo, sem codificar>

Duas armadilhas neste passo, e as duas são silenciosas o bastante para custar uma tarde:

  • Não mande Authorization. A URL pré-assinada carrega a própria autorização nos parâmetros de consulta; acrescentar o cabeçalho do seu token costuma invalidar a assinatura da URL. O reflexo de "toda chamada leva o Bearer" é exatamente o que atrapalha aqui.
  • O Content-Type tem de bater com o que você declarou no presign, caractere por caractere. A assinatura da URL cobre esse cabeçalho: application/pdf no presign e application/octet-stream no PUT são recusados.

3. Confirmar

POST https://api-hml.signdocs.com.br/v1/transactions/{transactionId}/document/confirm Authorization: Bearer <access_token> Content-Type: application/json { "uploadToken": "upt_01J..." } // 200 { "transactionId": "tx_01J...", "status": "DOCUMENT_UPLOADED", "documentHash": "4b6a...11f0", "documentFormat": "pdf" }

O confirm não é burocracia. O PUT foi direto ao armazenamento, então a API ainda não sabe que o arquivo existe. É esta chamada que faz a API ler o objeto, validar o documento, calcular o hash e mover a transação para DOCUMENT_UPLOADED. Pular o passo 3 deixa o arquivo lá e a transação sem documento — um estado que confunde justamente porque o upload "funcionou".

O que pode dar errado

Sintoma Causa provável
PUT recusado pelo armazenamento Content-Type diferente do declarado no presign, ou cabeçalho Authorization indevido
PUT recusado depois de um tempo A URL expirou. Refaça o presign — ele pode ser repetido
confirm devolve 404 ou erro de objeto O PUT não chegou a acontecer, ou foi para outra URL
confirm devolve 422 Documento inválido, corrompido ou protegido por senha — o detalhe vem no corpo
400 no presign ou no upload inline Arquivo não-PDF sem filename com extensão

O 422 merece um comentário. Ele não significa "seu upload falhou" — significa "o arquivo chegou e não serve": PDF corrompido, protegido por senha, ou fora dos critérios mínimos de qualidade. É um erro do documento, não do transporte, e repetir o upload do mesmo arquivo não resolve. Os demais códigos seguem o padrão de erro da API, descrito em códigos de erro e tratamento de falhas.

Uma decisão de implementação que economiza retrabalho

A tentação natural é implementar o base64 primeiro e "depois, se precisar", o pré-assinado. Vale pensar duas vezes quando o documento vem de fora:

  • Se você gera o documento — contrato montado a partir de um modelo, recibo, proposta — o tamanho é previsível e o base64 basta.
  • Se o documento vem de upload do usuário, o tamanho é uma variável que você não controla. Alguém vai anexar um escaneamento em 600 dpi. Implementar os dois caminhos e escolher por tamanho custa umas poucas linhas; descobrir o limite em produção custa um incidente.
// Escolha por tamanho, feita uma vez const LIMITE = 5 * 1024 * 1024; if (arquivo.tamanho < LIMITE) { await enviarBase64(transactionId, arquivo); } else { const { uploadUrl, uploadToken, contentType } = await presign(transactionId, arquivo); await fetch(uploadUrl, { method: 'PUT', body: arquivo.bytes, headers: { 'Content-Type': contentType } }); await confirmar(transactionId, uploadToken); }

Note que os dois ramos terminam no mesmo lugar: transação em DOCUMENT_UPLOADED, com documentHash. Do ponto de vista do resto da integração, o caminho escolhido é invisível — o que significa que trocar de um para o outro depois não obriga a mexer em mais nada.

Depois do upload

Com o documento no lugar, a transação segue o fluxo normal: criar a sessão de assinatura, conduzir as etapas da política e recuperar o resultado. Se você ainda não decidiu qual ponto de entrada usar — transação avulsa, sessão de assinatura ou envelope —, o guia transação, sessão ou envelope: qual usar compara os três. E o passo anterior a tudo isto, obter e usar o token, está em por que o seu token falha.

Para o outro extremo do ciclo — baixar o documento assinado e o pacote de evidências —, veja como baixar o documento assinado e o pacote de evidências.

Perguntas Frequentes

Quando devo usar o upload pré-assinado em vez do base64?

A recomendação da própria API é a partir de 5 MB. Abaixo disso, o base64 inline é mais simples e uma chamada só. Acima, o fluxo pré-assinado evita tanto o crescimento de um terço do base64 quanto o teto do corpo da requisição. Se o tamanho dos seus documentos é imprevisível — qualquer coisa que venha de upload do usuário final — implemente o pré-assinado e use-o sempre.

Quais são as três chamadas?

POST /v1/transactions/{id}/document/presign com contentType e filename devolve uploadUrl, uploadToken, s3Key e expiresIn. Você faz um PUT do arquivo binário direto na uploadUrl. Depois, POST /v1/transactions/{id}/document/confirm com o uploadToken fecha o ciclo e devolve o documentHash.

O PUT na URL pré-assinada leva o meu token de acesso?

Não — e mandar o cabeçalho Authorization nesse PUT costuma quebrar a assinatura da URL. A URL pré-assinada já carrega a própria autorização nos parâmetros de consulta. Envie o corpo binário e o Content-Type exatamente igual ao que você declarou no presign; qualquer divergência é recusada pelo armazenamento.

Por que preciso da terceira chamada, o confirm?

Porque o PUT vai direto ao armazenamento e a API não fica sabendo dele. O confirm é o que faz a API ler o objeto, validar o documento, calcular o documentHash e mover a transação para DOCUMENT_UPLOADED. Sem ele, o arquivo está lá e a transação continua sem documento.

A validação do PDF acontece nos dois caminhos?

Sim. Um PDF inválido, corrompido ou protegido por senha retorna 422 com o detalhe do problema — no document inline e no confirm igualmente. O caminho pré-assinado não é uma porta dos fundos que aceita qualquer arquivo: ele só muda por onde os bytes trafegam.

Que formatos são aceitos?

PDF e uma lista ampla de outros: .docx, .doc, .xlsx, .xls, .pptx, .ppt, .odt, .ods, .odp, .rtf, .txt, .csv, imagens (.jpg, .png, .tiff, .bmp, .svg), .xml, .json e .html. Para qualquer arquivo que não seja PDF, o campo filename com extensão é obrigatório. A resposta informa em documentFormat se o arquivo foi tratado como pdf ou generic.

A URL pré-assinada expira?

Sim, e o prazo vem na resposta em expiresIn, em segundos. É uma janela curta, dimensionada para o upload acontecer logo em seguida — não para guardar a URL e usá-la mais tarde. Se ela expirar antes do PUT, peça outra: o presign pode ser repetido.

Envie documentos de qualquer tamanho sem esbarrar no payload

O fluxo pré-assinado é uma chamada a mais e resolve a classe inteira de falha por tamanho de corpo. Teste os dois caminhos no sandbox gratuito antes de decidir qual usar.

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