> ## Documentation Index
> Fetch the complete documentation index at: https://docs-platform.services-valid.com.br/llms.txt
> Use this file to discover all available pages before exploring further.

# Notificações via webhook

> Receba notificações de mudanças de estado do envelope (finalizado, cancelado, expirado) em tempo real.

Webhooks permitem que seu backend seja notificado automaticamente quando um envelope muda de estado, sem depender de polling contínuo.

## Como funcionar

Ao criar um envelope, você pode especificar uma URL de notificação:

```json theme={"theme":"catppuccin-latte"}
POST /v1/envelopes
{
  "title": "Contrato 2025",
  "signingMode": "sequential",
  "document": { /* ... */ },
  "signers": [ /* ... */ ],
  "urlNotification": "https://seu-backend.com/webhooks/envelope-status"
}
```

Quando o envelope muda de estado (finaliza, expira ou é cancelado), o serviço dispara um `POST` HTTP para essa URL com um payload JSON.

## Payload do webhook

O webhook é disparado com um `POST` contendo:

<ResponseField name="status" type="string" required>
  Estado do envelope: `FINISHED`, `CANCELLED`, ou `EXPIRED`.
</ResponseField>

<ResponseField name="envelopeUuid" type="string" required>
  UUID do envelope que mudou de estado.
</ResponseField>

### Exemplo

```json theme={"theme":"catppuccin-latte"}
POST https://seu-backend.com/webhooks/envelope-status
Content-Type: application/json

{
  "status": "FINISHED",
  "envelopeUuid": "550e8400-e29b-41d4-a716-446655440000"
}
```

## Estados e quando disparam

| Estado        | Evento                    | Descrição                                                                                                  |
| :------------ | :------------------------ | :--------------------------------------------------------------------------------------------------------- |
| **FINISHED**  | Último signatário assinou | O envelope foi finalizado. Todos os signatários completaram a assinatura.                                  |
| **CANCELLED** | Cancelamento explícito    | Você chamou `POST /v1/envelopes/:uuid/cancel` ou o envelope foi cancelado via admin.                       |
| **EXPIRED**   | Expiração da sessão       | Nenhum signatário acessou antes da `dueDate`, ou a sessão expirou durante o processo de assinatura/recusa. |

## Características importantes

* **Best-effort**: webhooks são disparados uma única vez. Se a requisição HTTP falhar (timeout, erro de servidor, DNS inválido), **não há retry automático**.
* **Sem idempotência garantida**: o mesmo webhook pode ser disparado mais de uma vez em raríssimos casos (ex: falha de rede seguida de retry interno). Implemente tratamento de duplicatas no seu backend se crítico.
* **Sem autenticação**: o webhook não inclui header de autenticação. Se precisar validar a origem, implemente uma estratégia: usar um token secreto na URL (`?token=secret`), validar o IP de origem, ou assinar o payload com HMAC.
* **Sem garantia de ordem**: se múltiplos eventos forem disparados rapidamente, a ordem de entrega não é garantida.
* **Timeout curto**: seu endpoint deve responder em até 30 segundos com status HTTP 2xx (200-299).

## Implementação recomendada

### 1. Receba o webhook

```javascript theme={"theme":"catppuccin-latte"}
// Express.js exemplo
app.post('/webhooks/envelope-status', (req, res) => {
  const { status, envelopeUuid } = req.body;
  
  // Responda imediatamente com 200 OK (antes de processar)
  res.status(200).json({ received: true });
  
  // Processe de forma assíncrona (não bloqueie a resposta)
  handleEnvelopeStatusChange(status, envelopeUuid)
    .catch(err => console.error('Erro ao processar webhook:', err));
});
```

**Nunca bloqueie a resposta do webhook.** Responda com 200 OK imediatamente e processe de forma assíncrona (fila de mensagens, background job, etc.).

### 2. Valide e processe

```javascript theme={"theme":"catppuccin-latte"}
async function handleEnvelopeStatusChange(status, envelopeUuid) {
  // Valide o estado
  if (!['FINISHED', 'CANCELLED', 'EXPIRED'].includes(status)) {
    console.warn('Status desconhecido:', status);
    return;
  }
  
  // Evite duplicatas: verifique se já foi processado
  const alreadyProcessed = await db.webhookLog.findOne({ envelopeUuid, status });
  if (alreadyProcessed) {
    console.log('Webhook já processado, ignorando');
    return;
  }
  
  // Registre no log antes de processar (para rastrear)
  await db.webhookLog.create({ envelopeUuid, status, receivedAt: new Date() });
  
  // Processe conforme o status
  switch (status) {
    case 'FINISHED':
      await sendConfirmationEmail(envelopeUuid);
      await updateContractStatus(envelopeUuid, 'SIGNED');
      break;
      
    case 'EXPIRED':
      await sendReminderEmail(envelopeUuid);
      await updateContractStatus(envelopeUuid, 'EXPIRED');
      break;
      
    case 'CANCELLED':
      await notifyAdmin(envelopeUuid);
      break;
  }
}
```

### 3. Fallback: polling

**Sempre implemente polling como fallback.** Webhooks são best-effort — não confie exclusivamente neles para fluxos críticos.

```javascript theme={"theme":"catppuccin-latte"}
// A cada 5 minutos, verifique envelopes pendentes
setInterval(async () => {
  const pendingEnvelopes = await db.envelope.find({ state: { $in: ['CREATED', 'AWAITING', 'SIGNING'] } });
  
  for (const envelope of pendingEnvelopes) {
    const current = await api.getEnvelope(envelope.uuid);
    
    // Se o estado mudou mas o webhook não foi recebido, processe agora
    if (current.state !== envelope.state) {
      await handleEnvelopeStatusChange(
        current.state.toUpperCase(),
        envelope.uuid
      );
      await db.envelope.updateOne({ uuid: envelope.uuid }, { state: current.state });
    }
  }
}, 5 * 60 * 1000);
```

## Teste seu webhook

### Localmente com ngrok

```bash theme={"theme":"catppuccin-latte"}
# Expose seu servidor local
ngrok http 3000

# Use a URL gerada em urlNotification
curl -X POST "https://signer.vcc-service.com/v1/envelopes" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Test",
    "document": { /* ... */ },
    "signers": [ /* ... */ ],
    "urlNotification": "https://abc123.ngrok.io/webhooks/envelope-status"
  }'
```

### Teste manual com curl

```bash theme={"theme":"catppuccin-latte"}
# Simule um webhook
curl -X POST "http://localhost:3000/webhooks/envelope-status" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "FINISHED",
    "envelopeUuid": "550e8400-e29b-41d4-a716-446655440000"
  }'
```

## Dicas de depuração

* **Verifique os logs**: Procure por chamadas HTTP POST em seus logs (`curl`, `wget`, ou cliente HTTP do servidor).
* **Webhook nunca chega?** Confirme que a `urlNotification` é uma URL pública e acessível (não localhost), que não há firewall bloqueando, e que seu endpoint retorna status 2xx.
* **Webhook duplicado?** Implemente um `envelopeUuid + status` como chave única na sua tabela de logs — descarte duplicatas.
* **Ordem incorreta?** Não assuma ordem de entrega. Se precisar de sequência (ex: FINISHED antes de processar), use timestamps (`createdAt` da mudança de estado) para ordenar.

## Relacionado

Confira também como listar e detalhar envelopes para um fallback robusto de polling:

* [`GET /v1/envelopes`](/plataforma/assinatura-digital/api/get-envelopes) — listar envelopes
* [`GET /v1/envelopes/:uuid`](/plataforma/assinatura-digital/api/get-envelope-by-id) — detalhe do envelope
