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
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.
https://apisign.bitsai.app/v1Autenticaçã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.
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
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.curl https://apisign.bitsai.app/v1/me \
-H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI"{
"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
| Passo | Onde |
|---|---|
| 1. Entre na sua conta | Área logada da plataforma |
| 2. Abra Desenvolvedores › Chaves de API | Menu lateral |
| 3. Clique em Criar chave | Botão no topo da lista |
| 4. Dê um nome e escolha os escopos | O nome identifica a integração na trilha de auditoria |
| 5. Copie o token | Ele 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.
| Escopo | Permite |
|---|---|
envelopes:read | Listar envelopes e consultar o status de cada signatário |
envelopes:write | Criar envelopes, disparar convites e cancelar |
documents:read | Baixar o PDF assinado, o certificado e a trilha de auditoria |
webhooks:read | Listar os webhooks da chave e consultar a saúde de cada endpoint |
webhooks:write | Cadastrar, 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.
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.
Idempotency-Key: pedido-48219Limite 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.
{
"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
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
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.x-request-id: req_8f2c41d9a7Erros
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.
| type | HTTP | O que fazer |
|---|---|---|
invalid_request_error | 400, 413, 415, 422 | Corrigir o payload. Repetir sem mudar nada dá o mesmo erro |
authentication_error | 401 | Conferir o token. Pode ter sido revogado, expirado ou a conta perdeu o plano |
permission_error | 403 | A chave é válida mas o ato não é permitido — falta escopo, ou o envelope restringe aquele download |
billing_error | 402 | Cota ou plano. Não há campo errado no payload: o que falta é contrato |
not_found_error | 404 | O recurso não existe ou não pertence a esta chave |
conflict_error | 409 | O estado atual não permite a operação — por exemplo, cancelar algo já concluído |
rate_limit_error | 429 | Esperar o tempo de Retry-After |
api_error | 500 | Falha nossa. Repita com a mesma Idempotency-Key; se persistir, fale com o suporte |
404, e não 403. É de propósito: um 403 confirmaria que aquele identificador existe em algum lugar da conta.{
"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.
| code | HTTP | Quando |
|---|---|---|
invalid_api_key | 401 | Token ausente, inválido, revogado, expirado, ou a conta perdeu o plano de API |
insufficient_scope | 403 | A chave não tem o escopo que a rota exige. A mensagem diz qual |
rate_limit_exceeded | 429 | Passou do teto da chave. Veja Retry-After |
resource_not_found | 404 | O envelope não existe ou não foi criado por esta chave |
unknown_endpoint | 404 | O caminho não existe. Confira a rota e o método |
invalid_request | 400 | Campo faltando, com formato errado ou desconhecido. Veja param |
duplicate_signer_email | 400 | O mesmo e-mail aparece duas vezes em signers |
document_too_large | 413 | O PDF passou de 10 MB, ou do limite do plano da conta |
invalid_document_type | 415 | O arquivo não é PDF — inclusive quando o MIME declarado mente |
invalid_pdf | 422 | PDF corrompido ou truncado; não foi possível abrir |
encrypted_pdf | 422 | PDF protegido por senha. Remova a proteção antes de enviar |
invalid_pdf_page_count | 422 | Mais de 200 páginas |
invalid_field_geometry | 422 | Posiçã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_found | 422 | O signer_email de um campo não está em signers. O meta traz o e-mail recusado |
pre_signed_field_not_allowed | 422 | Veio um campo com pre_signed: true num envelope sem pre_signed |
signer_without_signature_field | 422 | Você mandou fields e alguém ficou sem campo de assinatura. O meta lista os e-mails |
envelope_quota_exceeded | 402 | A cota de envelopes do plano acabou no ciclo atual |
pre_signed_* | 422 | Quatro variações da pré-assinatura — ver Regras de negócio |
invalid_envelope_state | 409 | O estado atual não permite a operação, como cancelar algo concluído |
document_not_found | 404 | O artefato pedido ainda não existe. O meta traz artifact_not_ready |
original_download_disabled | 403 | Quem enviou desativou o download do original neste envelope. Não é problema de escopo |
internal_error | 500 | Falha 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.
| Rota | HTTP |
|---|---|
POST /v1/envelopes | 201 — inclusive na repetição idempotente: o status conta o resultado, não o caminho |
Todas as outras | 200 |
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 "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.
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.
| Campo | Tipo | Regra |
|---|---|---|
file | arquivo | Obrigatório. PDF, até 10 MB e 200 páginas. O plano da conta pode impor um limite menor |
title | texto | Obrigatório. De 2 a 255 caracteres |
signers | JSON | Obrigató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 |
message | texto | Opcional. Até 2000 caracteres. Aparece no e-mail de convite |
signing_order | texto | Opcional e só aceita parallel, que é o padrão. Ver abaixo |
expires_at | ISO 8601 | Opcional, precisa ser futuro. Sem ele vale o prazo padrão da conta |
require_cpf | booleano | Opcional. A política da conta tem a última palavra |
pre_signed | booleano | Opcional. Inclui o titular da chave como parte já assinada — ver Regras de negócio |
fields | JSON | Opcional. 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
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
fields pronto. Roda no navegador — o arquivo não é enviado.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"}
]'{
"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.
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.
| Campo | Tipo | Regra |
|---|---|---|
signer_email | texto | O e-mail exato de um item de signers. Obrigatório, exceto em campo da parte pré-assinada |
pre_signed | booleano | Use true no lugar de signer_email para apontar o campo à parte pré-assinada. Só true é aceito, e os dois juntos são recusados |
page | inteiro | Obrigatório. De 1 até o total de páginas do PDF |
position_x | número | Obrigatório. Borda esquerda do campo ÷ largura da página. De 0 a menos de 1 |
position_y | número | Obrigatório. Borda superior do campo ÷ altura da página. De 0 a menos de 1 |
width | número | Opcional. Largura do campo ÷ largura da página. Sem ele, o padrão do tipo |
height | número | Opcional. Altura do campo ÷ altura da página. Sem ele, o padrão do tipo |
type | texto | Opcional. signature (padrão), initial ou date |
label | texto | Opcional. Até 150 caracteres, exibido junto do campo |
required | booleano | Opcional, padrão true. Uma assinatura com required: false NÃO conta para a cobertura abaixo |
Os três tipos
| type | O que é | Tamanho padrão |
|---|---|---|
signature | A assinatura do signatário, desenhada ou digitada por ele | 0.3025 × 0.0641 |
initial | Rubrica. Mesma coleta da assinatura, em tamanho menor — é o campo de repetir em toda folha | 0.1613 × 0.0641 |
date | Data da assinatura. Quem preenche é o SERVIDOR, no momento em que a pessoa assina — o valor não é digitado nem aceito do cliente | 0.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
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
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}
]'[
{ "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.
Lista apenas os envelopes criados por esta chave. Não existe parâmetro que amplie o conjunto.
| Parâmetro | Regra |
|---|---|
status | Opcional. Um dos status da tabela de Regras de negócio |
limit | Opcional. De 1 a 100. Padrão 25 |
cursor | Opcional. O next_cursor da página anterior |
curl "https://apisign.bitsai.app/v1/envelopes?status=sent&limit=50" -H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI"{
"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.
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 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.
É 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
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.
| Recusa | HTTP | Quando |
|---|---|---|
resend_cooldown_active | 429 | Menos de 60s desde o último envio para esse signatário. O meta traz retry_after_seconds |
invalid_envelope_state | 409 | O envelope não está em curso, ou o signatário já assinou, recusou ou não está na vez |
signer_not_found | 404 | O signatário não é deste envelope |
resource_not_found | 404 | O envelope não existe ou não foi criado por esta chave |
curl -X POST https://apisign.bitsai.app/v1/envelopes/813720b6-…/signers/9855ee09-…/resend -H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI"{
"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.
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.
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 -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.
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.
| Artefato | O que é |
|---|---|
original | O PDF exatamente como você enviou |
signed | O PDF final, com as assinaturas aplicadas |
certificate | Certificado de evidências, em PDF |
manifest | Manifesto legível por máquina, em JSON |
audit | Trilha de auditoria do envelope, em JSON |
package | ZIP com tudo o que existir |
404 com artifact_not_ready no meta. Aguarde o envelope chegar a completed.curl -O -J https://apisign.bitsai.app/v1/envelopes/813720b6-…/artifacts/signed \
-H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI"| Artefato | Disponível quando |
|---|---|
original | Sempre |
signed | Depois de completed |
certificate | Depois de completed |
manifest | Depois de completed |
audit | Sempre |
package | Sempre |
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.
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.0Eventos 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
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.| Evento | Quando dispara | Status depois |
|---|---|---|
envelope.sent | Envelope enviado e convites disparados. | sent |
envelope.viewed | Um signatário abriu o documento pela primeira vez. | sent |
envelope.signed | Uma assinatura foi coletada. | sent |
envelope.declined | Um signatário recusou; o envelope parou. | declined |
envelope.completed | Todos assinaram e o processo terminou. | completed |
envelope.cancelled | Envelope cancelado; os links de assinatura deixaram de valer. | cancelled |
envelope.expired | Prazo vencido sem conclusão. | expired |
document.ready | PDF 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.
| Campo | Descrição |
|---|---|
id | Identificador único DESTE evento. É a chave de deduplicação: as retentativas repetem o mesmo id. |
type | Um dos oito eventos da tabela acima. |
created_at | Quando o evento aconteceu, em ISO 8601 UTC. Não é a hora do envio. |
workspace_id | A conta a que o evento pertence. Útil se você recebe eventos de várias contas na mesma URL. |
envelope_id | O envelope, quando o evento tem um. Sempre presente nos oito eventos atuais. |
data | Os dados do evento. Trate como aberto: campos novos podem aparecer sem aviso, e nenhum campo existente é removido. |
Campos de data por evento
| Evento | Campos em data (além de envelope_id) |
|---|---|
envelope.sent | status, title, signing_order, total_signers, invited_now, sent_at, expires_at |
envelope.viewed | signer_id, signer_email, viewed_at |
envelope.signed | status, signer_id, signer_email, signed_at, signature_hash, signed_count, total_count |
envelope.declined | status, signer_id, signer_email, reason_code, declined_at |
envelope.completed | status, completed_at, validation_code |
envelope.cancelled | status, title, cancelled_at, revoked_signers |
envelope.expired | status, title, expires_at, revoked_signers, signed_count |
document.ready | validation_code, final_sha256, manifest_sha256, pages, signed_document_id, certificate_document_id |
O que nunca vem no payload
GET /v1/envelopes/{envelope_id}/artifacts/{artifact} com a sua chave.{
"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çalho | Conteúdo |
|---|---|
x-leggal-signature | HMAC-SHA256 em hexadecimal minúsculo da mensagem assinada. |
x-leggal-timestamp | Unix time em SEGUNDOS do momento do envio. Entra na mensagem assinada. |
x-leggal-event-id | Igual ao id do corpo. Permite deduplicar sem ler o corpo. |
x-leggal-event-type | Igual ao type do corpo. Permite rotear sem ler o corpo. |
x-leggal-signature-version | Versã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
assinatura = HMAC_SHA256(segredo, timestamp + "." + corpo_cru)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);
});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 "", 200Entrega 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
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}.| Regra | Valor |
|---|---|
| Sucesso | Qualquer resposta 2xx |
| Timeout | 10 segundos por tentativa |
| Redirecionamentos | Não seguimos. Um 3xx conta como falha — cadastre a URL final. |
| Tentativas | 6 no total, contando a primeira |
| Esperas entre elas | 10s · 1min · 5min · 30min · 2h |
| Janela total | ≈ 2h36 entre a primeira tentativa e a última. Depois disso a entrega é encerrada como falha. |
| Suspensão automática | 20 falhas consecutivas pausam o endpoint. Reativar zera o contador. |
| Endpoints ativos | Até 10 por conta. Cada evento gera uma entrega por endpoint inscrito. |
| User-Agent | BitsAI-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.
Cria o endpoint e devolve o segredo. Exige Idempotency-Key, como toda escrita da API.
Aqui a chave ainda NÃO deduplica
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 -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"]
}'{
"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
secret_last_four serve para conferir qual segredo está em uso, e não substitui o valor.Lista os endpoints da sua chave, mais recentes primeiro, com paginação por cursor (limit e cursor) igual à de envelopes.
curl https://apisign.bitsai.app/v1/webhooks \
-H "Authorization: Bearer sk_live_SEU_TOKEN_AQUI"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.
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 -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
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.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 -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.
| HTTP | code | O que fazer |
|---|---|---|
| 404 | not_found | O endpoint não existe, é de outra conta ou foi criado por outra chave. Não há como distinguir os três, de propósito. |
| 422 | invalid_request | URL 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. |
| 422 | invalid_request | Nenhum evento válido na lista. Use os nomes exatos da tabela de eventos ou ["*"]. |
| 422 | webhook_endpoint_limit_reached | A conta já tem 10 endpoints ativos. Revogue um antes de criar outro. |
| 402 | plan_limit_reached | O plano da conta não inclui webhooks. A leitura continua liberada; só a escrita é recusada. |
| 403 | insufficient_scope | A 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
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.
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.
| Modo | Onde existe |
|---|---|
parallel | API e plataforma. Todos recebem o convite ao mesmo tempo e podem assinar em qualquer ordem |
sequential | Só 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
| Recusa (422) | Quando | Como resolver |
|---|---|---|
pre_signed_signature_missing | O titular da chave não tem assinatura cadastrada | Peça a ele para cadastrar em Perfil › Assinatura. O e-mail dele vem no meta da resposta |
pre_signed_owner_inactive | O titular deixou de ser membro ativo da conta | Crie uma chave nova com alguém que esteja na conta |
pre_signed_owner_duplicated | O titular já está na lista de signers | Remova-o da lista ou desligue pre_signed |
pre_signed_owner_unavailable | O usuário que criou a chave não existe mais | Crie 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.
| Status | Significa |
|---|---|
sent | Enviado; ninguém abriu ainda |
viewed | Ao menos um signatário abriu o documento |
partial | Alguém já assinou, mas faltam outros |
finalizing | Todos assinaram; o PDF final está sendo gerado. Costuma durar segundos |
completed | Concluído. É a partir daqui que os artefatos assinados existem |
declined | Alguém recusou; o envelope parou |
cancelled | Cancelado por você ou pela conta |
expired | Passou 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.
| Status | Significa |
|---|---|
pending | Convite enviado, aguardando. Em envelope sequencial da plataforma, quem ainda não foi chamado também aparece aqui — sem invited_at |
viewed | Abriu o link do convite |
authenticated | Confirmou o código enviado por e-mail e está no meio da assinatura |
signed | Assinou. signed_at preenchido |
declined | Recusou. declined_at e decline_reason preenchidos |
cancelled | O envelope foi cancelado antes de esta pessoa assinar |
expired | O 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
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.
{
"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 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 -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 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 -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 -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ça | Por quê |
|---|---|
| Guarde o token no gerenciador de segredos, nunca no código | Repositó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ínimo | Quando algo vaza, você revoga só aquela e sabe exatamente o que ela alcançava |
| Sempre mande Idempotency-Key nas escritas | Timeout 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-After | Repetir antes prolonga o bloqueio e não adianta a fila |
| Compare error.code, nunca error.message | A mensagem é escrita para humano e pode mudar de redação; o código é contrato |
| Revogue chaves que saíram de uso | Chave viva é porta aberta, mesmo que ninguém esteja usando |
| Nunca chame a API do navegador ou do app do usuário | Qualquer pessoa com o DevTools aberto levaria o token e passaria a criar contratos em nome da sua conta |
O que fica registrado

