# Webhooks

Los webhooks notifican a su aplicación, en tiempo real, sobre el desenlace de las operaciones. Usted registra una URL, elige los eventos y recibe un POST firmado en cada ocurrencia. Todos los endpoints están acotados a las credenciales del socio, y cada entrega lleva el `accountId` de la cuenta involucrada, para que usted la enrute internamente.

## Eventos disponibles

Todos los eventos de abajo son efectivamente emitidos por la plataforma. El catálogo en `GET /webhooks/events` devuelve exactamente esta lista.

### PIX

| Evento | Cuándo dispara |
|---|---|
| `pix.charge.created` | Cobro o QR creado |
| `pix.charge.paid` | Cobro pagado, PIX de entrada acreditado |
| `pix.payout.queued` | PIX de salida encolado para procesamiento |
| `pix.payout.processing` | PIX de salida en procesamiento (aceptación intermedia) |
| `pix.payout.confirmed` | PIX de salida liquidado |
| `pix.payout.failed` | PIX de salida rechazado o expirado |
| `pix.payout.returned` | PIX de salida devuelto |
| `pix.refund.requested` | Devolución de PIX solicitada |
| `pix.refund.completed` | Devolución de PIX concluida |
| `pix.refund.failed` | Devolución de PIX rechazada |
| `pix.return.received` | Devolución recibida |
| `pix.received` | PIX recibido en la cuenta, sin cobro emitido |

### Claves PIX

| Evento | Cuándo dispara |
|---|---|
| `pix.key.registered` | Clave PIX registrada |
| `pix.key.deleted` | Clave PIX eliminada |
| `pix.key.blocked` | Clave PIX bloqueada |
| `pix.key.unblocked` | Clave PIX desbloqueada |

### Portabilidad

| Evento | Cuándo dispara |
|---|---|
| `pix.claim.created` | Reivindicación de portabilidad creada |
| `pix.claim.acknowledged` | Reivindicación reconocida |
| `pix.claim.confirmed` | Reivindicación confirmada |
| `pix.claim.cancelled` | Reivindicación cancelada |
| `pix.claim.completed` | Reivindicación concluida |

### TED y transferencias

| Evento | Cuándo dispara |
|---|---|
| `ted.confirmed` | TED liquidada |
| `ted.failed` | TED rechazada o expirada |
| `ted.received` | TED recibida en la cuenta |
| `ted.refund.requested` | Devolución de TED recibida solicitada |
| `ted.refund.completed` | Devolución de TED recibida liquidada |
| `ted.refund.failed` | Devolución de TED recibida rechazada |
| `transfer.confirmed` | Transferencia interna concluida |
| `transfer.failed` | Transferencia interna fallida |
| `transfer.received` | Transferencia interna recibida |

### Infracciones (MED)

| Evento | Cuándo dispara |
|---|---|
| `pix.infraction.created` | Infracción/MED abierta sobre una cuenta |
| `pix.infraction.resolved` | Infracción analizada y resuelta |
| `pix.infraction.defense_submitted` | Defensa de la infracción enviada por el cliente |
| `pix.med.created` | Intervención MED abierta por la Partner API |
| `pix.med.completed` | Recuperación MED concluida, fondos devueltos |
| `pix.med.cancelled` | Intervención MED cancelada |

### Tarifas

| Evento | Cuándo dispara |
|---|---|
| `fee.charged` | Tarifa cobrada a una cuenta |

### Cuentas

| Evento | Cuándo dispara |
|---|---|
| `account.created` | Cuenta creada |
| `account.blocked` | Cuenta bloqueada |
| `account.unblocked` | Cuenta desbloqueada |
| `account.closed` | Cuenta cerrada |

### Prueba

| Evento | Cuándo dispara |
|---|---|
| `webhook.test` | Entrega de prueba manual |

Los eventos de éxito (`pix.charge.paid`, `pix.payout.confirmed`, `pix.refund.completed`, `pix.return.received`) solo se entregan después de que la operación está liquidada.

## Registrar un webhook

```http
POST /api/partner/v1/webhooks
Authorization: Bearer {{access_token}}
Content-Type: application/json

{
  "url": "https://su-sistema.com/webhooks/monetarie",
  "events": ["pix.payout.confirmed", "pix.payout.failed", "pix.charge.paid"],
  "description": "Integración principal",
  "is_active": true
}
```

- **Alcance:** `webhook:write` · **Obligatorios:** `url`, `events`

Respuesta `201`:

```json
{
  "data": {
    "id": "0f7f279c-217b-4ba2-9a96-27d7401dfe2f",
    "url": "https://su-sistema.com/webhooks/monetarie",
    "events": ["pix.payout.confirmed", "pix.payout.failed", "pix.charge.paid"],
    "description": "Integración principal",
    "is_active": true,
    "created_at": "2026-07-10T13:00:00Z",
    "updated_at": "2026-07-10T13:00:00Z"
  }
}
```

::: warning Secreto de firma
El registro **no** devuelve el secreto de firma. Para obtener (o rotar) el secreto, llame a `POST /webhooks/:id/rotate-secret`, que devuelve el secreto en texto plano **una sola vez**. Guárdelo de forma segura.
:::

La URL debe ser pública, `http` o `https`, y no puede apuntar a direcciones privadas o internas.

## Obtener y rotar el secreto

```http
POST /api/partner/v1/webhooks/{id}/rotate-secret
Authorization: Bearer {{access_token}}
```

- **Alcance:** `webhook:write`

```json
{ "message": "Secret rotacionado com sucesso", "webhook_id": "0f7f...", "new_secret": "3e071f7ee1ef00db1178a96b65476db0..." }
```

El `new_secret` se muestra solo en esta respuesta. Una nueva rotación invalida el anterior.

## Gestionar webhooks

```http
GET    /api/partner/v1/webhooks              # listar (webhook:read)
GET    /api/partner/v1/webhooks/{id}         # detallar (webhook:read)
PUT    /api/partner/v1/webhooks/{id}         # actualizar url/events/description/is_active (webhook:write)
DELETE /api/partner/v1/webhooks/{id}         # eliminar, devuelve 204 (webhook:write)
POST   /api/partner/v1/webhooks/{id}/test    # enviar una entrega de prueba (webhook:write)
POST   /api/partner/v1/webhooks/{id}/replay  # reenviar la entrega más reciente (webhook:write)
GET    /api/partner/v1/webhooks/events       # catálogo de eventos (webhook:read)
```

`replay` devuelve `404` cuando aún no hay ninguna entrega para reenviar.

## Formato de la entrega

Cada evento llega como un `POST` a la URL configurada. El cuerpo es el JSON del evento, plano (sin envoltura) y con las claves en `camelCase`, y los metadatos van en los headers:

| Header | Descripción |
|---|---|
| `X-Monetarie-Event-Type` | Tipo del evento, por ejemplo `pix.payout.confirmed` |
| `X-Monetarie-Event-Id` | Identificador de la entrega, para deduplicación |
| `X-Monetarie-Timestamp` | Instante de la firma, en segundos Unix |
| `X-Monetarie-Signature` | Firma `sha256=<hex>` |
| `User-Agent` | `Monetarie-Webhook/1.0` |

Identifique el evento por el header `X-Monetarie-Event-Type`; el cuerpo no repite el tipo. Deduplique por `X-Monetarie-Event-Id`, porque la misma entrega puede llegar más de una vez.

## Payload de cada evento

Todos los cuerpos usan claves en `camelCase` y todos los valores monetarios están en **centavos**. Los campos de abajo son los que su integración necesita; un evento puede traer campos informativos adicionales, así que localice siempre por la clave, nunca por la posición.

### PIX de salida

Vale para `pix.payout.queued`, `pix.payout.processing`, `pix.payout.confirmed` y `pix.payout.failed`. Lo que cambia entre ellos es el `status`.

```json
{
  "accountId": 1042,
  "transactionId": "PIX20260711a1b2c3d4e5f6",
  "endToEndId": "E4602656220260711100000abcdef123",
  "amount": 25000,
  "status": "settled",
  "errorReason": null
}
```

| Evento | `status` |
|---|---|
| `pix.payout.queued` | `queued` |
| `pix.payout.processing` | `processing` |
| `pix.payout.confirmed` | `settled` |
| `pix.payout.failed` | `rejected`, con el motivo en `errorReason` |

### Devolución de PIX de salida

`pix.payout.returned` avisa que un PIX que usted envió fue devuelto por el receptor. Trae el valor devuelto en `amount` (puede ser parcial) y `status: "returned"`, con el `endToEndId` de la operación original. `returnId` es el identificador de la devolución en el arreglo PIX.

```json
{
  "accountId": 1042,
  "transactionId": "PIXOUT20260718a1b2c3d4e5f6a7b8c9d0",
  "endToEndId": "E4602656220260711100000abcdef123",
  "returnId": "D46026562202607181015aabbccddeef",
  "amount": 25000,
  "reason": "Devolução solicitada pelo pagador",
  "status": "returned"
}
```

### Devolución solicitada, concluida y rechazada

`pix.refund.requested` se emite cuando usted solicita la devolución de un PIX recibido. El campo `endToEndId` lleva el identificador de la transacción original que se está devolviendo.

```json
{
  "accountId": 1042,
  "transactionId": "PIXRET20260711aa11bb22",
  "endToEndId": "PIXIN20260710090000ffee00112",
  "amount": 25000,
  "status": "requested"
}
```

`pix.refund.completed` tiene el mismo formato que el PIX de salida, con `status: "settled"`, cuando la devolución liquida. `pix.refund.failed` avisa que la devolución fue rechazada: el valor permanece en la cuenta y vuelve a componer el restante devolvible, con el motivo en `errorReason`. En los dos desenlaces, correlacione por la clave `transactionId`, que es el `refundId` devuelto en la aceptación de la devolución; `endToEndId` puede venir nulo.

```json
{
  "accountId": 1042,
  "transactionId": "PIXRET20260711aa11bb22",
  "endToEndId": null,
  "amount": 25000,
  "status": "rejected",
  "errorReason": "Devolução rejeitada pela instituição do pagador original",
  "recipient": null
}
```

### Cobro creado

`pix.charge.created` confirma la creación de un cobro o QR. Trae el BR Code listo para pago en `brcode`.

```json
{
  "accountId": 1042,
  "txId": "monetarie-7f3a1c9e4b",
  "amount": 15000,
  "status": "active",
  "brcode": "00020101021226880014br.gov.bcb.pix...6304AB12",
  "expiresAt": "2026-07-11T11:00:00Z"
}
```

### PIX recibido

`pix.charge.paid` confirma el pago de un cobro suyo. Correlacione por el `txId` del cobro y por el `externalId`, cuando fue informado en la creación.

```json
{
  "accountId": 1042,
  "entityId": 987,
  "txId": "monetarie-7f3a1c9e4b",
  "externalId": "pedido-4210",
  "endToEndId": "E4602656220260711100500aa00bb11c",
  "amount": 15000,
  "description": "Pedido 4210",
  "paidAt": "2026-07-11T10:05:00Z"
}
```

### Devolución recibida

`pix.return.received` avisa que una devolución cayó en la cuenta. Puede ser parcial, señalado por `isPartial`.

```json
{
  "accountId": 1042,
  "endToEndId": "E4602656220260710090000ffee00112",
  "amount": 12000,
  "originalAmount": 25000,
  "refundedAmount": 12000,
  "isPartial": true,
  "returnReason": "MD06",
  "status": "settled"
}
```

### PIX recibido sin cobro

`pix.received` avisa un PIX que cayó en la cuenta sin un cobro emitido, como una transferencia directa a una clave de la cuenta. Cuando el crédito viene de un cobro suyo, el evento es `pix.charge.paid`.

```json
{
  "accountId": 10024270,
  "amount": 454,
  "endToEndId": "E4602656220260714210000aabbccdd0",
  "payerName": "MARIA DA SILVA",
  "payerIspb": "00416968",
  "payerDocument": "12345678901",
  "receivedAt": "2026-07-14T21:00:00Z"
}
```

### Ciclo de vida de la clave PIX

`pix.key.registered`, `pix.key.deleted`, `pix.key.blocked` y `pix.key.unblocked` acompañan las claves de la cuenta y comparten el mismo formato; `pix.key.blocked` incluye además el campo `reason`.

```json
{
  "accountId": 10024270,
  "keyType": "CPF",
  "keyValue": "12345678901",
  "occurredAt": "2026-07-14T22:00:00Z"
}
```

### Reivindicación de portabilidad

`pix.claim.created` avisa la creación de una reivindicación de portabilidad de clave. `pix.claim.acknowledged`, `pix.claim.confirmed`, `pix.claim.cancelled` y `pix.claim.completed` tienen el mismo formato y acompañan las etapas siguientes.

```json
{
  "accountId": 10024270,
  "claimId": "CLAIM-...",
  "claimType": "PORTABILITY",
  "keyType": "CPF",
  "keyValue": "12345678901",
  "donorIspb": "46026562",
  "claimerIspb": "60746948",
  "donorDeadline": "2026-07-21T22:00:00Z",
  "claimerDeadline": "2026-08-13T22:00:00Z",
  "occurredAt": "2026-07-14T22:00:00Z"
}
```

### TED

`ted.confirmed` y `ted.failed` acompañan el desenlace de una TED enviada.

```json
{
  "accountId": 1042,
  "transactionId": "TED20260711d8a3695bc594e2577c23",
  "type": "ted",
  "amount": 25000,
  "status": "settled",
  "errorReason": null
}
```

El `status` es `settled` en `ted.confirmed`, y `rejected` o `timeout` en `ted.failed`, con el motivo en `errorReason`.

### TED recibida

`ted.received` avisa que una TED de otra institución fue acreditada en la cuenta.

```json
{
  "accountId": 10024270,
  "amount": 150075,
  "numCtrlStr": "STR20260714000000123",
  "messageType": "STR0008",
  "senderIspb": "60746948",
  "senderName": "JOAO PEREIRA",
  "senderDocument": "12345678901",
  "senderAgency": "0001",
  "senderAccount": "445566",
  "receivedAt": "2026-07-14T21:00:00Z"
}
```

### Devolución de TED recibida

`ted.refund.requested` confirma la aceptación de la devolución de una TED recibida que usted comandó; `ted.refund.completed` avisa la liquidación de la devolución en el SPB y `ted.refund.failed` avisa que la devolución no se concluyó y el importe reservado volvió a quedar disponible en la cuenta. Correlacione por el `numCtrlStr` del crédito original.

```json
{
  "accountId": 10024270,
  "amount": 150075,
  "numCtrlStr": "STR20260714000000123",
  "reasonCode": "70",
  "status": "processing"
}
```

En `ted.refund.completed` la entrega trae también `spbNumCtrl`, el número de control del mensaje de devolución (STR0010), y no repite el campo `status`. En `ted.refund.failed` vienen `errorCode` y `errorMessage` con el motivo informado por el SPB.

### Transferencia interna

`transfer.confirmed` y `transfer.failed` acompañan la transferencia entre dos cuentas del socio.

```json
{
  "accountId": 1042,
  "destinationAccountId": 1043,
  "transactionId": "TEF20260711aabbccddeeff",
  "type": "internal",
  "amount": 5000,
  "status": "settled"
}
```

### Transferencia interna recibida

`transfer.received` es la pata de crédito de la transferencia interna, entregada para la cuenta que recibió. `sourceAccountId` identifica la cuenta de origen.

```json
{
  "accountId": 10024271,
  "sourceAccountId": 10024270,
  "transactionId": "TEF20260714aabbccddeeff",
  "type": "internal",
  "amount": 5000,
  "status": "settled"
}
```

### Infracción (MED)

`pix.infraction.created` avisa la apertura de una infracción o MED sobre una cuenta, y `pix.infraction.resolved` trae el resultado del análisis.

```json
{
  "accountId": 1042,
  "infractionId": "b17c9a02-4a2f-4d5e-9b1a-77e0a1c2d3e4",
  "blockId": "3f21aa8c-1b2c-4d5e-8f90-0a1b2c3d4e5f",
  "endToEndId": "E4602656220260711100000abcdef123",
  "amount": 25000,
  "status": "created",
  "fraudCategory": "SCAM"
}
```

El `status` es `created` en la apertura, y `founded` o `unfounded` en la resolución.

### Defensa de la infracción

`pix.infraction.defense_submitted` confirma el registro de la defensa enviada por el cliente para una infracción con bloqueo cautelar. `blockId` identifica el bloqueo; el resultado del análisis llega por `pix.infraction.resolved`.

```json
{
  "accountId": 1042,
  "infractionId": "b17c9a02-4a2f-4d5e-9b1a-77e0a1c2d3e4",
  "blockId": "3f21aa8c-1b2c-4d5e-8f90-0a1b2c3d4e5f",
  "endToEndId": "E4602656220260711100000abcdef123",
  "status": "defense_submitted"
}
```

### Intervención MED

`pix.med.created` confirma la apertura de una intervención MED solicitada por la Partner API. `pix.med.completed` avisa que la recuperación concluyó con la devolución de los fondos, y `pix.med.cancelled` avisa la cancelación. `endToEndId` lleva la transacción raíz de la intervención.

```json
{
  "accountId": 1042,
  "medId": "REC-a1b2c3d4",
  "endToEndId": "E4602656220260711100000abcdef123",
  "amount": 25000,
  "status": "REFUND_COMPLETED"
}
```

El `status` es `CREATED` en la apertura, `REFUND_COMPLETED` o `COMPLETED` en la conclusión y `CANCELLED` en la cancelación. El campo `amount` (centavos) viene en los desenlaces informados por la red PIX; en la apertura y en las cancelaciones comandadas por la API la entrega puede venir sin él.

### Tarifa cobrada

`fee.charged` avisa que una tarifa fue cobrada a una cuenta. `originTransactionId` y `originalAmount` apuntan a la transacción que originó el cobro.

```json
{
  "feeTransactionId": "5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
  "accountId": 10024270,
  "feeType": "ted_out",
  "amount": 500,
  "originTransactionId": "E4602656220260714210000aabbccdd0",
  "originTransactionType": "ted",
  "originalAmount": 150075,
  "status": "posted",
  "chargedAt": "2026-07-14T21:00:00Z"
}
```

### Cuenta creada

`account.created` avisa la apertura de una cuenta. El `status` puede ser `active` o `blocked`, según la verificación de registro.

```json
{
  "accountId": 10024272,
  "customerId": 4711,
  "accountNumber": "100123-4",
  "agency": "0001",
  "iban": "BR9146026562000010010012345P1",
  "accountType": "payment",
  "status": "active"
}
```

### Bloqueo, desbloqueo y cierre de cuenta

`account.blocked`, `account.unblocked` y `account.closed` comparten el mismo formato. El `status` es `blocked` en el bloqueo, `active` en el desbloqueo y `closed` en el cierre.

```json
{
  "accountId": 10024272,
  "status": "blocked"
}
```

### Prueba

`webhook.test` es la entrega manual, útil para validar su URL y la verificación de firma. El `POST /webhooks/{id}/test` entrega el evento solamente al webhook indicado en la ruta, aunque no esté suscrito a `webhook.test`; ningún otro webhook recibe la entrega de prueba.

```json
{
  "message": "Test webhook delivery",
  "timestamp": "2026-07-11T10:00:00Z",
  "webhookId": "0f7f279c-217b-4ba2-9a96-27d7401dfe2f"
}
```

## Validar la firma

La firma es un HMAC-SHA256, en hexadecimal minúsculo, sobre la cadena `"{timestamp}.{cuerpo_bruto}"`, usando el secreto del webhook. El header viene como `sha256=<hex>`. Compare el valor calculado con el header, descartando el prefijo `sha256=`. Use el cuerpo bruto recibido, sin reserializar el JSON.

```javascript
const crypto = require('crypto')

function verificar(cuerpoBruto, headerFirma, timestamp, secreto) {
  const contenido = `${timestamp}.${cuerpoBruto}`
  const esperado = crypto.createHmac('sha256', secreto).update(contenido).digest('hex')
  return `sha256=${esperado}` === headerFirma
}
```

Rechace la entrega si la firma no coincide.

## Reenvío automático

Una entrega sin éxito se reintenta con backoff, en hasta 8 intentos: inmediato, 30s, 2min, 10min, 30min, 1h, 2h y 4h. Responda `2xx` para confirmar la recepción. Las respuestas `400`, `401`, `403`, `404`, `410` y `422` se tratan como falla permanente, sin reintento.
