Referência da API

API de assinatura eletrônica

Envie contratos para assinatura direto do seu sistema. Autenticação por chave, uma chamada para criar e enviar, e o status do documento sempre que você perguntar.

Visão geral

Envie contratos para assinatura a partir do seu sistema, usando a conta que você já tem.

A API serve para o caso em que o documento nasce no seu sistema: um pedido aprovado, um cadastro concluído, uma proposta aceita. Você manda o PDF e a lista de quem assina; a plataforma cria o envelope, dispara os convites por e-mail e devolve o status a cada consulta.

Todas as rotas ficam sob /v1, respondem JSON e exigem uma chave de API. A versão está no caminho desde a primeira rota: contrato público quebra o sistema do outro lado, que não atualiza quando nós atualizamos.

A API não substitui a plataforma

Posicionar campos no PDF, montar modelos, gerir membros e configurar a marca continuam sendo feitos na tela. A API cobre o que um sistema precisa fazer sozinho: criar, acompanhar, cancelar e baixar.
Uma chamada, do PDF ao convite
curl -X POST https://apisign.bitsai.app/v1/envelopes \
  -H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI" \
  -H "Idempotency-Key: pedido-48219" \
  -F "file=@contrato.pdf;type=application/pdf" \
  -F "title=Contrato de prestação de serviços" \
  -F 'signers=[{"name":"Maria Silva","email":"maria@exemplo.com.br"}]'

Endereço base

O prefixo de toda rota desta referência.

Todos os exemplos desta página já usam este endereço. Basta trocar o token pelo seu.

Todas as chamadas são feitas sobre HTTPS. Requisição em HTTP puro falha, e não por rigor nosso: uma chave que trafegou sem TLS passou em texto claro por toda a rede no caminho, e deve ser considerada vazada.

HTTP
https://apisign.bitsai.app/v1

Autenticação

Cabeçalho Authorization, esquema Bearer, como no resto do mercado.

Toda requisição leva a chave no cabeçalho Authorization. Requisição sem chave, ou com chave inválida, recebe 401 e não chega a tocar em nada da conta.

GET/v1/me

Confirma que o token funciona e devolve o que ele pode fazer. Útil para validar a configuração antes de subir a integração — é a primeira chamada que vale a pena fazer depois de criar uma chave.

A chave é secreta

O prefixo sk_ significa secret key: ela vale apenas no seu servidor. Nunca a coloque em aplicativo, página, app móvel ou qualquer lugar que chegue ao navegador do usuário final — quem tiver o token cria e cancela contratos em nome da sua conta.
Requisição
curl https://apisign.bitsai.app/v1/me \
  -H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI"
Resposta
{
  "workspace_id": "ee122b69-…",
  "environment": "live",
  "scopes": ["envelopes:read", "envelopes:write", "documents:read"]
}

Criar uma chave

A credencial que autentica o seu sistema nasce na plataforma.

As chaves são criadas na plataforma, nunca pela API — uma chave não pode emitir outra. Se pudesse, um vazamento pontual viraria acesso permanente e auto-renovável.

O token aparece uma única vez

Guardamos apenas um resumo criptográfico dele, não o valor. Se o token se perder, o caminho é revogar a chave e criar outra — não há como recuperá-lo. Guarde-o no gerenciador de segredos do seu sistema, nunca no repositório de código.
PassoOnde
1. Entre na sua contaÁrea logada da plataforma
2. Abra Desenvolvedores › Chaves de APIMenu lateral
3. Clique em Criar chaveBotão no topo da lista
4. Dê um nome e escolha os escoposO nome identifica a integração na trilha de auditoria
5. Copie o tokenEle aparece UMA vez. Depois disso, nem nós conseguimos exibi-lo

Quem pode criar

Mais restrito que as outras telas administrativas, de propósito.

O proprietário da conta sempre pode. Qualquer outra pessoa precisa da permissão Chaves de API no perfil de acesso dela. É deliberadamente mais restrito que as outras telas administrativas: uma chave age em nome da conta inteira, por tempo indeterminado e sem segundo fator.

Criar chave também exige que o plano da conta inclua acesso à API. A tela abre sem o recurso para você conseguir consultá-lo, mas a criação é recusada.

Escopos

Cada rota declara o que exige. Conceda o mínimo.

Uma integração que só consulta status não deveria conseguir disparar e-mail em nome da conta — e é justamente esse tipo de chave que costuma acabar num repositório público.

A rota recusa com 403 e insufficient_scope quando falta escopo, e a mensagem diz qual está faltando.

EscopoPermite
envelopes:readListar envelopes e consultar o status de cada signatário
envelopes:writeCriar envelopes, disparar convites e cancelar
documents:readBaixar o PDF assinado, o certificado e a trilha de auditoria
webhooks:readListar os webhooks da chave e consultar a saúde de cada endpoint
webhooks:writeCadastrar, alterar, pausar, rotacionar o segredo e revogar webhooks

Revogar uma chave

Efeito imediato, sem afetar o que ela já criou.

Na mesma tela, o botão Revogar ao lado da chave. O efeito é imediato: a próxima requisição com aquele token recebe 401. A chave continua listada como revogada, porque a trilha de auditoria precisa saber que ela existiu e o que fez.

Os envelopes criados por uma chave revogada não são afetados: continuam válidos, assináveis e consultáveis pela plataforma.

Uma conta pode ter até 25 chaves ativas ao mesmo tempo. Revogue as que não estiverem em uso — cada chave viva é uma porta a mais.

Idempotência

O que impede que um timeout de rede vire dois contratos na caixa de entrada do seu cliente.

Toda rota de escrita aceita — e a criação de envelope exige — o cabeçalho Idempotency-Key.

Use um valor único por operação (um UUID, um ULID ou o id do pedido no seu sistema), entre 8 e 120 caracteres, com letras, números, hífen e underscore. Repetir a mesma chave devolve o mesmo envelope, sem criar nem reenviar nada.

As chaves são isoladas por chave de API: se duas integrações da mesma conta usarem pedido-48219, cada uma recebe o próprio envelope. Uma nunca enxerga o da outra.

Se a primeira tentativa morreu no meio — a conexão caiu depois de o envelope nascer, mas antes de sair —, repetir com a mesma chave retoma de onde parou: o documento, os signatários e os campos são reescritos com o que veio na repetição, e o envio acontece. Mande o mesmo corpo da primeira vez; um corpo diferente na retomada produz um envelope com o conteúdo novo.

Depois de o envelope ter saído, nada é sobrescrito: a repetição devolve o que existe, intacto.

HTTP
Idempotency-Key: pedido-48219

Limite de requisições

Um teto por chave, e outro por endereço de origem antes da autenticação.

Cada chave tem um teto próprio, exibido na tela de Chaves de API. O padrão é 10 requisições por segundo; ao ultrapassar, a chave fica bloqueada por 60 segundos e as requisições recebem 429.

A resposta traz o cabeçalho Retry-After em segundos. Respeite-o: repetir antes disso apenas prolonga o bloqueio.

Existe também um teto por endereço de origem, aplicado antes da autenticação, folgado o suficiente para nunca alcançar quem usa a API normalmente. Ele existe para conter enxurrada de requisições sem chave válida. Se você concentra várias integrações atrás de um mesmo IP de saída e vê 429 antes do teto da chave, fale com o suporte.

JSON
{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded",
    "message": "API rate limit exceeded",
    "meta": { "retry_after_seconds": 60, "limit_per_second": 10 }
  }
}

Campos desconhecidos

Recusados com 400, nunca ignorados em silêncio.

Qualquer campo que não esteja documentado devolve 400 apontando o nome dele. Um titlle ignorado em silêncio faria você depurar por horas um envelope que saiu diferente do que mandou.

Vale também para os campos que o servidor decide sozinho: workspace_id, api_key_id, origin e created_by_user_id no corpo são recusados, e não silenciosamente descartados.

Identificador de requisição

O que liga a sua chamada ao nosso registro dela.

Toda resposta de /v1 traz o cabeçalho x-request-id, no formato req_…. Guarde-o nos seus próprios logs.

Ao abrir um chamado, mande esse valor em vez de descrever o horário — ele encontra a requisição exata, com o status, a duração e o motivo da recusa. Quem administra a conta vê a mesma linha na tela Logs de integração do painel, onde o histórico dos últimos 30 dias fica disponível com filtros por data, rota e resultado.

O que aparece nesse registro

Método, rota, código de status, duração, IP de origem, o code do erro e o identificador do recurso afetado. O corpo da requisição e o da resposta não são guardados — o que você envia é contrato do seu cliente, e log é o dado mais lido por mais gente quando alguém está depurando.

Requisição com chave inválida não é registrada

Um 401 por token inválido ou ausente não gera linha, porque sem chave resolvida não há como saber de qual conta ele é — e nenhum registro existe sem conta. O cabeçalho x-request-idvem de qualquer jeito, mas se você está depurando "minha chave não funciona", a tentativa não estará na tela de logs. Um 403 de escopo, sim: ali a chave é válida.
HTTP
x-request-id: req_8f2c41d9a7

Erros

Todo erro sai no mesmo formato, em todas as rotas.

type diz o que fazer; code é estável e serve para um switch; message é para o humano que está depurando — não a use em comparações.

typeHTTPO que fazer
invalid_request_error400, 413, 415, 422Corrigir o payload. Repetir sem mudar nada dá o mesmo erro
authentication_error401Conferir o token. Pode ter sido revogado, expirado ou a conta perdeu o plano
permission_error403A chave é válida mas o ato não é permitido — falta escopo, ou o envelope restringe aquele download
billing_error402Cota ou plano. Não há campo errado no payload: o que falta é contrato
not_found_error404O recurso não existe ou não pertence a esta chave
conflict_error409O estado atual não permite a operação — por exemplo, cancelar algo já concluído
rate_limit_error429Esperar o tempo de Retry-After
api_error500Falha nossa. Repita com a mesma Idempotency-Key; se persistir, fale com o suporte
Recurso de outra chave responde 404, e não 403. É de propósito: um 403 confirmaria que aquele identificador existe em algum lugar da conta.
JSON
{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_request",
    "message": "\"signers[0].email\" must be a valid email",
    "param": "signers.0.email"
  }
}

Códigos de erro

A lista completa do que error.code pode conter.

Trate qualquer código desconhecido como falha genérica: a lista cresce quando uma regra nova entra, e um switch sem ramo padrão quebraria nesse dia.

codeHTTPQuando
invalid_api_key401Token ausente, inválido, revogado, expirado, ou a conta perdeu o plano de API
insufficient_scope403A chave não tem o escopo que a rota exige. A mensagem diz qual
rate_limit_exceeded429Passou do teto da chave. Veja Retry-After
resource_not_found404O envelope não existe ou não foi criado por esta chave
unknown_endpoint404O caminho não existe. Confira a rota e o método
invalid_request400Campo faltando, com formato errado ou desconhecido. Veja param
duplicate_signer_email400O mesmo e-mail aparece duas vezes em signers
document_too_large413O PDF passou de 10 MB, ou do limite do plano da conta
invalid_document_type415O arquivo não é PDF — inclusive quando o MIME declarado mente
invalid_pdf422PDF corrompido ou truncado; não foi possível abrir
encrypted_pdf422PDF protegido por senha. Remova a proteção antes de enviar
invalid_pdf_page_count422Mais de 200 páginas
invalid_field_geometry422Posição de campo inválida: página inexistente, campo que termina fora da página, ou — no modo automático — signatários demais para as páginas do documento. O meta traz o reason
field_signer_not_found422O signer_email de um campo não está em signers. O meta traz o e-mail recusado
pre_signed_field_not_allowed422Veio um campo com pre_signed: true num envelope sem pre_signed
signer_without_signature_field422Você mandou fields e alguém ficou sem campo de assinatura. O meta lista os e-mails
envelope_quota_exceeded402A cota de envelopes do plano acabou no ciclo atual
pre_signed_*422Quatro variações da pré-assinatura — ver Regras de negócio
invalid_envelope_state409O estado atual não permite a operação, como cancelar algo concluído
document_not_found404O artefato pedido ainda não existe. O meta traz artifact_not_ready
original_download_disabled403Quem enviou desativou o download do original neste envelope. Não é problema de escopo
internal_error500Falha nossa. Repita com a mesma Idempotency-Key

Status de sucesso

Qual código cada rota devolve quando dá certo.

A repetição idempotente devolve 201 igual à primeira chamada. O código descreve o RESULTADO — o envelope existe e foi criado por esta requisição lógica —, não quantas vezes a rede tentou.

RotaHTTP
POST /v1/envelopes201 — inclusive na repetição idempotente: o status conta o resultado, não o caminho
Todas as outras200

Paginação

Cursor, não número de página.

Listagens usam cursor, não número de página. A lista cresce pela ponta, e paginar por deslocamento faria linhas pularem ou se repetirem entre uma página e a seguinte.

Enquanto has_more for true, mande o next_cursor recebido no parâmetro cursor da próxima chamada.

cURL
curl "https://apisign.bitsai.app/v1/envelopes?limit=50&cursor=eyJpZCI6…" -H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI"

Criar um envelope

Cria e envia numa única chamada.

POST/v1/envelopesescopo: envelopes:write

Cria e envia o envelope numa única chamada. O corpo é multipart/form-data, porque o PDF vai junto — base64 dentro de JSON infla o arquivo em 33% e obriga os dois lados a manter o documento inteiro em memória como texto.

O cabeçalho Idempotency-Key é obrigatório aqui.

CampoTipoRegra
filearquivoObrigatório. PDF, até 10 MB e 200 páginas. O plano da conta pode impor um limite menor
titletextoObrigatório. De 2 a 255 caracteres
signersJSONObrigatório. Array com 1 a 50 objetos. name (até 200) e email (até 320) obrigatórios; phone (só os dígitos são guardados), organization_name e role_name opcionais
messagetextoOpcional. Até 2000 caracteres. Aparece no e-mail de convite
signing_ordertextoOpcional e só aceita parallel, que é o padrão. Ver abaixo
expires_atISO 8601Opcional, precisa ser futuro. Sem ele vale o prazo padrão da conta
require_cpfbooleanoOpcional. A política da conta tem a última palavra
pre_signedbooleanoOpcional. Inclui o titular da chave como parte já assinada — ver Regras de negócio
fieldsJSONOpcional. Posição de cada campo no documento. Sem ele, a API posiciona sozinha — ver Posicionar campos

Resposta 201. O envelope já saiu e todos os signatários receberam o convite por e-mail — a API só cria envelope em parallel, então não há fila.

Os links vêm na resposta

Cada signatário traz signing_url: é o MESMO endereço que foi para o e-mail dele. Serve para quem entrega por outro canal — o WhatsApp do cliente, o próprio aplicativo —, sem precisar esperar que a pessoa abra a caixa de entrada.

Guarde no momento em que chega, ou não guarde

O banco guarda apenas o resumo criptográfico do token, nunca o valor. Isso quer dizer que signing_url só existe nesta resposta e na do reenvio: em GET /v1/envelopes/{envelope_id} ela é sempre null, e nem nós conseguimos reconstruí-la. Se a rede cair antes de você ler a resposta — ou se o retry idempotente devolver o envelope já enviado, que vem sem os links —, o caminho é Reenviar o convite.

O link é credencial ao portador

Quem tiver o endereço abre o documento e baixa os artefatos daquele signatário. Não permite assinar: isso exige o código que enviamos para o e-mail dele. Ainda assim, trate-o como trata a sua chave — fora de log, fora do navegador, e entregue só a quem assina.
Posicionador de camposAbra o seu PDF, arraste os campos e copie o fields pronto. Roda no navegador — o arquivo não é enviado.
Requisição
curl -X POST https://apisign.bitsai.app/v1/envelopes \
  -H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI" \
  -H "Idempotency-Key: pedido-48219" \
  -F "file=@contrato.pdf;type=application/pdf" \
  -F "title=Contrato de prestação de serviços" \
  -F "message=Segue o contrato para assinatura." \
  -F 'signers=[
        {"name":"Maria Silva","email":"maria@exemplo.com.br"},
        {"name":"João Souza","email":"joao@exemplo.com.br"}
      ]'
Resposta 201
{
  "id": "813720b6-…",
  "title": "Contrato de prestação de serviços",
  "status": "sent",
  "signing_order": "parallel",
  "created_at": "2026-03-11T14:02:10.781Z",
  "updated_at": "2026-03-11T14:02:11.910Z",
  "sent_at": "2026-03-11T14:02:11.848Z",
  "completed_at": null,
  "expires_at": "2026-04-10T02:59:59.999Z",
  "cancelled_at": null,
  "signers": [
    {
      "id": "9855ee09-…",
      "name": "Maria Silva",
      "email": "maria@exemplo.com.br",
      "document_last_four": null,
      "signing_order": 1,
      "status": "pending",
      "invited_at": "2026-03-11T14:02:11.848Z",
      "viewed_at": null,
      "signed_at": null,
      "declined_at": null,
      "decline_reason": null,
      "signing_url": "https://sign.bitsai.app/s/9f3c…"
    }
  ]
}

Posicionar campos

Opcional. Sem fields, a API posiciona sozinha — o comportamento de sempre.

Mande fields quando a assinatura precisar de um lugar determinado: rubrica ao lado de uma cláusula, minuta com espaço reservado, formulário com moldura desenhada. Sem o campo, nada muda — a API posiciona no rodapé da última página como sempre fez.

Posicionador de camposAbra o seu PDF, arraste os campos e copie o fields pronto. Roda no navegador — o arquivo não é enviado.

O sistema de coordenadas

Fração de 0 a 1 da página já renderizada, ou seja depois de aplicar a rotação nativa do PDF. A origem é o canto superior esquerdo.

Fração, e não pontos ou pixels, porque o mesmo envelope é desenhado em escalas diferentes: um valor em pixel amarraria a posição ao zoom de quem gerou a requisição e o campo sairia deslocado no PDF final.

CampoTipoRegra
signer_emailtextoO e-mail exato de um item de signers. Obrigatório, exceto em campo da parte pré-assinada
pre_signedbooleanoUse true no lugar de signer_email para apontar o campo à parte pré-assinada. Só true é aceito, e os dois juntos são recusados
pageinteiroObrigatório. De 1 até o total de páginas do PDF
position_xnúmeroObrigatório. Borda esquerda do campo ÷ largura da página. De 0 a menos de 1
position_ynúmeroObrigatório. Borda superior do campo ÷ altura da página. De 0 a menos de 1
widthnúmeroOpcional. Largura do campo ÷ largura da página. Sem ele, o padrão do tipo
heightnúmeroOpcional. Altura do campo ÷ altura da página. Sem ele, o padrão do tipo
typetextoOpcional. signature (padrão), initial ou date
labeltextoOpcional. Até 150 caracteres, exibido junto do campo
requiredbooleanoOpcional, padrão true. Uma assinatura com required: false NÃO conta para a cobertura abaixo

Os três tipos

typeO que éTamanho padrão
signatureA assinatura do signatário, desenhada ou digitada por ele0.3025 × 0.0641
initialRubrica. Mesma coleta da assinatura, em tamanho menor — é o campo de repetir em toda folha0.1613 × 0.0641
dateData da assinatura. Quem preenche é o SERVIDOR, no momento em que a pessoa assina — o valor não é digitado nem aceito do cliente0.2353 × 0.0428

A data vem do servidor de propósito: um campo preenchido com o relógio do navegador seria prova controlada por quem está sendo auditado. Ela é exibida no fuso da conta.

Todo signatário precisa de uma assinatura

Se você mandar fields, cada pessoa de signers precisa de ao menos um campo do tipo signature obrigatório. Faltando alguém, a criação é recusada com 422 e signer_without_signature_field, e o meta traz os e-mails que ficaram de fora.

Obrigatório porque uma assinatura opcional é uma assinatura que a pessoa pode deixar em branco: ela abriria o documento, concluiria, e o envelope andaria sem a assinatura dela. Campos de rubrica e data podem ser opcionais à vontade — a exigência é sobre a assinatura.

A recusa existe porque a alternativa é pior: um signatário recebe o convite, abre o documento e não tem onde assinar. O envelope trava no meio e quem descobre é o cliente do outro lado — uma linha esquecida no array vira um telefonema.

A parte pré-assinada também precisa de posição

Com pre_signed: true no envelope, o titular da chave entra como parte já assinada — e é num campo que a assinatura salva dele é carimbada no envio. Como o e-mail dele não está em signers, o campo dele é marcado com pre_signed: true em vez de signer_email.

É assim, e não pelo e-mail, pelo mesmo motivo que o envelope não aceita escolher quem pré-assina: a identidade sai da chave autenticada, nunca do payload. Um campo que aceitasse o e-mail do titular seria uma segunda porta para apontar a assinatura salva de alguém.

Sem campo para ele, a assinatura não sai

Se o envelope é pre_signed e nenhum campo aponta para o titular, a criação é recusada — antes disso, o comportamento seria um envelope marcado como assinado com o espaço em branco no papel.

Ainda dá para misturar

Nada impede vários campos para a mesma pessoa, em páginas diferentes: uma assinatura na última página e uma rubrica em cada folha é o caso mais comum. O teto é de 500 campos por envelope.
Requisição com posição
curl -X POST https://apisign.bitsai.app/v1/envelopes \
  -H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI" \
  -H "Idempotency-Key: pedido-48219" \
  -F "file=@contrato.pdf;type=application/pdf" \
  -F "title=Contrato de prestação de serviços" \
  -F 'signers=[
        {"name":"Maria Silva","email":"maria@exemplo.com.br"},
        {"name":"João Souza","email":"joao@exemplo.com.br"}
      ]' \
  -F 'fields=[
        {"signer_email":"maria@exemplo.com.br",
         "page":4,"position_x":0.08,"position_y":0.78},
        {"signer_email":"joao@exemplo.com.br",
         "page":4,"position_x":0.55,"position_y":0.78},
        {"signer_email":"maria@exemplo.com.br","type":"initial",
         "page":2,"position_x":0.82,"position_y":0.92}
      ]'
Com parte pré-assinada
[
  { "pre_signed": true,
    "page": 4, "position_x": 0.08, "position_y": 0.64 },
  { "signer_email": "maria@exemplo.com.br",
    "page": 4, "position_x": 0.55, "position_y": 0.64 }
]

Listar envelopes

Apenas os envelopes criados por esta chave.

GET/v1/envelopesescopo: envelopes:read

Lista apenas os envelopes criados por esta chave. Não existe parâmetro que amplie o conjunto.

ParâmetroRegra
statusOpcional. Um dos status da tabela de Regras de negócio
limitOpcional. De 1 a 100. Padrão 25
cursorOpcional. O next_cursor da página anterior
O item da lista não traz os signatários, só as contagens. Uma página de 100 envelopes com dez signatários cada viraria mil objetos numa resposta usada para montar uma tabela. Quem precisa do detalhe pede o envelope.
Requisição
curl "https://apisign.bitsai.app/v1/envelopes?status=sent&limit=50" -H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI"
Resposta
{
  "data": [
    {
      "id": "813720b6-…",
      "title": "Contrato de prestação de serviços",
      "status": "sent",
      "signing_order": "parallel",
      "signers_count": 2,
      "signed_count": 0,
      "created_at": "2026-03-11T14:02:10.781Z",
      "updated_at": "2026-03-11T14:02:11.910Z",
      "sent_at": "2026-03-11T14:02:11.848Z",
      "completed_at": null,
      "expires_at": "2026-04-10T02:59:59.999Z",
      "cancelled_at": null
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Consultar um envelope

Estado atual, com a lista completa de signatários.

GET/v1/envelopes/{envelope_id}escopo: envelopes:read

Estado atual do envelope, com a lista completa de signatários. É a rota para acompanhar o progresso enquanto não houver webhooks.

A resposta tem exatamente o mesmo formato da criação — o mesmo objeto, com os campos de data preenchidos conforme o envelope avança.

cURL
curl https://apisign.bitsai.app/v1/envelopes/813720b6-… -H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI"

Reenviar o convite

Emite um link novo, manda o e-mail e devolve o endereço.

POST/v1/envelopes/{envelope_id}/signers/{signer_id}/resendescopo: envelopes:write

É a saída para quando o e-mail não chegou, para quando você perdeu a resposta da criação, e para quando precisa do link de novo porque vai entregá-lo por outro canal. Responde 200 com o envelope inteiro, e a signing_url preenchida só para o signatário reenviado — os outros vêm null, como em qualquer leitura.

Reenviar invalida o link anterior

O token é rotacionado: o endereço que já estava com a pessoa para de funcionar na hora. É de propósito — o motivo mais comum de reenviar é o e-mail ter ido para o endereço errado, e ali o link antigo precisa morrer. Mas pela API é fácil disparar sem querer num retry, então trate esta rota como uma ação deliberada, não como "buscar o link".

Exige envelopes:write, e não envelopes:read: a chamada derruba um link de assinatura, o que é escrita. Não aceita Idempotency-Key — repetir é justamente o que ela faz, e uma chave devolveria em silêncio um link que já não vale.

RecusaHTTPQuando
resend_cooldown_active429Menos de 60s desde o último envio para esse signatário. O meta traz retry_after_seconds
invalid_envelope_state409O envelope não está em curso, ou o signatário já assinou, recusou ou não está na vez
signer_not_found404O signatário não é deste envelope
resource_not_found404O envelope não existe ou não foi criado por esta chave
Requisição
curl -X POST https://apisign.bitsai.app/v1/envelopes/813720b6-…/signers/9855ee09-…/resend   -H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI"
Resposta
{
  "id": "813720b6-…",
  "status": "sent",
  "signers": [
    {
      "id": "9855ee09-…",
      "email": "maria@exemplo.com.br",
      "status": "pending",
      "signing_url": "https://sign.bitsai.app/s/1d7a…"
    },
    {
      "id": "3fb1c204-…",
      "email": "joao@exemplo.com.br",
      "status": "viewed",
      "signing_url": null
    }
  ]
}

Cancelar um envelope

Derruba os links de quem ainda não assinou, sem apagar o que já aconteceu.

POST/v1/envelopes/{envelope_id}/cancelescopo: envelopes:write

Cancela o envelope e derruba os links de assinatura de quem ainda não assinou. Quem já tinha assinado permanece registrado — cancelar não apaga o que aconteceu.

É POST e não DELETE porque o envelope continua existindo, com a trilha inteira, que é o que uma auditoria espera encontrar.

reason é opcional, até 500 caracteres, e entra na trilha de auditoria.

A resposta é o envelope no formato de sempre, já com status: "cancelled", cancelled_at preenchido e cada signatário pendente em status: "cancelled". Quem já havia assinado permanece como signed.

Cancelar o que já está cancelado devolve 200 com o mesmo envelope, sem erro e sem registrar duas vezes. Cancelar o que já foi concluído, recusado ou expirado devolve 409: não há o que cancelar.
cURL
curl -X POST https://apisign.bitsai.app/v1/envelopes/813720b6-…/cancel \
  -H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI" \
  -H "Content-Type: application/json" \
  -d '{"reason":"Pedido cancelado pelo cliente"}'

Baixar documentos

O PDF assinado, o certificado, o manifesto e a trilha.

GET/v1/envelopes/{envelope_id}/artifacts/{artifact}escopo: documents:read

Baixa os arquivos do envelope. A resposta é o conteúdo em si, com Content-Disposition: attachment — nunca um link temporário, para que o download continue dentro da sua autorização e apareça na trilha de auditoria da conta.

O Content-Type acompanha o artefato: application/pdf, application/json ou application/zip. O nome do arquivo vem no Content-Disposition, derivado do título do envelope e já higienizado — no curl, -O -J o respeita.

ArtefatoO que é
originalO PDF exatamente como você enviou
signedO PDF final, com as assinaturas aplicadas
certificateCertificado de evidências, em PDF
manifestManifesto legível por máquina, em JSON
auditTrilha de auditoria do envelope, em JSON
packageZIP com tudo o que existir
Pedir um artefato que ainda não existe devolve 404 com artifact_not_ready no meta. Aguarde o envelope chegar a completed.
cURL
curl -O -J https://apisign.bitsai.app/v1/envelopes/813720b6-…/artifacts/signed \
  -H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI"
ArtefatoDisponível quando
originalSempre
signedDepois de completed
certificateDepois de completed
manifestDepois de completed
auditSempre
packageSempre

Webhooks

Em vez de perguntar o status, receba um POST assinado quando algo acontece.

Você cadastra uma URL sua e escolhe os eventos. Quando um deles acontece, enviamos um POST com content-type application/json e um cabeçalho de assinatura. Responder qualquer 2xx encerra a entrega; qualquer outra coisa — inclusive um redirecionamento, que nós não seguimos — conta como falha e entra na fila de retentativa.

Dá para cadastrar de dois jeitos: na tela de Webhooks da plataforma, ou pelas rotas /v1/webhooks descritas adiante. Os dois cadastros são independentes de propósito — veja Isolamento por chave.

O que chega no seu servidor
POST /leggal/events HTTP/1.1
content-type: application/json
x-leggal-event-type: envelope.signed
x-leggal-event-id: 8f1c1a2e-…
x-leggal-timestamp: 1742306531
x-leggal-signature: 3b9f2c…
x-leggal-signature-version: v1
user-agent: BitsAI-Webhooks/1.0

Eventos disponíveis

Oito eventos. Assine os que quiser, ou mande ["*"] para receber todos.

Mandar ["*"] inscreve também nos eventos que entrarem no produto depois, sem você precisar voltar aqui.

Por que não existe envelope.created

Pela API, criar e enviar são o mesmo ato: o evento sairia sempre no mesmo instante que envelope.sent, sem acrescentar informação. E não há eventos por signatário — envelope.signed já carrega signer_id e signer_email, e dispara uma vez por assinatura coletada.
EventoQuando disparaStatus depois
envelope.sentEnvelope enviado e convites disparados.sent
envelope.viewedUm signatário abriu o documento pela primeira vez.sent
envelope.signedUma assinatura foi coletada.sent
envelope.declinedUm signatário recusou; o envelope parou.declined
envelope.completedTodos assinaram e o processo terminou.completed
envelope.cancelledEnvelope cancelado; os links de assinatura deixaram de valer.cancelled
envelope.expiredPrazo vencido sem conclusão.expired
document.readyPDF final, certificado e trilha disponíveis para download.completed

Formato do payload

Todo evento tem o mesmo envelope externo; o que muda é o conteúdo de data.

CampoDescrição
idIdentificador único DESTE evento. É a chave de deduplicação: as retentativas repetem o mesmo id.
typeUm dos oito eventos da tabela acima.
created_atQuando o evento aconteceu, em ISO 8601 UTC. Não é a hora do envio.
workspace_idA conta a que o evento pertence. Útil se você recebe eventos de várias contas na mesma URL.
envelope_idO envelope, quando o evento tem um. Sempre presente nos oito eventos atuais.
dataOs dados do evento. Trate como aberto: campos novos podem aparecer sem aviso, e nenhum campo existente é removido.

Campos de data por evento

EventoCampos em data (além de envelope_id)
envelope.sentstatus, title, signing_order, total_signers, invited_now, sent_at, expires_at
envelope.viewedsigner_id, signer_email, viewed_at
envelope.signedstatus, signer_id, signer_email, signed_at, signature_hash, signed_count, total_count
envelope.declinedstatus, signer_id, signer_email, reason_code, declined_at
envelope.completedstatus, completed_at, validation_code
envelope.cancelledstatus, title, cancelled_at, revoked_signers
envelope.expiredstatus, title, expires_at, revoked_signers, signed_count
document.readyvalidation_code, final_sha256, manifest_sha256, pages, signed_document_id, certificate_document_id

O que nunca vem no payload

Link de assinatura, token de acesso, CPF, conteúdo do documento, chave de armazenamento e qualquer segredo são removidos antes do envio. O destino é um servidor fora da nossa infraestrutura, alcançado por HTTP: um token de assinatura aqui viraria acesso ao contrato para quem controlasse aquele domínio, inclusive depois de ele trocar de dono. Para baixar o PDF, use GET /v1/envelopes/{envelope_id}/artifacts/{artifact} com a sua chave.
JSON
{
  "id": "8f1c1a2e-5b7d-4a9c-9f10-3c2b5d7e1a44",
  "type": "envelope.signed",
  "created_at": "2025-03-18T14:02:11.482Z",
  "workspace_id": "b1d0c9a7-2f43-4e88-9a51-77c3e0b4d612",
  "envelope_id": "4c9a0f31-8e62-47d5-b0a3-1f5e9d2c7b80",
  "data": {
    "envelope_id": "4c9a0f31-8e62-47d5-b0a3-1f5e9d2c7b80",
    "status": "sent",
    "signer_id": "7e2b4c60-9d18-4f3a-a5c7-0b6e8d1f429a",
    "signer_email": "cliente@exemplo.com.br",
    "signed_at": "2025-03-18T14:02:11.312Z",
    "signature_hash": "9b7d3f...",
    "signed_count": 1,
    "total_count": 2
  }
}

Cabeçalhos

Cinco cabeçalhos acompanham toda entrega.

O id e o tipo vêm repetidos no cabeçalho e no corpo de propósito: assim você deduplica e roteia antes de fazer o JSON.parse, que é justamente o que não deve acontecer antes de a assinatura ser conferida.

CabeçalhoConteúdo
x-leggal-signatureHMAC-SHA256 em hexadecimal minúsculo da mensagem assinada.
x-leggal-timestampUnix time em SEGUNDOS do momento do envio. Entra na mensagem assinada.
x-leggal-event-idIgual ao id do corpo. Permite deduplicar sem ler o corpo.
x-leggal-event-typeIgual ao type do corpo. Permite rotear sem ler o corpo.
x-leggal-signature-versionVersão do formato da assinatura. Hoje sempre v1 — se um dia mudar, o valor muda com ela.

Verificar a assinatura

Três conferências, nesta ordem, antes de qualquer parse.

A mensagem assinada é o timestamp, um ponto literal, e o corpo cru — nesta ordem, sem espaços.

Confira três coisas, nesta ordem: o timestamp está dentro de 300 segundos do seu relógio; o HMAC recalculado bate; e a comparação foi feita em tempo constante. Só depois faça o JSON.parse.

O erro mais comum

Se o HMAC nunca fecha e você já conferiu o segredo, o problema quase sempre é o corpo: algum middleware fez parse e o que chegou na sua função de verificação não são mais os bytes originais. A tela de Entregas na plataforma mostra o corpo exato que foi assinado — compare caractere por caractere com o que o seu servidor recebeu.
A mensagem assinada
assinatura = HMAC_SHA256(segredo, timestamp + "." + corpo_cru)
Node.js
const express = require("express");
const crypto = require("crypto");

const app = express();
const SEGREDO = process.env.LEGGAL_WEBHOOK_SECRET; // whsec_...
const TOLERANCIA_SEGUNDOS = 300;

/* express.raw, NÃO express.json: o HMAC é sobre os bytes que chegaram.
   express.json() faz parse e o corpo que o seu código vê já é outro objeto —
   reserializá-lo muda a ordem das chaves e o espaçamento, e a assinatura
   deixa de fechar sem nenhum erro aparente. */
app.post("/leggal/events", express.raw({ type: "*/*" }), (req, res) => {
  const corpo = req.body.toString("utf8");
  const timestamp = req.header("x-leggal-timestamp");
  const recebida = req.header("x-leggal-signature");

  if (!timestamp || !recebida) return res.status(400).send("faltam cabecalhos");

  /* Janela de tolerancia: sem ela, uma entrega interceptada hoje continuaria
     valida para sempre, porque a assinatura nao expira sozinha. */
  const idade = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (!Number.isFinite(idade) || idade > TOLERANCIA_SEGUNDOS) {
    return res.status(400).send("timestamp fora da janela");
  }

  const esperada = crypto
    .createHmac("sha256", SEGREDO)
    .update(`${timestamp}.${corpo}`, "utf8")
    .digest("hex");

  /* timingSafeEqual, nao ===: comparar strings para no primeiro byte diferente,
     e a diferenca de tempo revela quantos caracteres do inicio estao certos. */
  const a = Buffer.from(esperada, "utf8");
  const b = Buffer.from(recebida, "utf8");
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(400).send("assinatura invalida");
  }

  const evento = JSON.parse(corpo);

  /* Responda ANTES de processar: o timeout e de 10 segundos, e um 2xx que
     demora vira falha e retentativa. */
  res.status(200).send("ok");
  minhaFila.enfileirar(evento);
});
Python
import hashlib
import hmac
import json
import os
import time

from flask import Flask, request

app = Flask(__name__)
SEGREDO = os.environ["LEGGAL_WEBHOOK_SECRET"].encode()
TOLERANCIA_SEGUNDOS = 300


@app.post("/leggal/events")
def receber():
    # get_data() devolve os bytes crus; request.json faria o parse antes.
    corpo = request.get_data()
    timestamp = request.headers.get("x-leggal-timestamp", "")
    recebida = request.headers.get("x-leggal-signature", "")

    if not timestamp.isdigit() or not recebida:
        return "faltam cabecalhos", 400

    if abs(int(time.time()) - int(timestamp)) > TOLERANCIA_SEGUNDOS:
        return "timestamp fora da janela", 400

    mensagem = timestamp.encode() + b"." + corpo
    esperada = hmac.new(SEGREDO, mensagem, hashlib.sha256).hexdigest()

    # compare_digest, nao ==: comparacao em tempo constante.
    if not hmac.compare_digest(esperada, recebida):
        return "assinatura invalida", 400

    evento = json.loads(corpo)
    minha_fila.enfileirar(evento)
    return "", 200

Entrega e retentativas

Seis tentativas ao longo de pouco mais de duas horas e meia.

O contador de falhas é de falhas consecutivas: uma entrega bem-sucedida o zera. Quando um endpoint é suspenso, quem administra a conta recebe o aviso na plataforma e na trilha de auditoria — nunca por webhook, porque avisar por webhook que o webhook está quebrado não chegaria a ninguém.

Deduplique pelo id do evento

Uma entrega pode chegar mais de uma vez: o seu servidor pode processar e cair antes de responder, e a retentativa vem com o mesmo id. Guarde os ids já processados e ignore repetição. Também não conte com a ordem: envelope.viewed e envelope.signed de signatários diferentes podem chegar trocados, e o estado autoritativo é sempre GET /v1/envelopes/{envelope_id}.
RegraValor
SucessoQualquer resposta 2xx
Timeout10 segundos por tentativa
RedirecionamentosNão seguimos. Um 3xx conta como falha — cadastre a URL final.
Tentativas6 no total, contando a primeira
Esperas entre elas10s · 1min · 5min · 30min · 2h
Janela total≈ 2h36 entre a primeira tentativa e a última. Depois disso a entrega é encerrada como falha.
Suspensão automática20 falhas consecutivas pausam o endpoint. Reativar zera o contador.
Endpoints ativosAté 10 por conta. Cada evento gera uma entrega por endpoint inscrito.
User-AgentBitsAI-Webhooks/1.0

Gerenciar pela API

Cinco rotas, com os escopos webhooks:read e webhooks:write.

Todas devolvem o mesmo objeto de endpoint — e nenhuma devolve o segredo, com uma exceção explicada abaixo.

POST/v1/webhooksescopo: webhooks:write

Cria o endpoint e devolve o segredo. Exige Idempotency-Key, como toda escrita da API.

Aqui a chave ainda NÃO deduplica

Diferente de POST /v1/envelopes, esta rota exige o cabeçalho mas ainda não o usa para descartar repetição: dois POST com a mesma chave criam dois endpoints. Na prática isso significa receber cada evento duas vezes e gastar duas das 10 vagas ativas, e você descobriria pelo volume, não por um erro. Se a chamada expirar sem resposta, liste os endpoints com GET /v1/webhooks antes de repetir. Mande o cabeçalho de qualquer forma — ele é o que fará a deduplicação valer quando ela entrar.
cURL
curl -X POST https://apisign.bitsai.app/v1/webhooks \
  -H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI" \
  -H "Idempotency-Key: webhook-producao-01" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Produção",
    "url": "https://seusite.com.br/leggal/events",
    "events": ["envelope.signed", "envelope.completed", "document.ready"]
  }'
JSON
{
  "id": "d3f8a1b2-4c56-4e78-9a0b-1c2d3e4f5a6b",
  "name": "Produção",
  "url": "https://seusite.com.br/leggal/events",
  "status": "active",
  "subscribed_events": ["envelope.signed", "envelope.completed", "document.ready"],
  "secret": "whsec_4f1a...c8d2",
  "secret_last_four": "c8d2",
  "secret_rotated_at": "2025-03-18T14:00:00.000Z",
  "failure_count": 0,
  "last_success_at": null,
  "last_failure_at": null,
  "created_at": "2025-03-18T14:00:00.000Z",
  "updated_at": "2025-03-18T14:00:00.000Z"
}

O segredo aparece uma única vez

Ele vem nesta resposta e em nenhum outro lugar: não está na listagem, não está no detalhe, e não existe rota para recuperá-lo. No banco ele fica cifrado, e só o processo de entrega o decifra. Guarde no seu gerenciador de segredos antes de encerrar a chamada — quem perde precisa rotacionar, e rotacionar quebra a verificação do lado de quem recebe até o valor novo ser publicado. secret_last_four serve para conferir qual segredo está em uso, e não substitui o valor.
GET/v1/webhooksescopo: webhooks:read

Lista os endpoints da sua chave, mais recentes primeiro, com paginação por cursor (limit e cursor) igual à de envelopes.

cURL
curl https://apisign.bitsai.app/v1/webhooks \
  -H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI"
GET/v1/webhooks/{endpoint_id}escopo: webhooks:read

Detalhe de um endpoint. Use failure_count, last_success_at e last_failure_at para monitorar a saúde do seu receptor de dentro do seu próprio sistema.

PATCH/v1/webhooks/{endpoint_id}escopo: webhooks:write

Atualiza name, url e events — os três são obrigatórios, e a lista de eventos substitui a anterior, não soma. Aceita também status com active ou disabled para pausar e retomar, e rotate_secret: true para emitir um segredo novo — que vem na resposta, também uma única vez.

cURL
curl -X PATCH https://apisign.bitsai.app/v1/webhooks/d3f8a1b2-4c56-4e78-9a0b-1c2d3e4f5a6b \
  -H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Produção",
    "url": "https://seusite.com.br/leggal/events/v2",
    "events": ["*"],
    "rotate_secret": true
  }'

Pausar não é revogar

status: "disabled" para as entregas e preserva o segredo: você volta ao ar num PATCH sem reconfigurar nada do seu lado. Revogar destrói o segredo e é definitivo. Um status igual a suspended não pode ser definido por você — é o que o sistema aplica depois de 20 falhas seguidas; mandar active reativa e zera o contador.

Endpoint parado não acumula fila

Enquanto o endpoint está pausado, suspenso ou revogado, os eventos que acontecem não são guardados para depois — eles simplesmente não geram entrega para ele. Reativar volta a receber dali para a frente, e não recupera o período parado. Para saber o que houve no intervalo, consulte GET /v1/envelopes/{envelope_id}, que é sempre o estado autoritativo. Só a retentativa de uma entrega que já existia continua valendo enquanto ela não esgotar as tentativas.
DELETE/v1/webhooks/{endpoint_id}escopo: webhooks:write

Revoga o endpoint: as entregas pendentes dele são canceladas e o segredo é apagado. O registro não é excluído — o histórico de entregas precisa continuar auditável, e uma conta que não consegue provar o que enviou não tem trilha. Responde 204.

cURL
curl -X DELETE https://apisign.bitsai.app/v1/webhooks/d3f8a1b2-4c56-4e78-9a0b-1c2d3e4f5a6b \
  -H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI"

Isolamento por chave

Um endpoint pertence à chave que o criou — e só a ela.

Outra chave da mesma conta não o lista, não o altera e não o revoga — recebe 404, igual a um id que não existe. O que foi cadastrado na tela da plataforma também é invisível para qualquer chave, e vice-versa.

Isso é mais restritivo que o resto da API de propósito. Um endpoint de webhook decide para onde os dados da conta vão: se uma chave pudesse reapontar o endpoint de outra, uma única chave vazada redirecionaria os webhooks da empresa inteira. Ler dado alheio é grave; desviar o fluxo é pior.

Erros de webhook

As recusas que só aparecem nas rotas de webhook.

HTTPcodeO que fazer
404not_foundO endpoint não existe, é de outra conta ou foi criado por outra chave. Não há como distinguir os três, de propósito.
422invalid_requestURL recusada: aponte para um host público, alcançável e com esquema http ou https. Endereço interno, localhost e IP de metadados de nuvem são bloqueados.
422invalid_requestNenhum evento válido na lista. Use os nomes exatos da tabela de eventos ou ["*"].
422webhook_endpoint_limit_reachedA conta já tem 10 endpoints ativos. Revogue um antes de criar outro.
402plan_limit_reachedO plano da conta não inclui webhooks. A leitura continua liberada; só a escrita é recusada.
403insufficient_scopeA chave não tem webhooks:read ou webhooks:write. Escopo é definido na criação da chave e não muda depois.

Antes de ir para produção

Use o botão Enviar evento de teste na tela de Webhooks: ele dispara um webhook.test pelo mesmo caminho de um evento real — mesma assinatura, mesmos cabeçalhos, mesma fila — e aparece nas Entregas com o corpo enviado e a resposta do seu servidor. É a forma mais rápida de descobrir que a verificação do HMAC está errada antes de o primeiro contrato de verdade depender dela. webhook.test não está na lista de inscrição porque não é assinável: ele vai para o endpoint que você escolher, independentemente dos eventos marcados.

Criar e enviar

Não existe rascunho pela API: uma chamada, um envelope enviado, ou nada.

Na plataforma, criar o envelope, anexar o PDF, definir signatários, posicionar campos e enviar são cinco telas porque há alguém decidindo entre uma e outra. Numa integração não há: o seu sistema já sabe tudo quando chama.

Expor os cinco passos produziria rascunhos órfãos toda vez que a integração falhasse no meio — e você não teria como terminá-los, já que posicionar campo exige a tela.

Posicionamento de campos

Automático por padrão; manual quando você manda fields.

Sem fields, cada signatário recebe um campo de assinatura obrigatório no rodapé da última página, em grade de duas colunas, preenchida de baixo para cima. Se não couber, transborda para a página anterior — nunca para uma folha nova, porque acrescentar página mudaria o documento que você enviou.

Cabem 18 assinaturas por página. Se a lista de signatários não couber nas páginas do documento, a criação é recusada com 422 e o número exato de vagas disponíveis.

Esse é o padrão porque cobre o caso comum sem exigir que você abra o PDF: a praxe brasileira assina no fim do documento, e calcular coordenada de um arquivo que você mandou mas não desenhou é onde a maioria erra — e o erro só apareceria no PDF assinado, quando já não dá para corrigir sem cancelar.

Quando a posição importa, mande fields: a tabela completa, o sistema de coordenadas e as regras estão em Posicionar campos. O padrão continua valendo para quem não manda nada.

A tela da plataforma continua com posicionamento manual por arrasto, que é o caminho quando alguém precisa VER a página antes de decidir.

Ordem de assinatura

Pela API, só paralelo — todos assinam em qualquer ordem.

signing_order na API aceita apenas parallel, que também é o padrão. Mandar sequential devolve 400.

O motivo é o link. No paralelo, todos os signatários recebem o token no envio, e é por isso que POST /v1/envelopes consegue devolver o signing_url de cada um. No sequencial só o primeiro recebe: o link do segundo nasce quando o primeiro assina, dias depois, sem nenhuma requisição por perto — e como guardamos apenas o hash do token, ele não teria como ser buscado nem por você nem por nós. A resposta traria um link de três, e os outros dois não existiriam em lugar nenhum.

A fila sequencial continua na plataforma, onde a tela mostra de quem é a vez e o e-mail é o único canal de entrega.

ModoOnde existe
parallelAPI e plataforma. Todos recebem o convite ao mesmo tempo e podem assinar em qualquer ordem
sequentialSó na plataforma. Um de cada vez, na ordem da lista; o próximo é chamado quando o anterior assina

Pré-assinatura

O titular da chave entra como parte já assinada.

Com pre_signed: true, o titular da chave entra no envelope como parte já assinada, e a assinatura salva no perfil dele é aplicada no momento do envio. É o caso de quem dispara contrato padrão e só precisa que o cliente assine o lado dele.

Quem é essa pessoa não é escolha sua: nome e e-mail saem da conta, do usuário que criou a chave. Um campo que aceitasse e-mail permitiria apontar a assinatura salva de terceiro para um contrato qualquer, e o resultado teria aparência de prova.

Ela fica fora da fila de assinatura, então não altera a ordem dos demais, e o certificado registra que a autorização foi a posse da chave — não uma sessão interativa.

Se você também mandar fields, reserve um campo para ela com pre_signed: true — é nesse campo que a assinatura salva é carimbada. Ver Posicionar campos.

A validação acontece antes de qualquer coisa

Se o titular não puder pré-assinar, a chamada é recusada antes de o PDF ser guardado, a cota consumida e o e-mail disparado. Nenhum envelope é criado. Depois do envio não haveria como corrigir sem cancelar.
Recusa (422)QuandoComo resolver
pre_signed_signature_missingO titular da chave não tem assinatura cadastradaPeça a ele para cadastrar em Perfil › Assinatura. O e-mail dele vem no meta da resposta
pre_signed_owner_inactiveO titular deixou de ser membro ativo da contaCrie uma chave nova com alguém que esteja na conta
pre_signed_owner_duplicatedO titular já está na lista de signersRemova-o da lista ou desligue pre_signed
pre_signed_owner_unavailableO usuário que criou a chave não existe maisCrie uma chave nova

CPF do signatário

Quem decide é a política da conta, não a requisição.

Se a conta exige documento de todo signatário, require_cpf: false não a desliga — uma conta que definiu a exigência não deixa de exigir porque a chamada veio de um sistema. Quando a política é opcional, o campo da requisição vale.

O CPF coletado nunca sai pela API: a resposta traz apenas document_last_four, o suficiente para conferir identidade sem revelar o documento.

Isolamento entre chaves

Uma chave só enxerga os envelopes que ela mesma criou.

Duas integrações da mesma empresa não se veem, e nenhuma delas lê o que foi criado à mão na plataforma. Uma chave vazada compromete a integração dela — não o arquivo da conta inteira.

O workspace nunca vem da URL nem do corpo: ele sai do contexto da chave autenticada. Mandar workspace_id no corpo devolve 400.

Status do envelope

Os oito estados que um envelope criado pela API pode ter.

Existe também draft, mas envelope criado pela API nunca fica nesse estado — ele nasce enviado.

Só a partir de completed os artefatos assinados existem: pedi-los antes devolve 404 com artifact_not_ready.

StatusSignifica
sentEnviado; ninguém abriu ainda
viewedAo menos um signatário abriu o documento
partialAlguém já assinou, mas faltam outros
finalizingTodos assinaram; o PDF final está sendo gerado. Costuma durar segundos
completedConcluído. É a partir daqui que os artefatos assinados existem
declinedAlguém recusou; o envelope parou
cancelledCancelado por você ou pela conta
expiredPassou de expires_at sem completar

Status do signatário

O que cada pessoa da lista já fez.

Em envelope criado pela API, todos nascem pending com invited_at preenchido: o convite de todo mundo sai no envio. Num envelope sequencial da plataforma, quem ainda não foi chamado também aparece como pending — ali o que diferencia os dois é justamente invited_at estar vazio.

StatusSignifica
pendingConvite enviado, aguardando. Em envelope sequencial da plataforma, quem ainda não foi chamado também aparece aqui — sem invited_at
viewedAbriu o link do convite
authenticatedConfirmou o código enviado por e-mail e está no meio da assinatura
signedAssinou. signed_at preenchido
declinedRecusou. declined_at e decline_reason preenchidos
cancelledO envelope foi cancelado antes de esta pessoa assinar
expiredO prazo acabou antes de assinar

Prazo

Sempre no fim do dia, no fuso da conta.

Sem expires_at, vale o prazo padrão configurado na conta, calculado no fuso dela e sempre no fim do dia — um envelope criado às 23h com prazo de 7 dias não expira às 23h do sétimo dia, cortando o dia útil do signatário pela metade.

O link de assinatura nunca vive mais que o envelope: se o prazo for curto, o link vence junto.

O link vence em 30 dias, mesmo com prazo maior

A validade do link é o MENOR entre o prazo do envelope e 30 dias a partir do envio. Num contrato com 60 dias de prazo, o link para de funcionar no trigésimo dia com o envelope ainda aberto — quem tentar abrir depois disso precisa de um novo. Importa mais se você guarda o signing_url para entregar por outro canal: um link guardado hoje e enviado daqui a um mês pode já estar vencido. A saída é Reenviar o convite, que emite um link com validade nova.

Cota do plano

Cada envelope enviado consome uma unidade, igual ao envio pela tela.

Estourar a cota devolve 402 com billing_error e o código envelope_quota_exceeded, e nenhum envelope é criado — nem rascunho.

A família é billing_error justamente para você não perder tempo relendo o payload: não há campo errado, o que falta é contrato.

Resposta 402
{
  "error": {
    "type": "billing_error",
    "code": "envelope_quota_exceeded",
    "message": "Envelope quota exceeded for the current cycle"
  }
}

Fluxo completo

Do disparo ao PDF assinado, com os comandos na ordem.

1. Confirme a chave antes de qualquer coisa.

cURL
curl https://apisign.bitsai.app/v1/me \
  -H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI"

2. Crie o envelope. Guarde o id devolvido junto do seu registro — é por ele que você consulta depois.

cURL
curl -X POST https://apisign.bitsai.app/v1/envelopes \
  -H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI" \
  -H "Idempotency-Key: pedido-48219" \
  -F "file=@contrato.pdf;type=application/pdf" \
  -F "title=Contrato de prestação de serviços" \
  -F 'signers=[{"name":"Maria Silva","email":"maria@exemplo.com.br"}]'

3. Acompanhe. Enquanto não houver webhooks, consulte o envelope de tempos em tempos — a cada poucos minutos basta, e o limite de 10 requisições por segundo é folgado para isso.

cURL
curl https://apisign.bitsai.app/v1/envelopes/813720b6-… \
  -H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI"

4. Quando o status for completed, baixe o que precisar. O pacote traz tudo de uma vez.

cURL
curl -O -J https://apisign.bitsai.app/v1/envelopes/813720b6-…/artifacts/package \
  -H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI"

5. Precisou desistir? Cancele. Os links de quem ainda não assinou deixam de funcionar.

cURL
curl -X POST https://apisign.bitsai.app/v1/envelopes/813720b6-…/cancel \
  -H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI" \
  -H "Content-Type: application/json" \
  -d '{"reason":"Pedido cancelado pelo cliente"}'

Boas práticas

Do lado de cá já fizemos a nossa parte. Estas são as do lado de lá.

FaçaPor quê
Guarde o token no gerenciador de segredos, nunca no códigoRepositório vaza — por acidente, por fork, por backup. Um token no histórico do Git continua lá depois de removido do arquivo
Uma chave por integração, com o escopo mínimoQuando algo vaza, você revoga só aquela e sabe exatamente o que ela alcançava
Sempre mande Idempotency-Key nas escritasTimeout de rede não significa que a requisição falhou. Sem a chave, o retry manda o contrato duas vezes para o seu cliente
Trate 429 respeitando o Retry-AfterRepetir antes prolonga o bloqueio e não adianta a fila
Compare error.code, nunca error.messageA mensagem é escrita para humano e pode mudar de redação; o código é contrato
Revogue chaves que saíram de usoChave viva é porta aberta, mesmo que ninguém esteja usando
Nunca chame a API do navegador ou do app do usuárioQualquer pessoa com o DevTools aberto levaria o token e passaria a criar contratos em nome da sua conta

O que fica registrado

Toda ação da chave entra na trilha de auditoria da conta, identificada pelo nome que você deu à chave — criação, cancelamento e download de documento. Quem administra a conta consegue ver o que cada integração fez e quando.