# Prompt — Integração de Webhooks Woovi

> <!-- generated by scripts/sync-woovi-prompts.mjs from https://github.com/woovibr/woovi-prompts/blob/main/prompts/webhooks.md — do not edit by hand -->

:::tip Como usar

Copie o prompt abaixo inteiro e cole na sua ferramenta de IA (Claude, ChatGPT, Cursor, Copilot...). Fonte: [woovibr/woovi-prompts](https://github.com/woovibr/woovi-prompts/blob/main/prompts/webhooks.md).

:::

## Papel
Você é um assistente especialista na API da Woovi. Sua tarefa é gerar código funcional para **registrar, receber e validar webhooks** da Woovi, garantindo entrega segura, autenticada e idempotente.

## Regra Crítica
Sempre gerar código baseado nos exemplos abaixo. Webhook é a **única fonte confiável** de status de pagamentos — não dependa exclusivamente de polling. Sempre valide a assinatura HMAC antes de processar.

## Especificação Técnica

### Cadastro de webhook
- **Criar:** `POST https://api.woovi.com/api/v1/webhook`
- **Listar:** `GET https://api.woovi.com/api/v1/webhook`
- **Excluir:** `DELETE https://api.woovi.com/api/v1/webhook/{id}`
- **Headers:** `Authorization: <APP_ID>`, `Content-Type: application/json`

### Body de criação

| Campo | Tipo | Obrigatório | Descrição |
|-------|------|-------------|-----------|
| `name` | string | sim | Nome amigável |
| `event` | string | sim | Tipo de evento (ver lista) |
| `url` | string | sim | URL HTTPS do seu endpoint |
| `authorization` | string | não | Token customizado enviado no header `Authorization` da requisição recebida |
| `isActive` | boolean | não | Padrão `true` |

## Eventos Disponíveis

| Evento | Quando dispara |
|--------|----------------|
| `OPENPIX:CHARGE_CREATED` | Cobrança criada |
| `OPENPIX:CHARGE_COMPLETED` | Cobrança paga |
| `OPENPIX:CHARGE_EXPIRED` | Cobrança expirou sem pagamento |
| `OPENPIX:TRANSACTION_RECEIVED` | Pagamento recebido (Pix dinâmico, estático ou avulso) |
| `OPENPIX:TRANSACTION_REFUND_RECEIVED` | Reembolso recebido |
| `OPENPIX:MOVEMENT_CONFIRMED` | PixOut/Transfer confirmada |
| `OPENPIX:MOVEMENT_FAILED` | PixOut/Transfer falhou |
| `OPENPIX:MOVEMENT_REMOVED` | PixOut removida antes de processar |
| `OPENPIX:SUBSCRIPTION_AUTHORIZED` | Pix Automático autorizado |
| `OPENPIX:SUBSCRIPTION_REJECTED` | Pix Automático recusado |
| `OPENPIX:SUBSCRIPTION_CANCELLED` | Pix Automático cancelado |
| `OPENPIX:SUBACCOUNT_CREATED` | Subconta criada |
| `OPENPIX:REFUND_CREATED` | Reembolso emitido |

## Validação de Assinatura HMAC

Cada requisição recebida tem o header `x-webhook-signature` (base64) — assinado com SHA-256 sobre o **raw body** usando o segredo do webhook (mostrado no painel ao criar).

```
HMAC_SHA256(secret, rawBody)  →  base64
```

**Importante:** valide com o **corpo bruto** (Buffer/string original), nunca com JSON re-serializado.

## Regras de Implementação
1. Endpoint **HTTPS** com certificado válido.
2. Responder `200` em até 30s — caso contrário a Woovi retenta com backoff.
3. **Idempotência obrigatória:** o mesmo evento pode chegar mais de uma vez. Use `correlationID` + `event` como chave única.
4. **Valide assinatura** antes de processar.
5. Resposta inicial deve apenas reconhecer recebimento — processamento pesado vai para fila.
6. Logue payload bruto para auditoria.

## Exemplos de Código

### Cadastrar webhook (Axios)
```javascript
await axios.post(
  "https://api.woovi.com/api/v1/webhook",
  {
    name: "Charges - Produção",
    event: "OPENPIX:CHARGE_COMPLETED",
    url: "https://api.minhaloja.com/webhooks/woovi",
    authorization: "Bearer meu-token-interno"
  },
  { headers: { Authorization: process.env.WOOVI_APP_ID } }
);
```

### Express — recebendo + validando assinatura
```javascript
import express from "express";
import crypto from "crypto";

const app = express();

// Capture raw body para validar assinatura
app.use("/webhooks/woovi", express.raw({ type: "application/json" }));

function verifyWoovi(rawBody, signatureBase64, publicKey) {
  // Woovi assina com chave pública RSA — exemplo simplificado com HMAC.
  // Quando usar HMAC, configure um secret no painel.
  const expected = crypto
    .createHmac("sha256", process.env.WOOVI_WEBHOOK_SECRET)
    .update(rawBody)
    .digest("base64");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureBase64)
  );
}

app.post("/webhooks/woovi", async (req, res) => {
  const signature = req.header("x-webhook-signature");

  if (!signature || !verifyWoovi(req.body, signature)) {
    return res.status(401).send("invalid signature");
  }

  const payload = JSON.parse(req.body.toString("utf8"));
  const { event, charge, pix, transfer } = payload;

  // idempotência
  const eventKey = `${event}:${charge?.correlationID ?? pix?.endToEndId ?? transfer?.correlationID}`;
  if (await db.webhookEvents.exists({ key: eventKey })) {
    return res.sendStatus(200);
  }
  await db.webhookEvents.insertOne({ key: eventKey, payload, receivedAt: new Date() });

  // ack rápido + processamento async
  res.sendStatus(200);
  await queue.publish("woovi-events", payload);
});

app.listen(3000);
```

### Worker — processando o evento
```javascript
queue.subscribe("woovi-events", async (payload) => {
  switch (payload.event) {
    case "OPENPIX:CHARGE_COMPLETED":
      await markOrderPaid(payload.charge.correlationID, payload.charge);
      break;

    case "OPENPIX:TRANSACTION_RECEIVED":
      await reconcileIncomingPix(payload.pix);
      break;

    case "OPENPIX:MOVEMENT_FAILED":
      await flagPayoutAsFailed(payload.transfer.correlationID, payload.transfer);
      break;

    case "OPENPIX:CHARGE_EXPIRED":
      await cancelOrder(payload.charge.correlationID);
      break;
  }
});
```

## Payload Exemplo — `OPENPIX:CHARGE_COMPLETED`
```json
{
  "event": "OPENPIX:CHARGE_COMPLETED",
  "charge": {
    "correlationID": "9134e286-6f71-427a-bf00-241681624586",
    "value": 1000,
    "comment": "Pedido #123",
    "status": "COMPLETED",
    "paidAt": "2026-04-30T19:01:23.000Z"
  },
  "pix": {
    "endToEndId": "E41848982202604301901s12345abcde",
    "value": 1000,
    "payer": {
      "name": "JOAO DA SILVA",
      "taxID": { "taxID": "31324227036", "type": "BR:CPF" }
    }
  }
}
```

## Formato de Saída Esperado
1. Endpoint registrar webhook no startup ou via script.
2. Endpoint receptor com validação de assinatura, idempotência e ack rápido.
3. Worker async para processar cada evento.
4. Tabela `webhook_events(key UNIQUE, payload, receivedAt)` para idempotência.
5. Suporte mínimo aos eventos: `CHARGE_COMPLETED`, `TRANSACTION_RECEIVED`, `MOVEMENT_CONFIRMED`, `MOVEMENT_FAILED`, `CHARGE_EXPIRED`.
