# Registrar Webhook

Endpoints para crear, listar y remover webhooks de notificacion.

---

## Crear Webhook

```
POST /api/external/webhooks
```

### Headers

| Header | Tipo | Obligatorio | Descripcion |
|--------|------|-------------|-------------|
| `Authorization` | String | Si | `ApiKey {client_id}:{client_secret}` |
| `Content-Type` | String | Si | `application/json` |
| `hmac` | String | Si | Firma HMAC-SHA512 del body ([mas informacion](/es/hmac)) |

### Request Body

| Campo | Tipo | Obligatorio | Default | Descripcion |
|-------|------|-------------|---------|-------------|
| `url` | String | Si | -- | URL para recibir notificaciones (HTTPS por defecto) |
| `events` | Array | **Si** | -- | Lista de eventos a suscribir. Debe ser un array **no-vacio** con al menos un evento valido de la tabla abajo. Omitir el campo retorna `400 {"errors": {"events": ["can't be blank"]}}`. |
| `secret` | String | No | auto-generado | Clave para firma HMAC-SHA256 de las entregas. Si se omite, un valor aleatorio es generado automaticamente. |
| `description` | String | No | `null` | Descripcion libre del webhook para identificacion interna |
| `allow_insecure` | Boolean | No | `false` | Permite URLs HTTP (no-HTTPS). La seguridad de los datos es responsabilidad del cliente. |

Eventos disponibles (PIX, TEF entre cuentas Monetarie y prueba operacional de webhook):

| Evento | Status body | Descripcion | Disparo |
|--------|-------------|-------------|---------|
| `pix.charge.created` | `created` | QR code generado o cash-in iniciado | **Activo** |
| `pix.charge.paid` | `paid` | PIX recibido y liquidado | **Activo** |
| `pix.charge.expired` | `expired` | QR code expiro sin pago | **Activo** |
| `pix.charge.cancelled` | `cancelled` | QR code cancelado antes del pago | Registrado, aun no disparado |
| `pix.payout.queued` | `queued` | PIX enviado encolado por limite operacional | **Activo** |
| `pix.payout.processing` | `processing` | PIX enviado, aguardando confirmacion | **Activo** |
| `pix.payout.confirmed` | `settled` | PIX enviado y confirmado (**terminal**) | **Activo** |
| `pix.payout.failed` | `rejected` | PIX enviado rechazado (**terminal**) | **Activo** |
| `pix.payout.returned` | `returned` | PIX enviado devuelto | **Activo** |
| `pix.refund.requested` | `requested` | Solicitud de devolucion recibida (infraccion BACEN); bloqueo cautelar creado en el saldo del cliente | **Activo** |
| `pix.refund.completed` | `settled` / `completed` | Analisis de la defensa finalizado y devolucion ejecutada (o liberada) | **Activo** |
| `pix.return.received` | `settled` | Devolucion PIX recibida (credito) | **Activo** |
| `pix.infraction.created` | `ACKNOWLEDGED` | Infraccion PIX reportada por la contraparte via BACEN DICT | **Activo** |
| `pix.infraction.resolved` | `CLOSED` / `CANCELLED` | Infraccion resuelta por decision final o cancelacion de la contraparte | **Activo** |
| `pix.infraction.defense_submitted` | `defense_submitted` | Defensa enviada por el merchant | **Activo** |
| `tef.transfer.sent` | `settled` | TEF entre cuentas Monetarie liquidada para la cuenta de origen | **Activo** |
| `tef.transfer.received` | `settled` | TEF entre cuentas Monetarie liquidada para la cuenta de destino | **Activo** |
| `tef.transfer.failed` | `failed` | TEF entre cuentas Monetarie rechazada o no liquidada | **Activo** |
| `webhook.test` | `test` | Prueba manual. **Disponible solo via portal Admin/Merchant - no hay endpoint External API para disparar** | Disparo manual |

::: warning `pix.charge.cancelled` aun no es disparado
El evento puede ser suscrito, pero todavia no hay flujo publico de cancelacion de QR code que dispare este tipo de notificacion. Puede incluirlo en el array `events` - la API acepta - pero ninguna notificacion llegara a su endpoint hasta que el flujo sea disponibilizado.
:::

::: tip Cualquier evento fuera de esa tabla es rechazado
Al crear un webhook, cada evento solicitado se valida contra la lista publica de eventos soportados. Eventos desconocidos (`boleto.paid`, `account.created`, `sta.file.*`, etc.) retornan `400` con error `events: contains invalid events: ...`.
:::

::: tip Payloads de cada evento
Ejemplos de payload completos para cada evento estan en [Payloads de los Webhooks](/es/webhooks-payloads).
:::

### Ejemplo

```bash
BODY='{"url":"https://seusite.com.br/webhook","events":["pix.charge.paid","pix.payout.confirmed"]}'
HMAC=$(echo -n "$BODY" | openssl dgst -sha512 -hmac "$CLIENT_SECRET" | awk '{print $2}')

curl -X POST https://api.monetarie.com/api/external/webhooks \
  -H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -H "hmac: $HMAC" \
  -d "$BODY"
```

### Respuesta de Exito (201)

```json
{
  "worked": true,
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "url": "https://seusite.com.br/webhook",
  "events": ["pix.charge.paid", "pix.payout.confirmed"],
  "secret": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
  "description": null,
  "is_active": true,
  "created_at": "2026-03-07T15:30:00Z"
}
```

::: tip Formato del `id`
El `id` del webhook es un UUID v4 canonico (36 caracteres con guiones). Use ese valor directo en `DELETE /api/external/webhooks/:id`.
:::

### Respuesta de Error (422)

```json
{
  "worked": false,
  "detail": "URL deve utilizar HTTPS"
}
```

::: warning Solo HTTPS por Default
La URL del webhook debe usar HTTPS. URLs con HTTP seran rechazadas, a menos que `allow_insecure: true` sea enviado en el registro.
:::

::: danger Importante - Secret del Webhook
El campo `secret` retornado en la respuesta del registro es la clave usada para firmar las entregas del webhook (HMAC-SHA256). **Almacene ese valor con seguridad** tan pronto lo reciba - es el que valida que una notificacion vino realmente de Monetarie.

**NO confunda con `client_secret`:**
- `client_secret` = autenticacion de sus solicitudes a la API (header Authorization)
- `secret` del webhook = verificacion de la firma de las entregas recibidas (header X-Monetarie-Signature)

Si usted no envia el campo `secret` en el registro, un valor aleatorio sera generado automaticamente y retornado en la respuesta.

Vea [Validacion de Webhooks](/es/webhooks) para ejemplos de como verificar la firma.
:::

::: tip Recuperando el secret despues
El `secret` es retornado tanto en `POST /api/external/webhooks` (creacion) como en `GET /api/external/webhooks` y `GET /api/external/webhooks/:id` (consulta). Si usted perdio el valor, basta consultar el webhook nuevamente via GET.

En una futura version este comportamiento puede ser restringido (exhibir solo en la creacion); recomendamos almacenar el secret en un secreto gestionado (vault, SSM, etc.) en el momento del registro.
:::

### URLs HTTP

Por defecto, los webhooks exigen HTTPS para garantizar la seguridad de los datos en transito. Para utilizar HTTP, envie `allow_insecure: true` en el registro del webhook.

::: warning Atencion
URLs HTTP transmiten datos sin cifrado. La seguridad y el secreto de las informaciones transitadas quedan bajo entera responsabilidad del cliente. Monetarie realiza la entrega del webhook normalmente, pero no se responsabiliza por interceptacion o fuga de datos en conexiones no cifradas.
:::

::: warning URLs privadas son siempre bloqueadas
Incluso con `allow_insecure: true`, URLs apuntando para direcciones privadas/internas son rechazadas:
- `localhost` / `127.x.x.x`
- RFC1918: `10.x.x.x`, `192.168.x.x`, `172.16-31.x.x`
- TLDs internos: `.local`, `.internal`

Webhooks deben apuntar para URLs publicas accesibles por internet.
:::

---

## Listar Webhooks

```
GET /api/external/webhooks
```

### Headers

| Header | Tipo | Obligatorio | Descripcion |
|--------|------|-------------|-------------|
| `Authorization` | String | Si | `ApiKey {client_id}:{client_secret}` |

### Ejemplo

```bash
curl -X GET https://api.monetarie.com/api/external/webhooks \
  -H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET"
```

### Respuesta de Exito (200)

Retorna un **array** de objetos (no envuelto en `{"worked": true}`). Cada item contiene los mismos campos del registro, incluyendo `secret`.

```json
[
  {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "url": "https://seusite.com.br/webhook",
    "events": ["pix.charge.paid", "pix.payout.confirmed"],
    "description": null,
    "account_id": 10014,
    "is_active": true,
    "allow_insecure": false,
    "status": "active",
    "secret": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
    "created_at": "2026-03-07T15:30:00",
    "updated_at": "2026-03-07T15:30:00"
  }
]
```

| Campo | Tipo | Descripcion |
|-------|------|-------------|
| `id` | string (UUID) | Identificador del webhook |
| `url` | string | URL de destino |
| `events` | array | Eventos suscritos |
| `description` | string o null | Descripcion opcional |
| `account_id` | integer o null | Cuenta asociada. `null` = webhook global para la API key (si soportado) |
| `is_active` | boolean | `false` = webhook pausado, ninguna delivery es disparada |
| `allow_insecure` | boolean | `true` = URLs HTTP aceptadas |
| `status` | string | Derivado de `is_active` - `"active"` o `"inactive"` |
| `secret` | string | **Clave HMAC-SHA256** usada para firmar entregas. Retornada en el LIST para permitir recuperacion caso el cliente haya perdido el valor original |
| `created_at` / `updated_at` | string ISO 8601 | Timestamps en UTC, formato `NaiveDateTime` (sin sufijo `Z`, ej: `"2026-03-07T15:30:00"`). Diferente de otros campos de fecha en payloads de webhook (`paid_at`, `returned_at`, `expired_at`) que usan `DateTime` ISO 8601 con `Z` final. Siempre asuma UTC para los campos del webhook object |

---

## Remover Webhook

```
DELETE /api/external/webhooks/:id
```

### Headers

| Header | Tipo | Obligatorio | Descripcion |
|--------|------|-------------|-------------|
| `Authorization` | String | Si | `ApiKey {client_id}:{client_secret}` |

### Path Parameters

| Parametro | Tipo | Obligatorio | Descripcion |
|-----------|------|-------------|-------------|
| `id` | String (UUID) | Si | ID del webhook (UUID v4 retornado por el endpoint de creacion) |

### Ejemplo

```bash
curl -X DELETE https://api.monetarie.com/api/external/webhooks/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
  -H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET"
```

### Respuesta de Exito (204)

**HTTP 204 No Content** - body vacio. El webhook fue removido con exito; ninguna delivery pendiente sera disparada.

::: tip Primera llamada: 204. Subsecuentes: 404
La primera llamada exitosa retorna `204 No Content`. Llamadas subsecuentes con el mismo `id` retornan `404 { "errors": { "not_found": "webhook not found" } }` - el webhook ya fue removido. Esto **no es idempotencia estricta HTTP** (en que toda llamada retornaria 204) - es el comportamiento estandar de DELETE cuando el recurso deja de existir. Escriba su integracion para aceptar tanto `204` como `404` como "el webhook no esta mas activo".
:::

### Respuesta de Error (400)

Formato de `id` invalido (no es UUID):

```json
{
  "errors": {
    "bad_request": "id must be a valid UUID"
  }
}
```

### Respuesta de Error (404)

Webhook no existe o no pertenece a su cuenta:

```json
{
  "errors": {
    "not_found": "webhook not found"
  }
}
```
