Exemplos de integração completos
Código pronto para criar e enviar envelopes, receber os webhooks com a assinatura conferida e baixar o documento assinado, em PHP (Laravel), Node.js e Python.
7 min de leitura Avançado Atualizado em 11 de novembro de 2026
Nesta página
Os três exemplos abaixo fazem a mesma integração, de ponta a ponta:
- Enviar: criar o envelope com o PDF e o signatário e enviar (
send=1), guardando oid. - Receber: um endpoint que recebe os webhooks, confere o
X-Signature, ignora repetições e responde rápido. - Baixar: quando chega
envelope.completed, baixar o documento assinado e conferir o SHA-256 com ofinal_sha256.
Em todos, o token da API fica em ODSIGN_TOKEN e o segredo do webhook em ODSIGN_WEBHOOK_SECRET, variáveis de
ambiente do servidor. Adapte a parte de banco de dados ao seu sistema.
Nota: Os exemplos usam
auth_method=email(token de 6 dígitos por e-mail na hora de assinar). Troque pela forma que fizer sentido para o seu documento (veja O que acontece depois do envio).
PHP (Laravel)
Enviar
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.odsign.token'))
->acceptJson()
->timeout(60)
->attach('files[]', file_get_contents(storage_path('contratos/123.pdf')), 'contrato-123.pdf')
->post('https://app.odsign.com.br/api/v1/envelopes', [
'subject' => 'Contrato 123 - Prestação de serviços',
'message' => 'Olá! Segue o contrato para assinatura.',
'recipients[0][name]' => $cliente->nome,
'recipients[0][email]' => $cliente->email,
'recipients[0][auth_method]' => 'email',
'send' => 1,
]);
if ($response->status() === 422) {
// Pendências (por exemplo, forma de autenticação indisponível): o envelope ficou como rascunho.
logger()->warning('ODSign recusou o envio', $response->json('errors'));
}
$response->throw(); // 401, 403, 429, 5xx
$contrato->update(['odsign_envelope_id' => $response->json('data.id'), 'status' => 'aguardando_assinatura']);
Em config/services.php, inclua 'odsign' => ['token' => env('ODSIGN_TOKEN'), 'webhook_secret' => env('ODSIGN_WEBHOOK_SECRET')].
Receber
// routes/api.php (fora da verificação de CSRF)
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
Route::post('/webhooks/odsign', function (Request $request) {
$expected = hash_hmac('sha256', $request->getContent(), config('services.odsign.webhook_secret'));
abort_unless(hash_equals($expected, (string) $request->header('X-Signature')), 401);
$event = $request->json()->all();
$key = 'odsign:'.$event['event'].':'.$event['envelope']['id'].':'.($event['recipient']['id'] ?? '-');
if (! Cache::add($key, true, now()->addDays(7))) {
return response()->noContent(); // repetição: já processado
}
ProcessarEventoODSign::dispatch($event); // fila: responde já e processa depois
return response()->noContent();
});
Baixar
use Illuminate\Support\Facades\Http;
class ProcessarEventoODSign implements \Illuminate\Contracts\Queue\ShouldQueue
{
use \Illuminate\Foundation\Queue\Queueable;
public function __construct(public array $event) {}
public function handle(): void
{
$contrato = Contrato::where('odsign_envelope_id', $this->event['envelope']['id'])->first();
if (! $contrato) {
return;
}
match ($this->event['event']) {
'recipient.signed' => $contrato->update(['ultima_assinatura_em' => now()]),
'envelope.declined', 'envelope.voided' => $contrato->update(['status' => 'cancelado']),
'envelope.completed' => $this->baixar($contrato),
default => null,
};
}
protected function baixar(Contrato $contrato): void
{
$id = $this->event['envelope']['id'];
$pdf = Http::withToken(config('services.odsign.token'))->timeout(120)
->get("https://app.odsign.com.br/api/v1/envelopes/{$id}/documents/signed")
->throw()
->body();
// Integridade: o hash do arquivo tem de ser o final_sha256 do envelope.
if (hash('sha256', $pdf) !== $this->event['envelope']['final_sha256']) {
throw new \RuntimeException("SHA-256 diferente no envelope {$id}");
}
Storage::put("contratos/{$contrato->id}-assinado.pdf", $pdf);
$contrato->update(['status' => 'assinado']);
}
}
Node.js (Express)
Node.js 18 ou mais novo (fetch, FormData e Blob nativos), como módulo ES.
Enviar
import { readFile } from 'node:fs/promises';
const BASE = 'https://app.odsign.com.br/api/v1';
const auth = { Authorization: `Bearer ${process.env.ODSIGN_TOKEN}`, Accept: 'application/json' };
export async function enviarContrato(cliente, caminhoPdf) {
const form = new FormData();
form.append('subject', 'Contrato 123 - Prestação de serviços');
form.append('recipients[0][name]', cliente.nome);
form.append('recipients[0][email]', cliente.email);
form.append('recipients[0][auth_method]', 'email');
form.append('send', '1');
form.append('files[]', new Blob([await readFile(caminhoPdf)], { type: 'application/pdf' }), 'contrato-123.pdf');
const res = await fetch(`${BASE}/envelopes`, { method: 'POST', headers: auth, body: form });
const body = await res.json();
if (res.status === 422) throw new Error(`Pendências: ${JSON.stringify(body.errors)}`); // ficou como rascunho
if (!res.ok) throw new Error(`${res.status}: ${body.message}`);
return body.data.id; // guarde junto do contrato
}
Receber e baixar
import crypto from 'node:crypto';
import { writeFile } from 'node:fs/promises';
import express from 'express';
const app = express();
const SECRET = process.env.ODSIGN_WEBHOOK_SECRET;
const processados = new Set(); // em produção, use o banco ou o Redis
app.post('/webhooks/odsign', express.raw({ type: 'application/json' }), (req, res) => {
const recebido = Buffer.from(req.get('X-Signature') || '', 'utf8');
const esperado = Buffer.from(crypto.createHmac('sha256', SECRET).update(req.body).digest('hex'), 'utf8');
if (recebido.length !== esperado.length || !crypto.timingSafeEqual(recebido, esperado)) {
return res.sendStatus(401);
}
const evento = JSON.parse(req.body.toString('utf8'));
const chave = `${evento.event}:${evento.envelope.id}:${evento.recipient?.id ?? '-'}`;
res.sendStatus(204); // responde já; processa depois
if (processados.has(chave)) return;
processados.add(chave);
if (evento.event === 'envelope.completed') {
baixarAssinado(evento.envelope).catch((err) => console.error('Falha ao baixar', err));
}
});
async function baixarAssinado({ id, final_sha256 }) {
const res = await fetch(`https://app.odsign.com.br/api/v1/envelopes/${id}/documents/signed`, {
headers: { Authorization: `Bearer ${process.env.ODSIGN_TOKEN}` },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const pdf = Buffer.from(await res.arrayBuffer());
const hash = crypto.createHash('sha256').update(pdf).digest('hex');
if (hash !== final_sha256) throw new Error(`SHA-256 diferente no envelope ${id}`);
await writeFile(`./assinados/${id}.pdf`, pdf);
}
app.listen(3000);
Python (Flask)
Enviar
import os
import requests
BASE = "https://app.odsign.com.br/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['ODSIGN_TOKEN']}", "Accept": "application/json"}
def enviar_contrato(cliente: dict, caminho_pdf: str) -> str:
with open(caminho_pdf, "rb") as pdf:
resp = requests.post(
f"{BASE}/envelopes",
headers=HEADERS,
data={
"subject": "Contrato 123 - Prestação de serviços",
"recipients[0][name]": cliente["nome"],
"recipients[0][email]": cliente["email"],
"recipients[0][auth_method]": "email",
"send": "1",
},
files=[("files[]", ("contrato-123.pdf", pdf, "application/pdf"))],
timeout=60,
)
if resp.status_code == 422:
raise RuntimeError(f"Pendências: {resp.json()['errors']}") # ficou como rascunho
resp.raise_for_status()
return resp.json()["data"]["id"] # guarde junto do contrato
Receber e baixar
import hashlib
import hmac
import os
import threading
import requests
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["ODSIGN_WEBHOOK_SECRET"].encode()
processados: set[str] = set() # em produção, use o banco ou o Redis
@app.post("/webhooks/odsign")
def webhook():
corpo = request.get_data()
esperado = hmac.new(SECRET, corpo, hashlib.sha256).hexdigest()
if not hmac.compare_digest(esperado, request.headers.get("X-Signature", "")):
abort(401)
evento = request.get_json()
chave = f"{evento['event']}:{evento['envelope']['id']}:{(evento.get('recipient') or {}).get('id', '-')}"
if chave not in processados:
processados.add(chave)
if evento["event"] == "envelope.completed":
# Em produção, prefira uma fila (Celery, RQ...).
threading.Thread(target=baixar_assinado, args=(evento["envelope"],)).start()
return "", 204
def baixar_assinado(envelope: dict) -> None:
resp = requests.get(
f"https://app.odsign.com.br/api/v1/envelopes/{envelope['id']}/documents/signed",
headers={"Authorization": f"Bearer {os.environ['ODSIGN_TOKEN']}"},
timeout=120,
)
resp.raise_for_status()
if hashlib.sha256(resp.content).hexdigest() != envelope["final_sha256"]:
raise RuntimeError(f"SHA-256 diferente no envelope {envelope['id']}")
with open(f"assinados/{envelope['id']}.pdf", "wb") as f:
f.write(resp.content)
Conferência periódica
Webhooks podem falhar se o seu servidor ficar fora do ar por mais tempo que as novas tentativas. Uma vez por hora (ou por dia), liste o que mudou e compare com o seu banco:
curl "https://app.odsign.com.br/api/v1/envelopes?status=completed,declined,voided,expired&from_date=2026-11-10&per_page=100" \
-H "Authorization: Bearer $ODSIGN_TOKEN" \
-H "Accept: application/json"
Para cada envelope cujo estado no seu sistema estiver diferente, aplique a mesma lógica do webhook (por exemplo, baixar o documento assinado).
Problemas comuns
- A assinatura do webhook nunca confere. O framework converteu o corpo antes do cálculo. Veja Validar a assinatura dos webhooks.
- O SHA-256 não bate com o
final_sha256. Baixesigned(oufinal) e calcule sobre os bytes do arquivo, sem convertê-lo para texto. - Envelopes duplicados. Uma nova tentativa do seu lado depois de um tempo esgotado pode criar outro envelope.
Antes de repetir um
POST /envelopes, confira se ele já não foi criado (veja Boas práticas).
Este artigo foi útil?
Artigos relacionados
- Início rápido da APIEm seis passos, crie o token, confira o acesso, crie um rascunho, envie para assinatura, receba o webhook e baixe o documento assinado.
- Validar a assinatura dos webhooksConfira o cabeçalho X-Signature (HMAC-SHA256 do corpo com o segredo do webhook) com exemplos em PHP, Node.js e Python e um validador no navegador.
- Boas práticas e checklist de produçãoSegurança do token, novas tentativas com espera progressiva, como evitar envelopes duplicados, webhooks confiáveis, testes sem ambiente separado e proteção de dados.
- Referência dos endpointsTodos os endpoints da API v1 do ODSign, com método, caminho, parâmetros, respostas e exemplos em curl.