# Webhooks

Webhooks entregam notificações em tempo real quando recursos mudam. Em vez de fazer polling na API, registre uma URL e receba requisições `POST` sempre que eventos ocorrerem. A API Dinie segue a especificação [Standard Webhooks](https://www.standardwebhooks.com/).

## Configurando Endpoints de Webhook

### Criar um Endpoint

Registre uma URL para receber eventos de webhook. Você pode se inscrever em eventos específicos ou receber todos.

```typescript Node.js
const endpoint = await dinie.webhooks.endpoints.create({
  url: "https://yourapp.example.com/webhooks/dinie",
  events: ["customer.active", "credit_offer.available", "loan.*"],
  description: "Production webhook",
});

// Armazene endpoint.secret com segurança -- ele é exibido apenas uma vez.
console.log(endpoint.secret); // "whsec_xxxxx..."
```

```ruby Ruby
endpoint = dinie.webhooks.endpoints.create(
  url: "https://yourapp.example.com/webhooks/dinie",
  events: ["customer.active", "credit_offer.available", "loan.*"],
  description: "Production webhook"
)

# Armazene endpoint.secret com segurança -- ele é exibido apenas uma vez.
puts endpoint.secret  # "whsec_xxxxx..."
```

```python Python
endpoint = dinie.webhooks.endpoints.create(
    url="https://yourapp.example.com/webhooks/dinie",
    events=["customer.active", "credit_offer.available", "loan.*"],
    description="Production webhook",
)

# Armazene endpoint.secret com segurança -- ele é exibido apenas uma vez.
print(endpoint.secret)  # "whsec_xxxxx..."
```

```bash cURL
curl -X POST https://api-dev.dinietech.io/api/v3/webhooks/endpoints \
  -H "Authorization: Bearer dinie_at_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.example.com/webhooks/dinie",
    "events": ["customer.active", "credit_offer.available", "loan.*"],
    "description": "Production webhook"
  }'
```

O `endpoint.secret` retornado é necessário para verificar assinaturas -- armazene-o imediatamente no seu gerenciador de secrets.

> **Warning:** O `secret` é **retornado apenas na criação e rotação**. Armazene-o imediatamente no seu gerenciador de secrets. Você precisa dele para verificar assinaturas de webhook.


### Gerenciando Endpoints

| Operação | Método | Endpoint |
|  --- | --- | --- |
| Listar todos | `GET` | `/webhooks/endpoints` |
| Obter um | `GET` | `/webhooks/endpoints/{id}` |
| Atualizar | `PATCH` | `/webhooks/endpoints/{id}` |
| Excluir | `DELETE` | `/webhooks/endpoints/{id}` |


Você pode desabilitar um endpoint sem excluí-lo definindo `status: "disabled"` via `PATCH`.

## Recebendo Webhooks

Cada entrega é uma requisição `POST` com headers de autenticação (`webhook-id`, `webhook-timestamp`, `webhook-signature`). Use o SDK para verificar e parsear o evento:

```typescript Node.js
import express from "express";
import Dinie from "dinie";

const app = express();
const dinie = new Dinie({
  clientId: process.env.DINIE_CLIENT_ID,
  clientSecret: process.env.DINIE_CLIENT_SECRET,
  webhookSecret: process.env.DINIE_WEBHOOK_SECRET,
});

app.post("/webhooks/dinie", express.raw({ type: "application/json" }), (req, res) => {
  const event = dinie.webhooks.unwrap(req.body.toString(), req.headers);

  switch (event.type) {
    case "customer.active":
      console.log(`Cliente ${event.data.id} aprovado`);
      break;
    case "loan.active":
      console.log(`Empréstimo ${event.data.id} desembolsado`);
      break;
  }

  res.sendStatus(200);
});
```

```ruby Ruby
class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

  def create
    event = dinie.webhooks.unwrap(request.raw_post, request.headers)

    case event.type
    when "customer.active"
      puts "Cliente #{event.data.id} aprovado"
    when "loan.active"
      puts "Empréstimo #{event.data.id} desembolsado"
    end

    head :ok
  end
end
```

```python Python
import os
from flask import Flask, request
from dinie import Dinie

app = Flask(__name__)
dinie = Dinie(
    client_id=os.environ["DINIE_CLIENT_ID"],
    client_secret=os.environ["DINIE_CLIENT_SECRET"],
    webhook_secret=os.environ["DINIE_WEBHOOK_SECRET"],
)

@app.route("/webhooks/dinie", methods=["POST"])
def handle_webhook():
    event = dinie.webhooks.unwrap(
        request.get_data(as_text=True),
        request.headers,
    )

    if event.type == "customer.active":
        print(f"Cliente {event.data.id} aprovado")
    elif event.type == "loan.active":
        print(f"Empréstimo {event.data.id} desembolsado")

    return "", 200
```

```bash cURL
# Teste seu endpoint enviando um payload de webhook simulado:
curl -X POST https://yourapp.example.com/webhooks/dinie \
  -H "Content-Type: application/json" \
  -H "webhook-id: msg_550e8400e29b41d4a716446655440000" \
  -H "webhook-timestamp: 1709546400" \
  -H "webhook-signature: v1,K5oZfzN95Z2P2rk2kbX1XISvnJtgBXMiGMKQ0iR1sLA=" \
  -d '{
    "type": "customer.active",
    "timestamp": 1709546400,
    "data": {
      "id": "cust_550e8400...",
      "status": "active"
    }
  }'
```

O `webhook_secret` é configurado no construtor do client. O método `unwrap` usa esse secret para verificar a assinatura HMAC-SHA256, validar o timestamp (proteção contra replay de 5 minutos) e retornar o evento tipado. Se a verificação falhar, uma exceção é lançada -- retorne `400` nesse caso.

## Verificação Manual de Assinatura

Se você não estiver usando os SDKs, implemente a verificação manualmente seguindo a especificação [Standard Webhooks](https://www.standardwebhooks.com/):

1. Construa o conteúdo assinado: `{webhook-id}.{webhook-timestamp}.{raw_body}`
2. Decodifique o secret: remova o prefixo `whsec_` e decodifique o restante em base64
3. Calcule o HMAC-SHA256 do conteúdo assinado usando o secret decodificado
4. Codifique o resultado em base64 e compare com cada assinatura no header `webhook-signature`
5. Rejeite se o `webhook-timestamp` for mais antigo que 5 minutos (proteção contra replay)


Consulte a especificação [Standard Webhooks](https://www.standardwebhooks.com/) para detalhes completos sobre os headers e formato de assinatura.

## Códigos de Resposta

Seu endpoint de webhook deve responder em até **15 segundos**. Apenas o código de status HTTP importa -- o corpo da resposta é ignorado.

| Status | Comportamento | Descrição |
|  --- | --- | --- |
| **2xx** (200-299) | **Sucesso** | Entrega confirmada. Mais comuns: `200 OK` ou `204 No Content`. |
| **3xx** (300-399) | **Falha + retentativa** | Redirecionamentos NÃO são seguidos. Atualize a URL do endpoint via API. |
| **410** Gone | **Endpoint desativado** | Dinie desativa permanentemente o endpoint e para de enviar eventos. Use para cancelar a inscrição sem chamar a API. |
| **429** Too Many Requests | **Throttle** | Dinie reduz a taxa de envio e reagenda com backoff. |
| **4xx** (outros) | **Falha + retentativa** | Qualquer outro erro de cliente dispara retentativas. |
| **502** Bad Gateway | **Throttle** | Dinie reduz a taxa de envio antes de retentar. |
| **504** Gateway Timeout | **Throttle** | Mesmo comportamento do 502. |
| **5xx** (outros) | **Falha + retentativa** | Qualquer outro erro de servidor dispara retentativas. |
| Timeout (sem resposta em 15s) | **Falha + retentativa** | Conexão encerrada, entrega tratada como falha. |


## Política de Retentativa

Entregas com falha são reenviadas com exponential backoff. Jitter aleatório é adicionado a cada intervalo para evitar thundering herd.

| Tentativa | Intervalo |
|  --- | --- |
| 1 | Imediato |
| 2 | 5 segundos |
| 3 | 5 minutos |
| 4 | 30 minutos |
| 5 | 2 horas |
| 6 | 5 horas |
| 7 | 10 horas |
| 8 | 14 horas |
| 9 | 20 horas |
| 10 | 24 horas |


Após 10 tentativas com falha (~3 dias), o evento é marcado como `failed`. Eventos com falha podem ser reenviados manualmente. Falhas repetidas em múltiplos eventos desabilitarão automaticamente o endpoint de webhook.

> **Info:** Use o header `webhook-id` como chave de idempotência para deduplicar entregas do seu lado. O mesmo evento pode ser entregue mais de uma vez durante as retentativas.


## Rotação de Secret

Rotacione o secret de assinatura do seu webhook sem downtime:

```bash
curl -X POST https://api-dev.dinietech.io/api/v3/webhooks/endpoints/we_550e8400.../rotate-secret \
  -H "Authorization: Bearer dinie_at_..." \
  -H "Content-Type: application/json" \
  -d '{ "expire_current_in": 3600 }'
```

Durante o período de transição (`expire_current_in`, padrão 1 hora, máximo 24 horas), ambos os secrets -- antigo e novo -- estão ativos. O header `webhook-signature` contém assinaturas para **ambos os secrets**, então seu código de verificação vai corresponder a qualquer um deles.

1. Chame `POST /webhooks/endpoints/{id}/rotate-secret` com um período de transição
2. Atualize sua aplicação com o novo secret da resposta
3. Faça o deploy -- durante o período de transição, ambos os secrets funcionam
4. Após o período de transição, apenas o novo secret é usado


## Boas Práticas

1. **Sempre verifique assinaturas** -- nunca confie nos payloads de webhook sem verificação
2. **Responda rapidamente** -- retorne um status `200` em até 15 segundos. Processe o evento de forma assíncrona se necessário.
3. **Implemente idempotência** -- use o `webhook-id` para deduplicar. Seu handler deve processar o mesmo evento duas vezes com segurança.
4. **Use HTTPS** -- URLs de webhook devem usar HTTPS. Endpoints HTTP são rejeitados.
5. **Registre as entregas** -- armazene o `webhook-id` e o `webhook-timestamp` para debugging e trilhas de auditoria.


## Eventos Disponíveis

Todos os payloads seguem o mesmo envelope:

```json
{
  "type": "resource.action",
  "timestamp": 1709546400,
  "data": { ... }
}
```

### Inscrição por Wildcard

- Eventos específicos: `["customer.active", "loan.created"]`
- Todos de um recurso: `["loan.*"]`
- Todos os eventos: omita o campo `events` ou passe um array vazio


### Eventos por Recurso

| Recurso | Evento | Descrição |
|  --- | --- | --- |
| **Cliente** | `customer.created` | Novo cliente cadastrado |
|  | `customer.under_review` | KYC enviado, análise iniciada |
|  | `customer.kyc_updated` | Status de requisito KYC alterado |
|  | `customer.active` | Totalmente verificado, elegível para crédito |
|  | `customer.denied` | Permanentemente bloqueado |
| **Oferta de Crédito** | `credit_offer.available` | Nova oferta criada para o cliente |
|  | `credit_offer.expired` | Oferta ultrapassou a data de validade |
| **Empréstimo** | `loan.created` | Empréstimo criado com CCB gerada sincronamente, `signing_url` disponível |
|  | `loan.signature_received` | Assinatura individual recebida |
|  | `loan.processing` | Todas assinaturas coletadas, desembolso em andamento |
|  | `loan.active` | Fundos desembolsados, pagamento iniciado |
|  | `loan.payment_received` | Parcela recebida |
|  | `loan.finished` | Empréstimo totalmente quitado |
|  | `loan.cancelled` | Empréstimo cancelado |
|  | `loan.error` | Erro no processamento ou desembolso |