Pular para o conteúdo
Fale conosco Entrar
ODSign

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:

  1. Enviar: criar o envelope com o PDF e o signatário e enviar (send=1), guardando o id.
  2. Receber: um endpoint que recebe os webhooks, confere o X-Signature, ignora repetições e responde rápido.
  3. Baixar: quando chega envelope.completed, baixar o documento assinado e conferir o SHA-256 com o final_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. Baixe signed (ou final) 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

Não encontrou o que procurava?

Nossa equipe ajuda você. Conte o que aconteceu e responderemos por e-mail.

Fale com o suporte