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

# Webhooks

> Recibe notificaciones firmadas y en tiempo real de cada evento de pago, con reintentos automáticos y verificación HMAC.

# Webhooks

Los webhooks empujan eventos a tu servidor en el momento en que ocurren: cargos creados y pagados, aprobaciones solicitadas, presupuestos agotados. Cada entrega va firmada con HMAC-SHA256 para que puedas demostrar que vino de Payzor.

## 1. Registra un endpoint

Desde una sesión de consola:

```bash theme={null}
curl -X POST https://tu-host-payzor/settings/webhooks \
  -H "Authorization: Bearer <console_jwt>" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://api.tuapp.com/payzor/webhook"}'
```

Respuesta: **el secreto de firma se muestra una sola vez**:

```json theme={null}
{
  "id": "wh_3318183a8183dd29",
  "url": "https://api.tuapp.com/payzor/webhook",
  "events": [],
  "secret": "whsec_5b30f56a99d954bf2c386ee8a2427656251cfc60ab4e3045"
}
```

Un array `events` vacío significa **todos los eventos**. Pasa un subconjunto para filtrar, por ejemplo `"events": ["payment.succeeded", "budget.exhausted"]`.

## 2. Recibe las entregas

Cada POST a tu endpoint incluye:

| Cabecera             | Significado                                    |
| -------------------- | ---------------------------------------------- |
| `X-Payzor-Event`     | Tipo de evento, por ejemplo `charge.succeeded` |
| `X-Payzor-Signature` | `t=<unix_ts>,v1=<hex_hmac>`                    |

El cuerpo es un sobre JSON; `data` lleva la carga del evento:

```json theme={null}
{
  "id": "evt_wh_9eab971e9e1a9bee",
  "type": "payment.succeeded",
  "createdAt": "2026-08-23T05:00:57.334Z",
  "data": {
    "chargeId": "chg_834e692669a3f8ad",
    "merchantId": "mch_c8fdac10d39c0c4d",
    "agentId": "agent_6b75c0e895ba9c3d",
    "amountMinor": 200,
    "currency": "USD",
    "method": "ledger",
    "reference": "PAY_63FC9EDD0FAD129D"
  }
}
```

## 3. Verifica la firma

La firma se calcula sobre los **bytes crudos del cuerpo** (no sobre el JSON reserializado) así:

```text theme={null}
hmac_sha256(secret, "<t>.<raw_body>")
```

donde `<t>` es la marca de tiempo de la cabecera de firma. Compara con una igualdad resistente a ataques de tiempo.

<Tip>
  Verificar contra el cuerpo crudo importa: si primero parseas y vuelves a serializar el JSON, el orden de las claves o el formato de los decimales pueden cambiar y la verificación fallará incluso en llamadas legítimas.
</Tip>

<CodeGroup>
  ```javascript Node / Express theme={null}
  const crypto = require('crypto');

  app.post('/payzor/webhook',
    express.raw({ type: '*/*' }),            // el cuerpo crudo es imprescindible
    (req, res) => {
      const sig = req.get('X-Payzor-Signature') || '';
      const [tPart, v1Part] = sig.split(',');
      const t = tPart.slice(2);                // "t=1690..."
      const v1 = v1Part.slice(3);              // "v1=abc..."

      const expected = crypto
        .createHmac('sha256', process.env.PAYZOR_WEBHOOK_SECRET)
        .update(`${t}.${req.body.toString()}`)
        .digest('hex');

      const ok = crypto.timingSafeEqual(
        Buffer.from(v1), Buffer.from(expected)
      );
      if (!ok) return res.status(401).end();

      const event = JSON.parse(req.body.toString());
      // ... maneja event.type
      res.json({ ok: true });
    });
  ```

  ```python Python / FastAPI theme={null}
  import hmac, hashlib

  @app.post("/payzor/webhook")
  async def payzor_webhook(request: Request):
      raw = await request.body()
      sig = request.headers["x-payzor-signature"]
      t_part, v1_part = sig.split(",")
      t = t_part[2:]          # después de "t="
      v1 = v1_part[3:]        # después de "v1="

      expected = hmac.new(
          PAYZOR_WEBHOOK_SECRET.encode(),
          f"{t}.".encode() + raw,
          hashlib.sha256,
      ).hexdigest()

      if not hmac.compare_digest(v1, expected):
          raise HTTPException(status_code=401)

      event = json.loads(raw)
      # ... maneja event["type"]
  ```
</CodeGroup>

Opcionalmente comprueba también que `t` esté dentro de unos 5 minutos respecto a ahora (protección contra repetición).

## 4. Responde rápido, procesa en segundo plano

Devuelve cualquier 2xx cuanto antes. Si tu handler lanza una excepción, Payzor **reintenta con backoff exponencial**. Las entregas se guardan en memoria por endpoint; puedes revisar las recientes en:

```bash theme={null}
curl https://tu-host-payzor/settings/webhooks/wh_xxx/deliveries \
  -H "Authorization: Bearer <console_jwt>"
```

## Catálogo de eventos

| Evento              | Cuándo                                       | Campos clave en `data`                       |
| ------------------- | -------------------------------------------- | -------------------------------------------- |
| `charge.created`    | El comercio crea un cargo                    | `chargeId`, `amountMinor`, `merchantName`    |
| `payment.succeeded` | El agente liquida desde su billetera         | `chargeId`, `agentId`, `reference`, `method` |
| `charge.succeeded`  | El cargo queda pagado (por cualquier método) | los mismos de arriba                         |
| `approval.required` | La política pide un humano                   | `approvalId`, `reason`, `expiresAt`          |
| `paylink.accepted`  | El agente acepta un grant de presupuesto     | `grantId`, `paylinkId`, `agentId`            |
| `budget.exhausted`  | Presupuesto consumido por completo           | `paylinkId`, `spentMinor`                    |
| `deposit.confirmed` | USDC on-chain acreditado                     | `agentId`, `amount`, `network`, `address`    |

Cargas completas: [Referencia de eventos de webhook](/es/api-reference/webhook-events).

<Warning>
  Trata tus handlers de webhooks como idempotentes. Los reintentos implican que puedes recibir el mismo evento más de una vez: deduplica por `event.id`.
</Warning>
