Guia 6 de 8
Assinatura eletrônica pela API
Seu sistema gera o contrato; a API manda o convite por e-mail, cada pessoa confirma um código e assina, e você recebe o PDF assinado com o relatório de evidências.
O que você vai construir
Um fluxo completo: criar o envelope com o PDF, um cliente e uma testemunha, posições das assinaturas e webhook; acompanhar; baixar o PDF final e o relatório quando todos assinarem.
Antes de começar
- Uma chave de API em
CONVERTAJA_KEY(veja Primeiros passos). - Um PDF sem senha, de até 20 MB e 100 páginas.
- Um endpoint de webhook (opcional, mas recomendado; veja Webhooks).
Antes de começar: custo e cota
- Créditos: 20 créditos por envelope enviado (até 10 signatários). Rascunho, consultas, lembretes, cancelamento e downloads são grátis. Se o envio falhar, os créditos voltam; cancelar um envelope já enviado não devolve.
- Cota mensal: quem tem um plano pago da API é limitado só pelos créditos. Sem plano pago da API, vale a cota de documentos do plano do site (Grátis 3, Pro 30, Empresas 200 por mês); acima dela,
402 esign_quota, sem cobrança. - Chaves de teste não gastam créditos, mas os e-mails são enviados de verdade e o envio conta na cota.
{
"error": {
"code": "esign_quota",
"message": "Você já enviou 3 de 3 documentos para assinatura este mês no plano Grátis. Assine um plano maior ou aguarde o próximo mês.",
"quota_limit": 3,
"quota_used": 3,
"request_id": "req_UkbrW3QRi59d"
}
}Passo 1: Defina quem assina e onde
Cada signatário tem name (nome completo), email e, opcionalmente, role (sign, approve ou witness), cpf (a pessoa precisa digitar o mesmo), ask_cpf e rubrica_all (rubrica em todas as páginas).
Os campos (fields) dizem onde cada pessoa assina, em frações da página (0 a 1) a partir do canto superior esquerdo, com page começando em 0. Tipos: signature, initials, date, name, cpf. Sem campos, as assinaturas aparecem só no relatório.
{ "signer": 0, "type": "signature", "page": 0, "x": 0.1, "y": 0.75, "w": 0.3, "h": 0.07 }
// pessoa 0, primeira página, começa a 10% da esquerda e 75% do topo, 30% de largura, 7% de alturaPasso 2: Crie e envie o envelope
Envie o PDF no campo file e o resto em data (JSON). Por padrão os convites saem na hora (send: true) e, com sequential: true (padrão), uma pessoa por vez, na ordem da lista:
curl -X POST "https://convertaja.com.br/v1/esign/envelopes" \
-H "Authorization: Bearer $CONVERTAJA_KEY" \
-F "file=@contrato.pdf" \
-F 'data={
"name": "Contrato de prestação de serviços",
"message": "Olá! Segue o contrato para assinatura.",
"signers": [
{ "name": "Ana Souza", "email": "ana@cliente.com.br" },
{ "name": "Bruno Lima", "email": "bruno@empresa.com.br", "role": "witness" }
],
"fields": [
{ "signer": 0, "type": "signature", "page": 0, "x": 0.1, "y": 0.75, "w": 0.3, "h": 0.07 },
{ "signer": 1, "type": "signature", "page": 0, "x": 0.55, "y": 0.75, "w": 0.3, "h": 0.07 }
],
"webhook_url": "https://seusite.com.br/webhooks/convertaja"
}'// enviar-contrato.mjs: sobe o PDF, cadastra quem assina e envia os convites
import { openAsBlob } from 'node:fs';
const form = new FormData();
form.append('file', await openAsBlob('contrato.pdf'), 'contrato.pdf');
form.append('data', JSON.stringify({
name: 'Contrato de prestação de serviços',
message: 'Olá! Segue o contrato para assinatura.',
signers: [
{ name: 'Ana Souza', email: 'ana@cliente.com.br' },
{ name: 'Bruno Lima', email: 'bruno@empresa.com.br', role: 'witness' },
],
fields: [ // onde cada pessoa assina (frações da página, a partir do canto superior esquerdo)
{ signer: 0, type: 'signature', page: 0, x: 0.1, y: 0.75, w: 0.3, h: 0.07 },
{ signer: 1, type: 'signature', page: 0, x: 0.55, y: 0.75, w: 0.3, h: 0.07 },
],
webhook_url: 'https://seusite.com.br/webhooks/convertaja',
}));
const res = await fetch('https://convertaja.com.br/v1/esign/envelopes', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.CONVERTAJA_KEY}` },
body: form,
});
const envelope = await res.json();
if (!res.ok) throw new Error(`${envelope.error.code}: ${envelope.error.message}`);
console.log(`Envelope ${envelope.id}: ${envelope.status_label} (${envelope.credits_charged} créditos)`);# enviar_contrato.py: sobe o PDF, cadastra quem assina e envia os convites
import json
import os
import requests
dados = {
"name": "Contrato de prestação de serviços",
"message": "Olá! Segue o contrato para assinatura.",
"signers": [
{"name": "Ana Souza", "email": "ana@cliente.com.br"},
{"name": "Bruno Lima", "email": "bruno@empresa.com.br", "role": "witness"},
],
"fields": [ # onde cada pessoa assina (frações da página, a partir do canto superior esquerdo)
{"signer": 0, "type": "signature", "page": 0, "x": 0.1, "y": 0.75, "w": 0.3, "h": 0.07},
{"signer": 1, "type": "signature", "page": 0, "x": 0.55, "y": 0.75, "w": 0.3, "h": 0.07},
],
"webhook_url": "https://seusite.com.br/webhooks/convertaja",
}
with open("contrato.pdf", "rb") as f:
res = requests.post(
"https://convertaja.com.br/v1/esign/envelopes",
headers={"Authorization": f"Bearer {os.environ['CONVERTAJA_KEY']}"},
files={"file": ("contrato.pdf", f, "application/pdf")},
data={"data": json.dumps(dados)},
timeout=60,
)
envelope = res.json()
if not res.ok:
raise SystemExit(f"{envelope['error']['code']}: {envelope['error']['message']}")
print(f"Envelope {envelope['id']}: {envelope['status_label']} ({envelope['credits_charged']} créditos)")<?php
// enviar-contrato.php: sobe o PDF, cadastra quem assina e envia os convites
$dados = [
'name' => 'Contrato de prestação de serviços',
'message' => 'Olá! Segue o contrato para assinatura.',
'signers' => [
['name' => 'Ana Souza', 'email' => 'ana@cliente.com.br'],
['name' => 'Bruno Lima', 'email' => 'bruno@empresa.com.br', 'role' => 'witness'],
],
'fields' => [ // onde cada pessoa assina (frações da página, a partir do canto superior esquerdo)
['signer' => 0, 'type' => 'signature', 'page' => 0, 'x' => 0.1, 'y' => 0.75, 'w' => 0.3, 'h' => 0.07],
['signer' => 1, 'type' => 'signature', 'page' => 0, 'x' => 0.55, 'y' => 0.75, 'w' => 0.3, 'h' => 0.07],
],
'webhook_url' => 'https://seusite.com.br/webhooks/convertaja',
];
$ch = curl_init('https://convertaja.com.br/v1/esign/envelopes');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('CONVERTAJA_KEY')],
CURLOPT_POSTFIELDS => [
'file' => new CURLFile('contrato.pdf', 'application/pdf'),
'data' => json_encode($dados),
],
]);
$envelope = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) !== 201) {
exit("{$envelope['error']['code']}: {$envelope['error']['message']}\n");
}
echo "Envelope {$envelope['id']}: {$envelope['status_label']} ({$envelope['credits_charged']} créditos)\n";
A resposta é 201 com o envelope. A primeira pessoa fica pending (convidada) e a testemunha, waiting (aguardando a vez):
{
"id": "JCGM-BEMM-8SYT-2JFL",
"object": "envelope",
"name": "Contrato de prestação de serviços",
"status": "pending",
"status_label": "Aguardando",
"pages": 1,
"signed_count": 0,
"signer_count": 2,
"sent_at": "2026-10-16T20:26:30.228Z",
"expires_at": "2026-11-15T20:26:30.222Z",
"original_sha256": "081010e21aaf012336eff1e0171f41f268c7180391d01809c71c07a01972538e",
"signers": [
{
"id": 1,
"position": 0,
"name": "Ana Souza",
"email": "ana@cliente.com.br",
"role": "sign",
"status": "pending",
"notified_at": "2026-10-16T20:26:30.230Z",
"signed_at": null
},
{
"id": 2,
"position": 1,
"name": "Bruno Lima",
"email": "bruno@empresa.com.br",
"role": "witness",
"status": "waiting",
"notified_at": null,
"signed_at": null
}
],
"files": null,
"credits_charged": 20,
"webhook_url": "https://seusite.com.br/webhooks/convertaja",
"url": "https://convertaja.com.br/v1/esign/envelopes/JCGM-BEMM-8SYT-2JFL"
}Outros campos de data: message (texto do convite), expires_at (prazo, de 1 hora a 90 dias; padrão 30 dias) e return_links: true, que devolve signers[].sign_url para você mandar o link pelo seu próprio canal (WhatsApp, área do cliente). Prefere JSON? Mande "file": { "base64": "…", "name": "contrato.pdf" } ou { "url": "https://…" } no mesmo corpo.
Variação: rascunho primeiro, envio depois
Com send: false, o envelope fica como rascunho (draft), grátis, até você chamar POST …/send:
# 1) Cria o rascunho (grátis, nada é enviado ainda)
curl -X POST "https://convertaja.com.br/v1/esign/envelopes" \
-H "Authorization: Bearer $CONVERTAJA_KEY" \
-F "file=@proposta.pdf" \
-F 'data={"name":"Proposta comercial","signers":[{"name":"Carla Dias","email":"carla@cliente.com.br"}],"send":false}'
# 2) Quando estiver tudo certo, envia (cobra 20 créditos)
curl -X POST "https://convertaja.com.br/v1/esign/envelopes/74W7-E8AV-X86B-67GZ/send" \
-H "Authorization: Bearer $CONVERTAJA_KEY"
Passo 3: Acompanhe a assinatura
O ideal é esperar o webhook (próximo passo). Para consultar a qualquer momento:
curl "https://convertaja.com.br/v1/esign/envelopes/JCGM-BEMM-8SYT-2JFL" \
-H "Authorization: Bearer $CONVERTAJA_KEY"
# Com a trilha de auditoria (quem abriu, confirmou o código, assinou; IP, porta e dispositivo)
curl "https://convertaja.com.br/v1/esign/envelopes/JCGM-BEMM-8SYT-2JFL?events=true" \
-H "Authorization: Bearer $CONVERTAJA_KEY"
Situações do envelope: draft → pending → completed, ou refused (alguém recusou), expired (passou o prazo) e canceled. Cada signatário tem notified_at, viewed_at, verified_at (confirmou o código do e-mail) e signed_at.
Passo 4: Receba o aviso de conclusão
Com webhook_url, avisamos uma vez quando o envelope chega a um estado final: esign.envelope.completed, refused, expired ou canceled. Conferimos a situação a cada 30 segundos, então o aviso pode levar até meio minuto. A assinatura X-Convertaja-Signature é a mesma das tarefas: use o código do guia Webhooks sem mudar nada.
{
"id": "evt_03885356524f1746b2ed",
"type": "esign.envelope.completed",
"created_at": "2026-10-16T20:26:46.869Z",
"data": {
"id": "JCGM-BEMM-8SYT-2JFL",
"object": "envelope",
"name": "Contrato de prestação de serviços",
"file_name": "contrato.pdf",
"status": "completed",
"status_label": "Assinado",
"source": "api",
"sequential": true,
"message": "Olá! Segue o contrato para assinatura.",
"pages": 1,
"signed_count": 2,
"signer_count": 2,
"created_at": "2026-10-16T20:26:30.222Z",
"updated_at": "2026-10-16T20:26:44.036Z",
"sent_at": "2026-10-16T20:26:30.228Z",
"completed_at": "2026-10-16T20:26:43.891Z",
"closed_at": "2026-10-16T20:26:43.891Z",
"expires_at": "2026-11-15T20:26:30.222Z",
"purge_at": "2027-01-14T20:26:43.891Z",
"original_sha256": "081010e21aaf012336eff1e0171f41f268c7180391d01809c71c07a01972538e",
"final_sha256": "9d0b6a1db020d48f1a2da2b06622b1d7dab0f9942ba9b091dc2dafc15f8a13a0",
"verify_url": "https://convertaja.com.br/verificar/JCGM-BEMM-8SYT-2JFL",
"signers": [
{
"id": 1,
"position": 0,
"name": "Ana Souza",
"email": "ana@cliente.com.br",
"role": "sign",
"status": "signed",
"ask_cpf": false,
"cpf_masked": null,
"rubrica_all": false,
"notified_at": "2026-10-16T20:26:30.230Z",
"viewed_at": "2026-10-16T20:26:41.554Z",
"verified_at": "2026-10-16T20:26:41.929Z",
"signed_at": "2026-10-16T20:26:41.950Z",
"refused_at": null,
"refuse_reason": null
},
{
"id": 2,
"position": 1,
"name": "Bruno Lima",
"email": "bruno@empresa.com.br",
"role": "witness",
"status": "signed",
"ask_cpf": false,
"cpf_masked": null,
"rubrica_all": false,
"notified_at": "2026-10-16T20:26:41.951Z",
"viewed_at": "2026-10-16T20:26:43.517Z",
"verified_at": "2026-10-16T20:26:43.870Z",
"signed_at": "2026-10-16T20:26:43.891Z",
"refused_at": null,
"refuse_reason": null
}
],
"fields": [
{
"type": "signature",
"signer": 0,
"page": 0,
"x": 0.1,
"y": 0.75,
"w": 0.3,
"h": 0.07
},
{
"type": "signature",
"signer": 1,
"page": 0,
"x": 0.55,
"y": 0.75,
"w": 0.3,
"h": 0.07
}
],
"files": {
"final": "https://convertaja.com.br/v1/esign/envelopes/JCGM-BEMM-8SYT-2JFL/files/final",
"audit": "https://convertaja.com.br/v1/esign/envelopes/JCGM-BEMM-8SYT-2JFL/files/audit"
},
"credits_charged": 20,
"webhook_url": "https://seusite.com.br/webhooks/convertaja",
"url": "https://convertaja.com.br/v1/esign/envelopes/JCGM-BEMM-8SYT-2JFL"
}
}Passo 5: Baixe o PDF assinado e o relatório
Com o envelope em completed, files.final é o PDF com as assinaturas aplicadas e files.audit é o relatório de assinaturas (IP e porta, dispositivo, horários, hashes SHA-256 e QR Code de verificação). Antes disso, a API responde 409 envelope_not_completed.
# PDF assinado
curl -f -o contrato-assinado.pdf "https://convertaja.com.br/v1/esign/envelopes/JCGM-BEMM-8SYT-2JFL/files/final" \
-H "Authorization: Bearer $CONVERTAJA_KEY"
# Relatório de assinaturas (evidências)
curl -f -o contrato-relatorio.pdf "https://convertaja.com.br/v1/esign/envelopes/JCGM-BEMM-8SYT-2JFL/files/audit" \
-H "Authorization: Bearer $CONVERTAJA_KEY"import { writeFile } from 'node:fs/promises';
const BASE = 'https://convertaja.com.br/v1';
const headers = { Authorization: `Bearer ${process.env.CONVERTAJA_KEY}` };
const id = process.argv[2]; // ex.: JCGM-BEMM-8SYT-2JFL
const envelope = await (await fetch(`${BASE}/esign/envelopes/${id}`, { headers })).json();
if (envelope.status !== 'completed') {
console.log(`Ainda não: ${envelope.status_label} (${envelope.signed_count} de ${envelope.signer_count} assinaram)`);
process.exit(0);
}
for (const [tipo, url] of Object.entries(envelope.files)) { // { final, audit }
const res = await fetch(url, { headers });
await writeFile(`${id}-${tipo}.pdf`, Buffer.from(await res.arrayBuffer()));
console.log(`Salvo: ${id}-${tipo}.pdf`);
}
console.log(`Hash do PDF final: ${envelope.final_sha256} · conferência pública: ${envelope.verify_url}`);import os
import sys
import requests
BASE = "https://convertaja.com.br/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['CONVERTAJA_KEY']}"}
env_id = sys.argv[1] # ex.: JCGM-BEMM-8SYT-2JFL
envelope = requests.get(f"{BASE}/esign/envelopes/{env_id}", headers=HEADERS, timeout=30).json()
if envelope["status"] != "completed":
raise SystemExit(f"Ainda não: {envelope['status_label']} ({envelope['signed_count']} de {envelope['signer_count']} assinaram)")
for tipo, url in envelope["files"].items(): # final e audit
res = requests.get(url, headers=HEADERS, timeout=60)
res.raise_for_status()
with open(f"{env_id}-{tipo}.pdf", "wb") as f:
f.write(res.content)
print(f"Salvo: {env_id}-{tipo}.pdf")
print(f"Hash do PDF final: {envelope['final_sha256']} · conferência pública: {envelope['verify_url']}")<?php
const BASE = 'https://convertaja.com.br/v1';
$id = $argv[1]; // ex.: JCGM-BEMM-8SYT-2JFL
$auth = ['Authorization: Bearer ' . getenv('CONVERTAJA_KEY')];
$ch = curl_init(BASE . "/esign/envelopes/$id");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $auth]);
$envelope = json_decode(curl_exec($ch), true);
if ($envelope['status'] !== 'completed') {
exit("Ainda não: {$envelope['status_label']} ({$envelope['signed_count']} de {$envelope['signer_count']} assinaram)\n");
}
foreach ($envelope['files'] as $tipo => $url) { // final e audit
$fp = fopen("$id-$tipo.pdf", 'wb');
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_FILE => $fp, CURLOPT_HTTPHEADER => $auth]);
curl_exec($ch);
fclose($fp);
echo "Salvo: $id-$tipo.pdf\n";
}
Qualquer pessoa pode conferir a autenticidade em verify_url. Os arquivos ficam guardados por 90 dias depois da conclusão (purge_at): copie para o seu armazenamento.
Lembrar, cancelar e listar
# Lembrar quem ainda não assinou (no máximo 1 vez a cada 30 minutos)
curl -X POST "https://convertaja.com.br/v1/esign/envelopes/74W7-E8AV-X86B-67GZ/remind" \
-H "Authorization: Bearer $CONVERTAJA_KEY"
# Cancelar (os links deixam de funcionar; os créditos de um envelope enviado não voltam)
curl -X POST "https://convertaja.com.br/v1/esign/envelopes/74W7-E8AV-X86B-67GZ/cancel" \
-H "Authorization: Bearer $CONVERTAJA_KEY"
# Listar os envelopes concluídos (20 por página; use next_cursor para a próxima)
curl "https://convertaja.com.br/v1/esign/envelopes?status=completed&limit=20" \
-H "Authorization: Bearer $CONVERTAJA_KEY"
{
"id": "74W7-E8AV-X86B-67GZ",
"reminded": 1
}{
"error": {
"code": "reminder_too_soon",
"message": "Você já enviou um lembrete há pouco. Tente de novo em 30 minutos.",
"request_id": "req_UkbrW3QRi59d"
}
}Para excluir de vez: DELETE /v1/esign/envelopes/{id} (ou POST …/delete).
Erros e o que fazer
| HTTP | code | Quando |
|---|---|---|
| 400 | invalid_request | Dado de signatário ou campo inválido. error.param diz qual (ex.: signer_name, signer_email, fields). |
| 400 | invalid_webhook_url | URL sem HTTPS, de rede interna ou com domínio que não existe. |
| 402 | esign_quota / insufficient_credits | Cota do mês ou créditos esgotados. Nada é cobrado. |
| 404 | envelope_not_found | Id errado, excluído ou de outra conta. |
| 409 | invalid_state | Ação incompatível (ex.: enviar de novo um envelope já enviado). |
| 413 / 422 | file_too_large / invalid_pdf | PDF acima de 20 MB, com senha, corrompido ou com mais de 100 páginas. |
{
"error": {
"code": "invalid_request",
"message": "Digite o nome da pessoa 1.",
"param": "signer_name",
"request_id": "req_UkbrW3QRi59d"
}
}Base legal: assinatura eletrônica nos termos da MP 2.200-2/2001, art. 10, § 2º, e da Lei 14.063/2020. Não é assinatura qualificada com certificado ICP-Brasil.
Próximos passos
Todos os endpoints e opções: referência da API. Dúvidas? Fale com a gente e informe o X-Request-Id da chamada.