Atualizações de Produtos

Novo no Firma.dev: Assine todo o pacote de transações num único pedido, com cada ficheiro selado individualmente

Escrito por

Conteúdo

Um acordo B2B geralmente é fechado com uma pilha de ficheiros: um contrato-quadro de prestação de serviços (MSA), um caderno de encargos (SOW), um acordo de confidencialidade (NDA) e uma nota de encomenda, todos destinados ao mesmo cliente para a mesma assinatura. A partir da API v1.39.0, todo esse pacote pode ser enviado como um único pedido de assinatura da Firma.dev, e cada ficheiro incluído regressa como o seu próprio documento selado.

A assinatura de múltiplos documentos está disponível para todas as contas Firma.dev

Isto é novidade e já está ativo para todos. A versão adiciona:

  • Um modelo ou pedido de assinatura pode conter até 20 documentos, com um limite de 100 MB por conjunto de documentos.

  • POST /templates, POST /signing-requests e POST /signing-requests/create-and-send aceitam document_ids, uma lista de uploads na ordem dos documentos.

  • Novas rotas /documents adicionam, substituem, renomeiam, reordenam, removem e descarregam documentos um a um.

  • As tags de âncora podem ter como alvo um único documento através de document_index.

  • Cada documento de um pedido de assinatura concluído é descarregado como o seu próprio ficheiro individualmente selado, com o seu próprio hash SHA-256.

O registo de alterações data o lançamento de 12 de outubro de 2026, e as funcionalidades de múltiplos documentos estão disponíveis para todas as empresas desde 13 de outubro de 2026. Não há nada para ativar nem qualquer flag a solicitar.

Os preços também não se alteram. Um pedido de assinatura equivale a um crédito, 0,049 € (~5¢ USD), quer contenha um documento ou vinte. As FAQ da documentação afirmam-no diretamente: um crédito equivale a um pedido de assinatura, independentemente de quantos signatários ou documentos contém. Continua a ser em regime pay-as-you-go, sem consumos mínimos mensais nem taxas por utilizador.

O que exigia anteriormente um pacote de quatro documentos

Até este lançamento, um pedido de assinatura continha apenas um documento. Enviar um pacote significava programar soluções alternativas para contornar esse limite, e o processo era habitualmente este:

Passo

O que o seu código tinha de fazer

1. Unir

Combinar o MSA, o SOW, o NDA e a nota de encomenda num único PDF

2. Compensar (offset)

Adicionar o número de páginas de cada ficheiro anterior ao número de página de cada campo

3. Enviar

Criar o pedido de assinatura a partir do PDF unificado

4. Dividir

Separar o PDF selado único por intervalo de páginas

5. Mapear

Associar cada parte ao registo correto na sua aplicação

Os passos 2 e 4 eram onde os problemas surgiam. Um cliente trocava um NDA de duas páginas por um que antes tinha três, e todos os campos seguintes iam parar à página errada. Os intervalos de divisão desfasavam-se da mesma forma... e, para uma plataforma SaaS cujos próprios clientes carregam estes conjuntos, o pacote de cada inquilino (tenant) é ligeiramente diferente, pelo que o erro surgia inquilino a inquilino.

Com a v1.39.0, os passos de união, compensação e divisão desaparecem. Cada ficheiro entra como o seu próprio upload, avança através de um único fluxo de assinatura e regressa selado de forma independente.

Enviar o pacote com document_ids

Carregar cada ficheiro com POST /documents

Cada documento do conjunto começa como um upload. O POST /documents recebe o nome do ficheiro, o tamanho e o tipo de conteúdo, devolvendo um document_id temporário e um upload_url pré-assinado. O utilizador faz o PUT dos bytes em bruto do ficheiro para esse URL. É um upload por ficheiro... logo, um pacote de quatro documentos implica quatro uploads antes mesmo de o pedido de assinatura existir.

const API = "https://api.firma.dev/functions/v1/signing-request-api";
const headers = {
  Authorization: process.env.FIRMA_API_KEY,
  "Content-Type": "application/json",
};

async function upload(fileName, bytes) {
  const res = await fetch(`${API}/documents`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      file_name: fileName,
      file_size: bytes.length,
      content_type: "application/pdf",
    }),
  });
  const { document_id, upload_url } = await res.json();

  await fetch(upload_url, {
    method: "PUT",
    headers: { "Content-Type": "application/pdf" },
    body: bytes,
  });
  return document_id;
}
const API = "https://api.firma.dev/functions/v1/signing-request-api";
const headers = {
  Authorization: process.env.FIRMA_API_KEY,
  "Content-Type": "application/json",
};

async function upload(fileName, bytes) {
  const res = await fetch(`${API}/documents`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      file_name: fileName,
      file_size: bytes.length,
      content_type: "application/pdf",
    }),
  });
  const { document_id, upload_url } = await res.json();

  await fetch(upload_url, {
    method: "PUT",
    headers: { "Content-Type": "application/pdf" },
    body: bytes,
  });
  return document_id;
}
const API = "https://api.firma.dev/functions/v1/signing-request-api";
const headers = {
  Authorization: process.env.FIRMA_API_KEY,
  "Content-Type": "application/json",
};

async function upload(fileName, bytes) {
  const res = await fetch(`${API}/documents`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      file_name: fileName,
      file_size: bytes.length,
      content_type: "application/pdf",
    }),
  });
  const { document_id, upload_url } = await res.json();

  await fetch(upload_url, {
    method: "PUT",
    headers: { "Content-Type": "application/pdf" },
    body: bytes,
  });
  return document_id;
}

Este método é também o indicado para ficheiros grandes. Desde a v1.40.0, qualquer corpo de pedido superior a 6 MiB devolve 413 REQUEST_TOO_LARGE, pelo que PDFs grandes devem usar sempre o POST /documents. Um único upload pode ter até 50 MB (o conjunto completo tem um limite máximo de 100 MB).

Criar e enviar na ordem dos documentos

Passe entre 1 e 20 IDs de upload em document_ids, na ordem em que os documentos devem surgir. POST /signing-requests/create-and-send aceita-os. O mesmo acontece com POST /signing-requests, para um rascunho que pretenda enviar mais tarde, e com POST /templates.

Cada criação aceita exatamente uma fonte de documento: document, document_id, document_ids ou template_id. Se enviar mais do que uma, a API devolve 400 CONFLICTING_DOCUMENT_SOURCE. Um ID repetido devolve 400 DUPLICATE_DOCUMENT_ID, mais de 20 devolve 400 MAX_DOCUMENTS_EXCEEDED, e um conjunto acima de 100 MB devolve 413 DOCUMENT_SET_TOO_LARGE.

Apontar as tags de âncora ao documento correto

As tags de âncora agora aceitam document_index, a posição de base 1 do documento de destino em document_ids. Com apenas um documento, este campo é opcional e assume o valor 1 por predefinição. Com mais de um documento, é obrigatório, e omiti-lo devolve 400 ANCHOR_DOCUMENT_REQUIRED. Ao utilizá-lo, o {{SIGN_HERE}} no NDA mantém-se separado do {{SIGN_HERE}} no SOW, mesmo quando todos os ficheiros do pacote utilizam o mesmo marcador.

const packet = [
  { name: "msa.pdf", bytes: msa },
  { name: "sow.pdf", bytes: sow },
  { name: "nda.pdf", bytes: nda },
  { name: "order-form.pdf", bytes: orderForm },
];
const documentIds = await Promise.all(packet.map((d) => upload(d.name, d.bytes)));

const res = await fetch(`${API}/signing-requests/create-and-send`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    name: "Acme Corp - Q4 deal packet",
    document_ids: documentIds, // MSA, SOW, NDA, order form
    recipients: [
      {
        id: "temp_1",
        first_name: "Dana",
        last_name: "Reyes",
        email: "dana@acme.example",
        designation: "Signer",
        order: 1,
      },
    ],
    anchor_tags: [
      { anchor_string: "{{SIGN_HERE}}", type: "signature", recipient_id: "temp_1", document_index: 1 },
      { anchor_string: "{{SIGN_HERE}}", type: "signature", recipient_id: "temp_1", document_index: 2 },
      { anchor_string: "{{SIGN_HERE}}", type: "signature", recipient_id: "temp_1", document_index: 3 },
      { anchor_string: "{{SIGN_HERE}}", type: "signature", recipient_id: "temp_1", document_index: 4 },
      { anchor_string: "{{DATE}}", type: "date", recipient_id: "temp_1", document_index: 4 },
    ],
  }),
});
const signingRequest = await res.json(); // signingRequest.id
const packet = [
  { name: "msa.pdf", bytes: msa },
  { name: "sow.pdf", bytes: sow },
  { name: "nda.pdf", bytes: nda },
  { name: "order-form.pdf", bytes: orderForm },
];
const documentIds = await Promise.all(packet.map((d) => upload(d.name, d.bytes)));

const res = await fetch(`${API}/signing-requests/create-and-send`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    name: "Acme Corp - Q4 deal packet",
    document_ids: documentIds, // MSA, SOW, NDA, order form
    recipients: [
      {
        id: "temp_1",
        first_name: "Dana",
        last_name: "Reyes",
        email: "dana@acme.example",
        designation: "Signer",
        order: 1,
      },
    ],
    anchor_tags: [
      { anchor_string: "{{SIGN_HERE}}", type: "signature", recipient_id: "temp_1", document_index: 1 },
      { anchor_string: "{{SIGN_HERE}}", type: "signature", recipient_id: "temp_1", document_index: 2 },
      { anchor_string: "{{SIGN_HERE}}", type: "signature", recipient_id: "temp_1", document_index: 3 },
      { anchor_string: "{{SIGN_HERE}}", type: "signature", recipient_id: "temp_1", document_index: 4 },
      { anchor_string: "{{DATE}}", type: "date", recipient_id: "temp_1", document_index: 4 },
    ],
  }),
});
const signingRequest = await res.json(); // signingRequest.id
const packet = [
  { name: "msa.pdf", bytes: msa },
  { name: "sow.pdf", bytes: sow },
  { name: "nda.pdf", bytes: nda },
  { name: "order-form.pdf", bytes: orderForm },
];
const documentIds = await Promise.all(packet.map((d) => upload(d.name, d.bytes)));

const res = await fetch(`${API}/signing-requests/create-and-send`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    name: "Acme Corp - Q4 deal packet",
    document_ids: documentIds, // MSA, SOW, NDA, order form
    recipients: [
      {
        id: "temp_1",
        first_name: "Dana",
        last_name: "Reyes",
        email: "dana@acme.example",
        designation: "Signer",
        order: 1,
      },
    ],
    anchor_tags: [
      { anchor_string: "{{SIGN_HERE}}", type: "signature", recipient_id: "temp_1", document_index: 1 },
      { anchor_string: "{{SIGN_HERE}}", type: "signature", recipient_id: "temp_1", document_index: 2 },
      { anchor_string: "{{SIGN_HERE}}", type: "signature", recipient_id: "temp_1", document_index: 3 },
      { anchor_string: "{{SIGN_HERE}}", type: "signature", recipient_id: "temp_1", document_index: 4 },
      { anchor_string: "{{DATE}}", type: "date", recipient_id: "temp_1", document_index: 4 },
    ],
  }),
});
const signingRequest = await res.json(); // signingRequest.id

Se ainda não domina as tags de âncora, o guia passo a passo de tags de âncora aborda compensações (offsets), ocorrências e dimensionamento.

Cada documento regressa selado de forma independente

GET /signing-requests/{id} devolve um array de documents. Cada entrada contém document_id, position, label, original_filename e page_count, e nos pedidos de assinatura inclui também document_only_hash, o SHA-256 do documento individualmente selado após a vinculação do certificado (null antes disso).

Para obter um ficheiro, chame GET /signing-requests/{id}/documents/{document_id}/download. Este devolve um URL assinado válido por 1 hora:

  • type=original (a predefinição) devolve o documento tal como foi carregado.

  • type=document devolve o documento selado individualmente após a vinculação do certificado. Antes desse momento, devolve 404 DOCUMENT_SIGNED_NOT_AVAILABLE, pelo que deve tratar este código como "ainda não disponível" e tentar novamente um pouco mais tarde.

Para uma aplicação SaaS, isto elimina a última parte do código de integração intermediário. O ciclo abaixo grava o MSA assinado no registo de contrato, o NDA assinado no registo de NDA e assim sucessivamente, com o hash de cada ficheiro guardado junto a este. E dado que a position segue a ordem enviada em document_ids, mapeia diretamente para o pacote que criou.

async function storeSignedPacket(signingRequestId, packetRecords) {
  const sr = await fetch(`${API}/signing-requests/${signingRequestId}`, { headers })
    .then((r) => r.json());

  for (const doc of sr.documents) {
    const res = await fetch(
      `${API}/signing-requests/${signingRequestId}/documents/${doc.document_id}/download?type=document`,
      { headers }
    );
    const body = await res.json();

    if (body.code === "DOCUMENT_SIGNED_NOT_AVAILABLE") {
      return scheduleRetry(signingRequestId); // certificate not bound yet
    }
    if (!res.ok) throw new Error(`${body.code}: ${body.error}`);

    const pdf = Buffer.from(await (await fetch(body.download_url)).arrayBuffer());

    await records.saveSignedFile({
      record: packetRecords[doc.position - 1], // contract, SOW, NDA, order form
      file: pdf,
      sha256: doc.document_only_hash,
    });
  }
}
async function storeSignedPacket(signingRequestId, packetRecords) {
  const sr = await fetch(`${API}/signing-requests/${signingRequestId}`, { headers })
    .then((r) => r.json());

  for (const doc of sr.documents) {
    const res = await fetch(
      `${API}/signing-requests/${signingRequestId}/documents/${doc.document_id}/download?type=document`,
      { headers }
    );
    const body = await res.json();

    if (body.code === "DOCUMENT_SIGNED_NOT_AVAILABLE") {
      return scheduleRetry(signingRequestId); // certificate not bound yet
    }
    if (!res.ok) throw new Error(`${body.code}: ${body.error}`);

    const pdf = Buffer.from(await (await fetch(body.download_url)).arrayBuffer());

    await records.saveSignedFile({
      record: packetRecords[doc.position - 1], // contract, SOW, NDA, order form
      file: pdf,
      sha256: doc.document_only_hash,
    });
  }
}
async function storeSignedPacket(signingRequestId, packetRecords) {
  const sr = await fetch(`${API}/signing-requests/${signingRequestId}`, { headers })
    .then((r) => r.json());

  for (const doc of sr.documents) {
    const res = await fetch(
      `${API}/signing-requests/${signingRequestId}/documents/${doc.document_id}/download?type=document`,
      { headers }
    );
    const body = await res.json();

    if (body.code === "DOCUMENT_SIGNED_NOT_AVAILABLE") {
      return scheduleRetry(signingRequestId); // certificate not bound yet
    }
    if (!res.ok) throw new Error(`${body.code}: ${body.error}`);

    const pdf = Buffer.from(await (await fetch(body.download_url)).arrayBuffer());

    await records.saveSignedFile({
      record: packetRecords[doc.position - 1], // contract, SOW, NDA, order form
      file: pdf,
      sha256: doc.document_only_hash,
    });
  }
}

Se pretender também as transferências ao nível do pedido (o PDF apenas com o documento e o certificado em separado), estas são abordadas no artigo sobre downloads de PDFs divididos. Cada pedido de assinatura mantém também o seu registo de auditoria.

Modelos que contêm um conjunto de documentos

Um modelo também pode conter todo o pacote, o que é ideal para plataformas que enviam o mesmo conjunto a cada novo cliente. Crie-o com document_ids no POST /templates e, em seguida, faça a gestão de cada ficheiro em /templates/{id}/documents:

  • POST /templates/{id}/documents adiciona um documento na posição especificada por position, ou no final se omitir este campo.

  • PATCH /templates/{id}/documents/{document_id} substitui o documento por um novo upload (document_id) ou altera o seu nome (label). Envie exatamente uma das duas opções por chamada.

  • PUT /templates/{id}/documents/order reordena o conjunto. A lista deve indicar cada documento exatamente uma vez, caso contrário devolve 400 INVALID_DOCUMENT_ORDER.

  • DELETE /templates/{id}/documents/{document_id} remove um documento. O último documento restante não pode ser removido.

Uma substituição mantém os campos no sítio desde que o novo ficheiro tenha o mesmo número de páginas e cada página tenha o mesmo tamanho com uma margem de 1 pt. Caso contrário, receberá 400 PAGE_COUNT_MISMATCH ou 400 DIMENSION_MISMATCH. Assim, quando o departamento jurídico atualiza o NDA, basta substituir esse ficheiro... e os campos no MSA, no SOW e na nota de encomenda mantêm-se exatamente onde estavam. A página da funcionalidade de substituição de documentos em modelos aborda este processo com mais detalhe.

Existe também uma proteção de concorrência, que se torna essencial em ambientes multi-tenant (dois serviços, ou dois administradores, a editar o mesmo modelo). O valor de documents_layout_version aumenta sempre que documentos são adicionados, removidos ou reordenados. As gravações de campos através de PUT e PATCH em /templates/{id} e /signing-requests/{id} enviam o valor obtido na sua última leitura. É obrigatório assim que existir mais do que um documento ou quando a versão do layout for superior a 1 (retornando 409 DOCUMENTS_LAYOUT_VERSION_REQUIRED caso esteja ausente), e um valor desatualizado devolve 409 DOCUMENT_SET_VERSION_CONFLICT. Se o administrador de um inquilino reordenar o pacote na sua interface de utilizador enquanto um processo em segundo plano estiver a mover um campo, a segunda gravação falha com o código 409 e a alteração de ninguém é silenciosamente substituída.

async function moveField(templateId, fieldId, x, y, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    const tpl = await fetch(`${API}/templates/${templateId}`, { headers })
      .then((r) => r.json());

    const res = await fetch(`${API}/templates/${templateId}`, {
      method: "PATCH",
      headers,
      body: JSON.stringify({
        field: { id: fieldId, x, y },
        documents_layout_version: tpl.documents_layout_version,
      }),
    });
    if (res.ok) return res.json();

    const err = await res.json();
    if (err.code !== "DOCUMENT_SET_VERSION_CONFLICT") {
      throw new Error(`${err.code}: ${err.error}`);
    }
    // The document set changed between our read and our write. Reload and retry.
  }
  throw new Error("Document set kept changing; giving up");
}
async function moveField(templateId, fieldId, x, y, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    const tpl = await fetch(`${API}/templates/${templateId}`, { headers })
      .then((r) => r.json());

    const res = await fetch(`${API}/templates/${templateId}`, {
      method: "PATCH",
      headers,
      body: JSON.stringify({
        field: { id: fieldId, x, y },
        documents_layout_version: tpl.documents_layout_version,
      }),
    });
    if (res.ok) return res.json();

    const err = await res.json();
    if (err.code !== "DOCUMENT_SET_VERSION_CONFLICT") {
      throw new Error(`${err.code}: ${err.error}`);
    }
    // The document set changed between our read and our write. Reload and retry.
  }
  throw new Error("Document set kept changing; giving up");
}
async function moveField(templateId, fieldId, x, y, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    const tpl = await fetch(`${API}/templates/${templateId}`, { headers })
      .then((r) => r.json());

    const res = await fetch(`${API}/templates/${templateId}`, {
      method: "PATCH",
      headers,
      body: JSON.stringify({
        field: { id: fieldId, x, y },
        documents_layout_version: tpl.documents_layout_version,
      }),
    });
    if (res.ok) return res.json();

    const err = await res.json();
    if (err.code !== "DOCUMENT_SET_VERSION_CONFLICT") {
      throw new Error(`${err.code}: ${err.error}`);
    }
    // The document set changed between our read and our write. Reload and retry.
  }
  throw new Error("Document set kept changing; giving up");
}

Os pedidos de assinatura utilizam as mesmas rotas /signing-requests/{id}/documents enquanto mantêm o estado de rascunho. Uma vez que o pedido seja enviado, concluído, cancelado ou recusado, qualquer alteração a documentos devolve 409 DOCUMENT_SET_LOCKED.

O que muda numa integração existente

A versão é cumulativa e retrocompatível. Os pedidos que nunca utilizem document_ids, document_index ou as rotas /documents comportam-se exatamente como antes. Há três aspetos que vale a pena verificar:

  1. Clientes gerados. Se gerar um cliente a partir da especificação OpenAPI, document passa a ser opcional em POST /templates, e o esquema do participante do selo nas respostas de pedidos de assinatura chama-se agora SealParticipantApi (anteriormente era SealParticipant).

  2. Validação estrita de erros. Se o seu código rejeitar códigos de erro desconhecidos, adicione os novos. O esquema MultiDocumentErrorCode na referência da API lista-os a todos.

  3. Cópias de modelos. POST /templates/{id}/copy converte um modelo multidocumento num modelo com um único documento unido. O conteúdo dos campos e os números de página absolutos são preservados. As etiquetas e as posições de cada documento individual são descartadas, o que deve ser considerado caso copie modelos para espaços de trabalho de clientes.

A nível legal nada se altera. A Firma.dev está em conformidade com o ESIGN Act, UETA, eIDAS (SES e AES) e UK eIDAS, e as assinaturas têm valor jurídico vinculativo em mais de 55 países, para todos os documentos do conjunto.

Resumo

A assinatura de múltiplos documentos já está disponível na API da Firma.dev para todas as contas. Carregue cada ficheiro com POST /documents, passe até 20 IDs em document_ids, posicione tags de âncora com document_index e transfira cada documento individualmente selado com o seu respetivo document_only_hash. Os modelos podem conter os mesmos conjuntos e ser atualizados ficheiro a ficheiro, com documents_layout_version a proteger contra edições concorrentes. Todo o pacote custa um crédito, 0,049 € (~5¢ USD). Todos os detalhes encontram-se no registo de alterações da API.

Comece a utilizar a Firma.dev gratuitamente, sem necessidade de cartão de crédito.

  1. Cabeçalho

Imagem de Fundo

Pronto para adicionar assinaturas eletrónicas à sua aplicação?

Comece gratuitamente. Não é necessário cartão de crédito. Pague apenas 0,049 € por envelope quando estiver pronto para começar.

Imagem de Fundo

Pronto para adicionar assinaturas eletrónicas à sua aplicação?

Comece gratuitamente. Não é necessário cartão de crédito. Pague apenas 0,049 € por envelope quando estiver pronto para começar.

Imagem de Fundo

Pronto para adicionar assinaturas eletrónicas à sua aplicação?

Comece gratuitamente. Não é necessário cartão de crédito. Pague apenas 0,049 € por envelope quando estiver pronto para começar.