Integrando com Next.js
Use Route Handlers ou Server Actions (server-side). Deploy na Vercel com a chave em Environment Variables.
Secret key
Vercel → Settings → Environment Variables → ASSINAPI_SECRET_KEY (sem NEXT_PUBLIC_).
Arquitetura
Client Component → Route Handler / Server Action → AssinAPI. Webhook → app/api/webhooks/assinapi/route.ts.
Prompt pronto
markdown
Implemente a integração com a AssinAPI neste projeto. Analise a stack existente e siga as convenções do código.
## Objetivo
Implementar: criação de envelope; upload de PDF; cadastro de signatário (nome, e-mail, CPF); envio para assinatura e armazenamento dos signingLinks; endpoint de webhook para envelope.completed (com validação HMAC); download do documento assinado e do certificado de evidências.
## Configuração
- Base URL: https://sandbox.api.assinapi.com.br/v1
- Autenticação: header `Authorization: Bearer ${ASSINAPI_SECRET_KEY}`
- Leia a chave SOMENTE da variável de ambiente ASSINAPI_SECRET_KEY no servidor.
- Leia o segredo do webhook de ASSINAPI_WEBHOOK_SECRET.
- Chaves ask_test_… = Sandbox (OTP de teste 123456, CPF de teste 529.982.247-25). Chaves ask_live_… = Produção.
## Regras de segurança (obrigatórias)
- NUNCA exponha ASSINAPI_SECRET_KEY no frontend, no navegador, em app mobile, em logs ou no repositório.
- Crie uma camada server-side chamada `assinapi.service` que concentra todas as chamadas à AssinAPI.
- O frontend conversa apenas com o seu backend.
- Não registre CPF, telefone, códigos OTP ou links de assinatura em logs.
## Endpoints
- POST https://sandbox.api.assinapi.com.br/v1/envelopes
body: { "title": string, "message"?: string, "signatureLevel"?: "SIMPLE"|"ADVANCED", "expirationDays"?: number, "externalId"?: string }
201 → { "id": "env_…", "status": "DRAFT", … }
- POST https://sandbox.api.assinapi.com.br/v1/envelopes/{envelopeId}/documents
multipart/form-data com o campo "file" (PDF) — ou JSON { "contentBase64": string, "filename": string }
201 → envelope com documents[].sha256
- POST https://sandbox.api.assinapi.com.br/v1/envelopes/{envelopeId}/signers
body: { "name": string, "email": string, "cpf": string, "phone"?: string, "authenticationMethod"?: "EMAIL_OTP"|"SMS_OTP", "signingOrder"?: number }
201 → { "id": "sgn_…", "status": "PENDING", "cpfMasked": "***.456.789-**" }
- POST https://sandbox.api.assinapi.com.br/v1/envelopes/{envelopeId}/send
body: { "notify"?: boolean } (notify=false → você envia o link por outro canal)
200 → { "status": "SENT", "signingLinks": [{ "signerId", "name", "signingUrl", "expiresAt" }] }
Os signingLinks só aparecem NESTA resposta: salve-os se precisar.
- GET https://sandbox.api.assinapi.com.br/v1/envelopes/{envelopeId}
200 → { "status": "DRAFT"|"SENT"|"IN_PROGRESS"|"COMPLETED"|"DECLINED"|"EXPIRED"|"CANCELLED", "signers": [{ "status" }] }
- GET https://sandbox.api.assinapi.com.br/v1/envelopes/{envelopeId}/evidence (somente após COMPLETED)
200 → { "downloads": { "documents": [{ "final": url, "original": url }], "certificate": url, "manifest": url }, "verifyUrl": string }
As URLs expiram em minutos: baixe e armazene no seu storage, não salve a URL.
Atalho (uma chamada só): POST https://sandbox.api.assinapi.com.br/v1/signature-requests
body: { "title": string, "document": { "filename": string, "contentBase64": string }, "signer": { "name", "email", "cpf" }, "authentication": "EMAIL_OTP" }
201 → { "id": "env_…", "status": "SENT", "signingUrl": string, "signers": [...] }
## Idempotência e retries
- Envie `Idempotency-Key: <uuid>` em todo POST. Em retry (timeout, 429, 5xx) reenvie com a MESMA chave.
- Em 429 respeite o header Retry-After e use backoff exponencial.
## Tratamento de erros
Erros têm o formato { "error": { "code", "message", "hint", "docs", "requestId" } }. Trate pelo `code` (estável), exiba `message` ao usuário quando fizer sentido e registre o `requestId` para suporte.
Códigos comuns: VALIDATION_FAILED, CPF_REQUIRED, INVALID_CPF, ENVELOPE_INCOMPLETE, INVALID_STATE_TRANSITION, INVALID_API_KEY, RATE_LIMITED.
## Webhook
- Registre o endpoint: POST https://sandbox.api.assinapi.com.br/v1/webhooks { "url": "https://…/api/webhooks/assinapi", "events": ["envelope.completed", "signer.signed"] } → guarde o "secret" retornado (exibido uma vez).
- Headers recebidos: X-Signature (v1=<hex>), X-Event-Id, X-Timestamp.
- Valide: HMAC_SHA256(ASSINAPI_WEBHOOK_SECRET, `${X-Event-Id}.${X-Timestamp}.${corpo_bruto}`) === X-Signature (comparação em tempo constante).
- Rejeite X-Timestamp com mais de 5 minutos; deduplique por X-Event-Id; responda 2xx rápido.
- Payload envelope.completed: { "id", "type", "data": { "envelope": { "id", "status", "externalId" }, "evidence": { "manifestHash", "verifyUrl" }, "documents": [...] } }
- Ao receber envelope.completed, atualize o status no banco e (se aplicável) baixe o PDF final via GET /envelopes/{id}/evidence.
## Entregáveis
- `assinapi.service` com funções tipadas para cada operação.
- Persistir envelopeId (e externalId) junto ao registro do seu sistema.
- Variáveis de ambiente documentadas (.env.example sem valores reais).
- Testes da camada de serviço com a API mockada.Criar envelope
- Cria um envelope (rascunho).
- Route Handler (server-side). Configure ASSINAPI_SECRET_KEY nas variáveis de ambiente da Vercel — sem o prefixo NEXT_PUBLIC_.
typescript
// app/api/assinapi/create-envelope/route.ts
export async function POST(request: Request) {
const res = await fetch('https://sandbox.api.assinapi.com.br/v1/envelopes', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.ASSINAPI_SECRET_KEY}`, '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 Response.json(data, { status: res.status });
}Enviar PDF
- Anexa o PDF ao envelope. O SHA-256 é calculado pela AssinAPI.
- Route Handler (server-side). Configure ASSINAPI_SECRET_KEY nas variáveis de ambiente da Vercel — sem o prefixo NEXT_PUBLIC_.
typescript
// app/api/assinapi/upload-pdf/route.ts
export async function POST(request: Request) {
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 ${process.env.ASSINAPI_SECRET_KEY}` },
body: form,
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message} (requestId ${data.error.requestId})`);
return Response.json(data, { status: res.status });
}Adicionar signatário
- Adiciona um signatário (CPF obrigatório na assinatura avançada).
- Route Handler (server-side). Configure ASSINAPI_SECRET_KEY nas variáveis de ambiente da Vercel — sem o prefixo NEXT_PUBLIC_.
typescript
// app/api/assinapi/add-signer/route.ts
export async function POST(request: Request) {
const res = await fetch('https://sandbox.api.assinapi.com.br/v1/envelopes/ENVELOPE_ID/signers', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.ASSINAPI_SECRET_KEY}`, '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 Response.json(data, { status: res.status });
}Enviar para assinatura
- Envia para assinatura. A resposta traz os signingLinks.
- Route Handler (server-side). Configure ASSINAPI_SECRET_KEY nas variáveis de ambiente da Vercel — sem o prefixo NEXT_PUBLIC_.
typescript
// app/api/assinapi/send/route.ts
export async function POST(request: Request) {
const res = await fetch('https://sandbox.api.assinapi.com.br/v1/envelopes/ENVELOPE_ID/send', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.ASSINAPI_SECRET_KEY}`, '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 Response.json(data, { status: res.status });
}Consultar status
- Consulta status do envelope e dos signatários.
- Route Handler (server-side). Configure ASSINAPI_SECRET_KEY nas variáveis de ambiente da Vercel — sem o prefixo NEXT_PUBLIC_.
typescript
// app/api/assinapi/status/route.ts
export async function POST(request: Request) {
const res = await fetch('https://sandbox.api.assinapi.com.br/v1/envelopes/ENVELOPE_ID', {
method: 'GET',
headers: { Authorization: `Bearer ${process.env.ASSINAPI_SECRET_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message} (requestId ${data.error.requestId})`);
return Response.json(data, { status: res.status });
}Baixar documento assinado
- Retorna URLs temporárias do PDF assinado, certificado e manifesto.
- Route Handler (server-side). Configure ASSINAPI_SECRET_KEY nas variáveis de ambiente da Vercel — sem o prefixo NEXT_PUBLIC_.
typescript
// app/api/assinapi/download/route.ts
export async function POST(request: Request) {
const res = await fetch('https://sandbox.api.assinapi.com.br/v1/envelopes/ENVELOPE_ID/evidence', {
method: 'GET',
headers: { Authorization: `Bearer ${process.env.ASSINAPI_SECRET_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message} (requestId ${data.error.requestId})`);
return Response.json(data, { status: res.status });
}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.
typescript
// 1) Registrar endpoint
/*
curl -X POST "https://sandbox.api.assinapi.com.br/v1/webhooks" \
-H "Authorization: Bearer $ASSINAPI_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://seu-sistema.com/api/webhooks/assinapi", "events": ["envelope.completed", "signer.signed"] }'
# Guarde o 4 (whsec_…) retornado em ASSINAPI_WEBHOOK_SECRET
*/
// 2) Validar cada requisição recebida
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyAssinapiWebhook(rawBody: string, headers: Headers, secret = process.env.ASSINAPI_WEBHOOK_SECRET!) {
const signature = headers.get('x-signature') ?? '';
const eventId = headers.get('x-event-id') ?? '';
const timestamp = Number(headers.get('x-timestamp'));
if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = 'v1=' + createHmac('sha256', secret).update(`${eventId}.${timestamp}.${rawBody}`).digest('hex');
return signature.length === expected.length && timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}