AssinAPI

Integrando com Supabase

Use Edge Functions (Deno) para chamar a AssinAPI e receber webhooks.

Secret key

supabase secrets set ASSINAPI_SECRET_KEY=ask_test_… ASSINAPI_WEBHOOK_SECRET=whsec_…

Arquitetura

Cliente → Edge Function assinapi-* → AssinAPI. Webhook → Edge Function assinapi-webhook (verify_jwt=false, HMAC) → Postgres/Storage.

Criar envelope

  • Cria um envelope (rascunho).
  • supabase secrets set ASSINAPI_SECRET_KEY=ask_test_…
typescript
// supabase/functions/assinapi-create-envelope/index.ts
Deno.serve(async (req) => {
  const secret = Deno.env.get('ASSINAPI_SECRET_KEY')!;
  const res = await fetch('https://sandbox.api.assinapi.com.br/v1/envelopes', {
    method: 'POST',
    headers: { Authorization: `Bearer ${secret}`, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID() },
    body: JSON.stringify({
      "title": "Contrato de prestação de serviços",
      "expirationDays": 7
    }),
  });
  const data = await res.json();
  if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message} (requestId ${data.error.requestId})`);
  return new Response(JSON.stringify(data), { status: res.status, headers: { 'Content-Type': 'application/json' } });
});

Enviar PDF

  • Anexa o PDF ao envelope. O SHA-256 é calculado pela AssinAPI.
  • supabase secrets set ASSINAPI_SECRET_KEY=ask_test_…
typescript
// supabase/functions/assinapi-upload-pdf/index.ts
Deno.serve(async (req) => {
  const secret = Deno.env.get('ASSINAPI_SECRET_KEY')!;
  const form = new FormData();
  form.append('file', await req.blob(), 'contrato.pdf');
  const res = await fetch('https://sandbox.api.assinapi.com.br/v1/envelopes/ENVELOPE_ID/documents', {
    method: 'POST',
    headers: { Authorization: `Bearer ${secret}` },
    body: form,
  });
  const data = await res.json();
  if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message} (requestId ${data.error.requestId})`);
  return new Response(JSON.stringify(data), { status: res.status, headers: { 'Content-Type': 'application/json' } });
});

Adicionar signatário

  • Adiciona um signatário (CPF obrigatório na assinatura avançada).
  • supabase secrets set ASSINAPI_SECRET_KEY=ask_test_…
typescript
// supabase/functions/assinapi-add-signer/index.ts
Deno.serve(async (req) => {
  const secret = Deno.env.get('ASSINAPI_SECRET_KEY')!;
  const res = await fetch('https://sandbox.api.assinapi.com.br/v1/envelopes/ENVELOPE_ID/signers', {
    method: 'POST',
    headers: { Authorization: `Bearer ${secret}`, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID() },
    body: JSON.stringify({
      "name": "João da Silva",
      "email": "joao@email.com",
      "cpf": "529.982.247-25",
      "authenticationMethod": "EMAIL_OTP"
    }),
  });
  const data = await res.json();
  if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message} (requestId ${data.error.requestId})`);
  return new Response(JSON.stringify(data), { status: res.status, headers: { 'Content-Type': 'application/json' } });
});

Enviar para assinatura

  • Envia para assinatura. A resposta traz os signingLinks.
  • supabase secrets set ASSINAPI_SECRET_KEY=ask_test_…
typescript
// supabase/functions/assinapi-send/index.ts
Deno.serve(async (req) => {
  const secret = Deno.env.get('ASSINAPI_SECRET_KEY')!;
  const res = await fetch('https://sandbox.api.assinapi.com.br/v1/envelopes/ENVELOPE_ID/send', {
    method: 'POST',
    headers: { Authorization: `Bearer ${secret}`, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID() },
    body: JSON.stringify({
      "notify": true
    }),
  });
  const data = await res.json();
  if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message} (requestId ${data.error.requestId})`);
  return new Response(JSON.stringify(data), { status: res.status, headers: { 'Content-Type': 'application/json' } });
});

Consultar status

  • Consulta status do envelope e dos signatários.
  • supabase secrets set ASSINAPI_SECRET_KEY=ask_test_…
typescript
// supabase/functions/assinapi-status/index.ts
Deno.serve(async (req) => {
  const secret = Deno.env.get('ASSINAPI_SECRET_KEY')!;
  const res = await fetch('https://sandbox.api.assinapi.com.br/v1/envelopes/ENVELOPE_ID', {
    method: 'GET',
    headers: { Authorization: `Bearer ${secret}` },
  });
  const data = await res.json();
  if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message} (requestId ${data.error.requestId})`);
  return new Response(JSON.stringify(data), { status: res.status, headers: { 'Content-Type': 'application/json' } });
});

Baixar documento assinado

  • Retorna URLs temporárias do PDF assinado, certificado e manifesto.
  • supabase secrets set ASSINAPI_SECRET_KEY=ask_test_…
typescript
// supabase/functions/assinapi-download/index.ts
Deno.serve(async (req) => {
  const secret = Deno.env.get('ASSINAPI_SECRET_KEY')!;
  const res = await fetch('https://sandbox.api.assinapi.com.br/v1/envelopes/ENVELOPE_ID/evidence', {
    method: 'GET',
    headers: { Authorization: `Bearer ${secret}` },
  });
  const data = await res.json();
  if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message} (requestId ${data.error.requestId})`);
  return new Response(JSON.stringify(data), { status: res.status, headers: { 'Content-Type': 'application/json' } });
});

Receber webhooks

  • Valide SEMPRE a assinatura HMAC (X-Signature) usando o corpo bruto.
  • Rejeite eventos com X-Timestamp com mais de 5 minutos e deduplique por X-Event-Id.
  • Responda 2xx rapidamente; processe de forma assíncrona. Falhas são reenviadas com backoff exponencial.
  • Crie a função com verify_jwt = false (o webhook usa HMAC, não JWT do Supabase).
typescript
// supabase/functions/assinapi-webhook/index.ts
Deno.serve(async (req) => {
  const raw = await req.text();
  const secret = Deno.env.get('ASSINAPI_WEBHOOK_SECRET')!;
  const eventId = req.headers.get('x-event-id') ?? '';
  const ts = Number(req.headers.get('x-timestamp'));
  if (!ts || Math.abs(Date.now() / 1000 - ts) > 300) return new Response('stale', { status: 400 });
  const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
  const mac = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(`${eventId}.${ts}.${raw}`));
  const expected = 'v1=' + [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, '0')).join('');
  if (expected !== req.headers.get('x-signature')) return new Response('invalid signature', { status: 401 });
  const event = JSON.parse(raw);
  // ex.: atualizar tabela contracts onde assinapi_envelope_id = event.data.envelope.id
  return new Response('ok');
});