> ## Documentation Index
> Fetch the complete documentation index at: https://scaleup-c34c4386.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Delivery

> Mecanismo de entrega, retry automático e verificação de assinatura HMAC

A Chargefy utiliza um sistema robusto de entrega de webhooks com retry automático, assinatura HMAC e monitoramento de entregas.

## Mecanismo de Entrega

Webhooks são processados de forma assíncrona via fila Postgres (`pgmq`)
disparada por `pg_cron`:

1. Um evento ocorre na plataforma (ex: pagamento confirmado)
2. O evento é enfileirado em `pgmq` (queue `webhook_delivery`)
3. Uma Edge Function consome a fila e envia o `POST` para cada endpoint inscrito
4. Se a entrega falhar, o retry automático é acionado

```
Evento → pgmq (Postgres) → Edge Function → POST https://seu-endpoint
                                  ↓
                             Falhou? → Retry com backoff exponencial
```

### Configurações de Entrega

| Parâmetro | Valor |
| - | - |
| Timeout por tentativa | 20 segundos |
| Máximo de retries | 10 tentativas |
| Backoff | Exponencial |
| Retenção de payload | 30 dias |

> [!NOTE]
>
> Após 30 dias, o payload do evento é removido automaticamente (archival), mas os metadados da entrega (status, data, HTTP code) são mantidos.

## Retry Automático

Quando seu endpoint retorna um status diferente de `2xx` ou não responde dentro de 20 segundos, a Chargefy automaticamente reagenda a entrega com backoff exponencial.

| Tentativa | Delay aproximado |
| - | - |
| 1ª | Imediato |
| 2ª | \~1 minuto |
| 3ª | \~5 minutos |
| 4ª | \~30 minutos |
| 5ª | \~2 horas |
| 6ª | \~8 horas |
| 7ª-10ª | Intervalos crescentes |

Após 10 tentativas falhadas, a entrega é marcada como falha permanente. Você pode solicitar reentrega manual via API ou dashboard.

## Verificação de Assinatura HMAC

Todos os webhooks são assinados usando **HMAC-SHA256** seguindo o padrão [Standard Webhooks](https://www.standardwebhooks.com/).

### Headers de Assinatura

Cada requisição de webhook inclui os seguintes headers:

| Header | Descrição | Exemplo |
| - | - | - |
| `webhook-id` | ID único do evento | `evt_abc123` |
| `webhook-timestamp` | Timestamp Unix (segundos) | `1710244200` |
| `webhook-signature` | Assinatura HMAC-SHA256 | `v1,K7gNU3sdo+OL0wNh...` |
| `content-type` | Tipo do conteúdo | `application/json` |
| `user-agent` | Agente da requisição | `chargefy.com webhooks` |

### Corpo JSON (payload)

O corpo é JSON com campos de primeiro nível nesta ordem:

| Campo | Descrição |
| - | - |
| `type` | Tipo do evento (ex.: `subscription.active`) |
| `id` | UUID do evento — **o mesmo valor** do header `webhook-id` e do registro em entregas |
| `timestamp` | ISO 8601 do momento em que o evento foi criado |
| `data` | Objeto com os dados específicos do evento |

Assim você pode persistir um único JSON (por exemplo em fila ou banco) já com o identificador para idempotência, alinhado ao header.

```json theme={null}
{
  "type": "subscription.active",
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2025-03-18T12:00:00.000Z",
  "data": { }
}
```

> [!NOTE]
>
> Eventos antigos (antes desta convenção) podem não incluir `id` no corpo; use sempre o header `webhook-id` para verificação de assinatura e idempotência.

### Como verificar

O secret está no formato canônico `whsec_<base64>`, então a forma mais
simples e segura é usar a [biblioteca oficial `svix`](https://github.com/svix/svix-webhooks)
— ela cuida da decodificação do secret, da verificação de timestamp,
da comparação constant-time e do suporte a múltiplas assinaturas durante
rotação. Existe lib oficial em Node, Python, Ruby, Go, PHP, Java, C# e
Rust.

```typescript Node.js / Express theme={null}
import { Webhook } from 'svix';
import express from 'express';

const app = express();
const wh = new Webhook(process.env.CHARGEFY_WEBHOOK_SECRET!); // whsec_...

// IMPORTANTE: use raw body — qualquer re-serialização quebra a assinatura
app.post('/webhooks/chargefy', express.raw({ type: 'application/json' }), (req, res) => {
  let evt: { type: string; id: string; timestamp: string; data: unknown };
  try {
    evt = wh.verify(req.body, {
      'webhook-id': req.headers['webhook-id'] as string,
      'webhook-timestamp': req.headers['webhook-timestamp'] as string,
      'webhook-signature': req.headers['webhook-signature'] as string,
    }) as typeof evt;
  } catch {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  switch (evt.type) {
    case 'subscription.active':
      handleSubscriptionActive(evt.data);
      break;
    case 'subscription.canceled':
      handleSubscriptionCanceled(evt.data);
      break;
  }

  res.status(200).json({ received: true });
});
```

```typescript Next.js (App Router) theme={null}
import { NextRequest, NextResponse } from 'next/server';
import { Webhook } from 'svix';

const wh = new Webhook(process.env.CHARGEFY_WEBHOOK_SECRET!); // whsec_...

export async function POST(req: NextRequest) {
  const body = await req.text(); // raw — não use req.json()
  let evt: { type: string; id: string; timestamp: string; data: unknown };
  try {
    evt = wh.verify(body, {
      'webhook-id': req.headers.get('webhook-id')!,
      'webhook-timestamp': req.headers.get('webhook-timestamp')!,
      'webhook-signature': req.headers.get('webhook-signature')!,
    }) as typeof evt;
  } catch {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }

  switch (evt.type) {
    case 'subscription.active':
      // ...
      break;
  }

  return NextResponse.json({ received: true });
}
```

```python Python / Flask theme={null}
from svix.webhooks import Webhook, WebhookVerificationError
from flask import Flask, request, jsonify
import os

app = Flask(__name__)
wh = Webhook(os.environ['CHARGEFY_WEBHOOK_SECRET'])  # whsec_...

@app.post('/webhooks/chargefy')
def chargefy_webhook():
    try:
        evt = wh.verify(request.get_data(), dict(request.headers))
    except WebhookVerificationError:
        return jsonify(error='Invalid signature'), 401

    if evt['type'] == 'subscription.active':
        # handle...
        pass

    return jsonify(received=True), 200
```

> [!WARNING]
>
> **Sempre use `express.raw()` (ou `req.text()`, `request.get_data()` etc.)** para receber o body como Buffer/string bruta. Se o body for parseado como JSON antes da verificação, a assinatura não vai corresponder porque a serialização pode diferir.

> [!TIP]
>
> Se preferir verificar manualmente sem a lib `svix`: decodifique o base64
> do secret depois do prefixo `whsec_`, calcule
> `HMAC-SHA-256(${webhook-id}.${webhook-timestamp}.${raw_body})` com esses
> bytes como chave, codifique em base64 e compare (constant-time) contra
> cada assinatura no header (separadas por espaço, prefixo `v1,`). Aceite
> apenas timestamps dentro de uma janela de ±5 minutos.

## Múltiplas Assinaturas

O header `webhook-signature` pode conter múltiplas assinaturas separadas por espaço. Isso permite rotação de secrets sem downtime:

```
webhook-signature: v1,assinatura_antiga v1,assinatura_nova
```

Sua aplicação deve verificar se **qualquer uma** das assinaturas é válida.

## Boas Práticas

> [accordion] Responda rápido (\< 20s)
>
> Processe webhooks de forma assíncrona. Responda com `200` imediatamente e processe o evento em background. Se seu handler demorar mais de 20 segundos, a Chargefy considerará a entrega como falhada.

> [accordion] Idempotência
>
> Use o `webhook-id` (ou o campo `id` no JSON do corpo, quando presente — mesmo valor) como chave de idempotência. O mesmo evento pode ser entregue mais de uma vez (retry). Garanta que processar o mesmo evento duas vezes não cause efeitos duplicados.
>
> ```typescript theme={null}
> // Exemplo: verificar se já processou
> const processed = await redis.get(`webhook:${webhookId}`);
> if (processed) {
>   return res.status(200).json({ received: true, duplicate: true });
> }
> await redis.set(`webhook:${webhookId}`, '1', 'EX', 86400);
> ```

> [accordion] Sempre verifique a assinatura
>
> Nunca processe um webhook sem verificar a assinatura HMAC. Isso protege contra requisições forjadas.

> [accordion] Monitore entregas no dashboard
>
> Acompanhe o status das entregas em **Configurações** → **Webhooks** → **Entregas**. Verifique regularmente se há falhas persistentes.

> [accordion] Use HTTPS
>
> URLs de webhook devem usar HTTPS. A Chargefy não aceita endpoints HTTP.

## Desenvolvimento Local

Para testar webhooks em desenvolvimento local, use um tunnel:

```bash ngrok theme={null}
# Instalar ngrok
npm install -g ngrok

# Expor porta local
ngrok http 3000

# Use a URL gerada (https://abc123.ngrok.io) como URL do webhook
```

```bash localtunnel theme={null}
# Instalar localtunnel
npm install -g localtunnel

# Expor porta local
lt --port 3000

# Use a URL gerada como URL do webhook
```

## Troubleshooting

| Problema | Causa Provável | Solução |
| - | - | - |
| Status 401 | Assinatura inválida | Verifique se está usando o secret correto e raw body |
| Status 500 | Erro no seu handler | Verifique logs da sua aplicação |
| Timeout | Handler muito lento | Processe em background, responda com 200 imediatamente |
| Não recebe webhooks | URL incorreta ou firewall | Verifique URL (HTTPS), teste com curl |
| Webhook duplicado | Retry após falha anterior | Implemente idempotência com `webhook-id` |

## Próximos Passos

> [card] [Eventos Disponíveis](https://scaleup-c34c4386.mintlify.site/integrate/webhooks/events.md)
>
> Lista completa de 27 tipos de eventos

> [card] [Gerenciar Endpoints](https://scaleup-c34c4386.mintlify.site/integrate/webhooks/endpoints.md)
>
> Criar, atualizar e deletar endpoints


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.