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');
});