AssinAPI

Integrando com Claude Code

Cole o prompt abaixo no Claude Code dentro do seu repositório. Ele cria a camada assinapi.service, o webhook e os testes.

Secret key

Adicione ASSINAPI_SECRET_KEY ao .env do servidor (e ao .env.example sem valor).

Arquitetura

Seu backend (assinapi.service) → AssinAPI. Frontend conversa só com o seu backend.

Prompt pronto

markdown
Implemente a integração com a AssinAPI neste repositório. Analise a stack existente e siga as convenções do projeto.

## 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).
  • npm install @assinapi/sdk
typescript
import { AssinAPI } from '@assinapi/sdk';

const assinapi = new AssinAPI({ apiKey: process.env.ASSINAPI_SECRET_KEY });

const envelope = await assinapi.envelopes.create({ title: 'Contrato de prestação de serviços', expirationDays: 7 });
console.log(envelope.id, envelope.status); // env_… DRAFT

Enviar PDF

  • Anexa o PDF ao envelope. O SHA-256 é calculado pela AssinAPI.
  • npm install @assinapi/sdk
typescript
import { AssinAPI } from '@assinapi/sdk';

const assinapi = new AssinAPI({ apiKey: process.env.ASSINAPI_SECRET_KEY });

import { readFile } from 'node:fs/promises';

await assinapi.envelopes.addDocument('ENVELOPE_ID', { file: await readFile('contrato.pdf'), filename: 'contrato.pdf' });

Adicionar signatário

  • Adiciona um signatário (CPF obrigatório na assinatura avançada).
  • npm install @assinapi/sdk
typescript
import { AssinAPI } from '@assinapi/sdk';

const assinapi = new AssinAPI({ apiKey: process.env.ASSINAPI_SECRET_KEY });

await assinapi.envelopes.addSigner('ENVELOPE_ID', {
  name: 'João da Silva',
  email: 'joao@email.com',
  cpf: '529.982.247-25',
  authenticationMethod: 'EMAIL_OTP',
});

Enviar para assinatura

  • Envia para assinatura. A resposta traz os signingLinks.
  • npm install @assinapi/sdk
typescript
import { AssinAPI } from '@assinapi/sdk';

const assinapi = new AssinAPI({ apiKey: process.env.ASSINAPI_SECRET_KEY });

const sent = await assinapi.envelopes.send('ENVELOPE_ID');
for (const link of sent.signingLinks) console.log(link.name, link.signingUrl);

Consultar status

  • Consulta status do envelope e dos signatários.
  • npm install @assinapi/sdk
typescript
import { AssinAPI } from '@assinapi/sdk';

const assinapi = new AssinAPI({ apiKey: process.env.ASSINAPI_SECRET_KEY });

const envelope = await assinapi.envelopes.get('ENVELOPE_ID');
console.log(envelope.status, envelope.signers.map((s) => s.status));

Baixar documento assinado

  • Retorna URLs temporárias do PDF assinado, certificado e manifesto.
  • npm install @assinapi/sdk
typescript
import { AssinAPI } from '@assinapi/sdk';

const assinapi = new AssinAPI({ apiKey: process.env.ASSINAPI_SECRET_KEY });

const evidence = await assinapi.envelopes.evidence('ENVELOPE_ID');
console.log(evidence.downloads.documents[0].final); // URL temporária do PDF assinado
console.log(evidence.downloads.certificate);

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
import { AssinAPI } from '@assinapi/sdk';
const assinapi = new AssinAPI({ apiKey: process.env.ASSINAPI_SECRET_KEY });

// Next.js Route Handler: app/api/webhooks/assinapi/route.ts
export async function POST(req: Request) {
  const event = assinapi.webhooks.constructEvent({
    payload: await req.text(),
    headers: req.headers,
    secret: process.env.ASSINAPI_WEBHOOK_SECRET!,
  });
  if (event.type === 'envelope.completed') {
    // atualize o status no seu banco e baixe as evidências com assinapi.envelopes.evidence(...)
  }
  return new Response('ok');
}