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

# Webhooks

> Inscreva-se em eventos do ciclo de vida de um payment intent. Assinados com HMAC, reentregues com backoff exponencial.

Webhooks de saída são como a Caratuva avisa seu ERP de cada mudança de estado de um payment intent — KYC do comprador aprovado, transferência confirmada, liquidado, falhou. Você cadastra uma URL e uma lista de eventos uma vez, depois verifica a assinatura HMAC em cada entrega.

## Inscrever

```bash theme={null}
curl -X POST https://api.caratuva.com/v1/webhook-subscriptions \
  -H "X-API-Key: $CARATUVA_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "url": "https://erp.example.com/webhooks/caratuva",
    "eventTypes": [
      "payment_intent.created",
      "payment_intent.buyer_kyc_approved",
      "payment_intent.on_chain_confirmed",
      "payment_intent.settled",
      "payment_intent.failed",
      "payment_intent.cancelled",
      "payment_intent.expired"
    ]
  }'
```

Resposta (o campo `secret` é o único lugar onde o segredo de assinatura aparece):

```json theme={null}
{
  "id": "cksub_123...",
  "orgId": "ckorg_456...",
  "url": "https://erp.example.com/webhooks/caratuva",
  "eventTypes": ["payment_intent.created", "payment_intent.settled", "..."],
  "active": true,
  "createdAt": "2026-05-02T14:00:00.000Z",
  "updatedAt": "2026-05-02T14:00:00.000Z",
  "secret": "whsec_<base64>"
}
```

Guarde `secret` imediatamente — a Caratuva o criptografa em repouso e não consegue retorná-lo de novo. Se perder, gire-o.

## Tipos de evento

Dois namespaces paralelos:

* `payment_intent.*` — disparam apenas para faturas criadas via `POST /v1/payment-intents`. Inscreva-se nestes se você é integrador B2B. Não verá ruído de nenhuma atividade do painel.
* `invoice.*` — disparam para toda fatura (tanto criada via API quanto via painel). Inscreva-se nestes apenas se você também opera o painel.

Lista completa de eventos `payment_intent.*`:

| Evento                              | Quando                                                                      |
| ----------------------------------- | --------------------------------------------------------------------------- |
| `payment_intent.created`            | Intent criado via API; comprador ainda não engajado.                        |
| `payment_intent.buyer_kyc_approved` | A submissão de KYC do comprador passou na verificação de identidade.        |
| `payment_intent.buyer_kyc_rejected` | KYC rejeitado. O intent termina em `failed`.                                |
| `payment_intent.on_chain_confirmed` | Transferência internacional confirmada na camada de liquidação da Caratuva. |
| `payment_intent.settled`            | Repasse PIX concluído; fundos na conta BRL do vendedor. Estado final feliz. |
| `payment_intent.failed`             | Falha terminal (qualquer fase). `data` carrega o motivo.                    |
| `payment_intent.cancelled`          | Cancelado via `POST /v1/payment-intents/:id/cancel` ou pela operação.       |
| `payment_intent.expired`            | `expiresAt` atingido sem pagamento.                                         |

## Formato de entrega

```http theme={null}
POST /webhooks/caratuva HTTP/1.1
Host: erp.example.com
Content-Type: application/json
X-Caratuva-Signature: t=1714658400,v1=8c9f...sha256hex
X-Caratuva-Delivery-Id: ckdel_789...
X-Caratuva-Event-Type: payment_intent.settled

{
  "event": "payment_intent.settled",
  "data": {
    "paymentIntentId": "ckabc123...",
    "externalId": "INV-2026-00042",
    "amount": "12500.00",
    "currency": "USD",
    "metadata": { "orderId": "42" }
  }
}
```

Endpoints precisam responder com status 2xx em até 10 segundos. Qualquer outra coisa é tratada como falha de entrega.

## Verificação de assinatura

O header de assinatura é `t=<unix_ts>,v1=<sha256_hex>`. O payload assinado é `${t}.${rawBody}` (os bytes brutos do body HTTP, não um objeto JSON re-serializado). Sempre verifique contra os bytes brutos — re-serializar muda whitespace e ordem de chaves e quebra o MAC.

### Passos

1. Faça parse de `t` e `v1` do header `X-Caratuva-Signature`.
2. Rejeite a requisição se `|now - t| > 300` (janela de replay de 5 minutos).
3. Compute `expected = HMAC-SHA256(secret, "${t}.${rawBody}")` e compare em hex contra `v1` com comparador de tempo constante.
4. Use `X-Caratuva-Delivery-Id` como chave de idempotência do seu lado — replays com o mesmo id devem ser no-op.

### Exemplo Node.js

```ts theme={null}
import { createHmac, timingSafeEqual } from 'node:crypto';
import express, { Request, Response } from 'express';

const SECRET = process.env.CARATUVA_WEBHOOK_SECRET!;
const REPLAY_WINDOW_SECONDS = 300;

const app = express();

// Capture o raw body — express.json() descarta os bytes que precisamos verificar.
app.post(
  '/webhooks/caratuva',
  express.raw({ type: 'application/json' }),
  (req: Request, res: Response) => {
    const header = req.header('x-caratuva-signature');
    if (!header) return res.status(401).json({ error: 'missing signature' });

    const parts = Object.fromEntries(
      header.split(',').map((p) => p.trim().split('=', 2)),
    ) as { t?: string; v1?: string };
    if (!parts.t || !parts.v1) return res.status(401).json({ error: 'malformed signature' });

    const t = Number(parts.t);
    if (!Number.isFinite(t)) return res.status(401).json({ error: 'malformed signature' });

    const now = Math.floor(Date.now() / 1000);
    if (Math.abs(now - t) > REPLAY_WINDOW_SECONDS) {
      return res.status(401).json({ error: 'timestamp out of window' });
    }

    const rawBody = (req.body as Buffer).toString('utf8');
    const expected = createHmac('sha256', SECRET)
      .update(`${t}.${rawBody}`)
      .digest('hex');

    const a = Buffer.from(expected, 'hex');
    const b = Buffer.from(parts.v1, 'hex');
    if (a.length !== b.length || !timingSafeEqual(a, b)) {
      return res.status(401).json({ error: 'bad signature' });
    }

    const event = JSON.parse(rawBody) as { event: string; data: unknown };
    // Idempotência: deduplique por x-caratuva-delivery-id antes de mutar estado.
    handleEvent(req.header('x-caratuva-delivery-id')!, event);

    res.status(204).end();
  },
);
```

### Exemplo Python

```python theme={null}
import hmac, hashlib, time
from flask import Flask, request, abort

SECRET = os.environ["CARATUVA_WEBHOOK_SECRET"].encode()
REPLAY_WINDOW = 300

app = Flask(__name__)

@app.post("/webhooks/caratuva")
def handle():
    header = request.headers.get("X-Caratuva-Signature", "")
    parts = dict(p.strip().split("=", 1) for p in header.split(","))
    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not v1:
        abort(401)
    if abs(int(time.time()) - int(t)) > REPLAY_WINDOW:
        abort(401)

    raw = request.get_data()
    expected = hmac.new(SECRET, f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, v1):
        abort(401)

    event = request.get_json(force=True)
    delivery_id = request.headers["X-Caratuva-Delivery-Id"]
    handle_event(delivery_id, event)
    return "", 204
```

## Retentativas e backoff

A Caratuva tenta entrega imediata, depois reentrega em qualquer não-2xx ou erro de transporte com backoff exponencial:

| Tentativa | Atraso desde a anterior |
| --------- | ----------------------- |
| 1         | (imediato)              |
| 2         | 30s                     |
| 3         | 2m                      |
| 4         | 10m                     |
| 5         | 1h                      |
| 6         | 6h                      |
| 7         | 24h                     |

Após a sétima tentativa, a entrega vira `dead_letter` e a Caratuva para de tentar. Use `GET /v1/webhook-subscriptions/:id/deliveries` para inspecionar entregas falhas e reenviá-las manualmente se preciso.

## Listar inscrições

```bash theme={null}
curl https://api.caratuva.com/v1/webhook-subscriptions \
  -H "X-API-Key: $CARATUVA_API_KEY"
```

## Inspecionar entregas

```bash theme={null}
curl https://api.caratuva.com/v1/webhook-subscriptions/<id>/deliveries?limit=50 \
  -H "X-API-Key: $CARATUVA_API_KEY"
```

Cada linha carrega `attempt`, `status` (`pending` | `delivered` | `failed` | `dead_letter`), o `responseStatus` upstream e o próximo `nextAttemptAt`. Na prática, os valores que você verá hoje são `pending`, `delivered` e `dead_letter` — uma linha permanece `pending` entre retentativas e vira `dead_letter` após a tentativa final; `failed` está reservado.

## Girar o segredo

```bash theme={null}
curl -X POST https://api.caratuva.com/v1/webhook-subscriptions/<id>/rotate-secret \
  -H "X-API-Key: $CARATUVA_API_KEY"
```

A resposta carrega o novo `secret` exatamente uma vez. A rotação é **imediata e atômica** — a API armazena apenas um segredo por inscrição, então o segredo antigo para de verificar já na próxima entrega e não há janela de sobreposição com segredo duplo. Implante o novo segredo no seu verificador primeiro (ou atomicamente junto com a chamada de rotação); quaisquer entregas em andamento ou reentregues são re-assinadas com o novo segredo quando enviadas. Não configure seu verificador para aceitar "qualquer um" dos segredos esperando uma sobreposição — ela não existe.

## Deletar (desativar)

```bash theme={null}
curl -X DELETE https://api.caratuva.com/v1/webhook-subscriptions/<id> \
  -H "X-API-Key: $CARATUVA_API_KEY"
```

Marca a inscrição como inativa (`active=false`). Nenhuma entrega futura dispara; as linhas históricas em `deliveries` continuam consultáveis.
