# Register Webhook

Endpoints to create, list, and remove notification webhooks.

---

## Create Webhook

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

### Headers

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | String | Yes | `ApiKey {client_id}:{client_secret}` |
| `Content-Type` | String | Yes | `application/json` |
| `hmac` | String | Yes | HMAC-SHA512 signature of the body ([learn more](/en/hmac)) |

### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `url` | String | Yes | -- | URL to receive notifications (HTTPS by default) |
| `events` | Array | **Yes** | -- | List of events to subscribe to. Must be a **non-empty** array with at least one valid event from the table below. Omitting the field returns `400 {"errors": {"events": ["can't be blank"]}}`. |
| `secret` | String | No | auto-generated | Key for HMAC-SHA256 signing of deliveries. If omitted, a random value is auto-generated. |
| `description` | String | No | `null` | Free description of the webhook for internal identification |
| `allow_insecure` | Boolean | No | `false` | Allow HTTP (non-HTTPS) URLs. Data security is the client's responsibility. |

Available events (PIX, TEF between Monetarie accounts, and operational webhook test):

| Event | Status body | Description | Dispatch |
|-------|-------------|-------------|----------|
| `pix.charge.created` | `created` | QR code generated or cash-in initiated | **Active** |
| `pix.charge.paid` | `paid` | PIX received and settled | **Active** |
| `pix.charge.expired` | `expired` | QR code expired without payment | **Active** |
| `pix.charge.cancelled` | `cancelled` | QR code cancelled before payment | Registered, not yet dispatched |
| `pix.payout.queued` | `queued` | PIX send queued by operational limit | **Active** |
| `pix.payout.processing` | `processing` | PIX sent, awaiting confirmation | **Active** |
| `pix.payout.confirmed` | `settled` | PIX sent and confirmed (**terminal**) | **Active** |
| `pix.payout.failed` | `rejected` | PIX send rejected (**terminal**) | **Active** |
| `pix.payout.returned` | `returned` | Sent PIX was returned | **Active** |
| `pix.refund.requested` | `requested` | Refund request received (BACEN infraction); preventive block created on the client balance | **Active** |
| `pix.refund.completed` | `settled` / `completed` | Defense analysis finalized and refund executed (or released) | **Active** |
| `pix.return.received` | `settled` | PIX return received (credit) | **Active** |
| `pix.infraction.created` | `ACKNOWLEDGED` | PIX infraction reported by counterparty via BACEN DICT | **Active** |
| `pix.infraction.resolved` | `CLOSED` / `CANCELLED` | Infraction resolved by final decision or counterparty cancellation | **Active** |
| `pix.infraction.defense_submitted` | `defense_submitted` | Defense submitted by the merchant | **Active** |
| `tef.transfer.sent` | `settled` | TEF between Monetarie accounts settled for the origin account | **Active** |
| `tef.transfer.received` | `settled` | TEF between Monetarie accounts settled for the destination account | **Active** |
| `tef.transfer.failed` | `failed` | TEF between Monetarie accounts rejected or not settled | **Active** |
| `webhook.test` | `test` | Manual test. **Available only via Admin/Merchant portal - there is no External API endpoint to dispatch** | Manual dispatch |

::: warning `pix.charge.cancelled` is not yet dispatched
The event can be subscribed to, but there is no public QR code cancellation flow currently dispatching this notification type. You may include it in the `events` array - the API accepts it - but no notification will arrive at your endpoint until the flow is made available.
:::

::: tip Any event outside this table is rejected
When creating a webhook, each requested event is validated against the public list of supported events. Unknown events (`boleto.paid`, `account.created`, `sta.file.*`, etc.) return `400` with error `events: contains invalid events: ...`.
:::

::: tip Payloads for each event
Full payload examples for each event are in [Webhook Payloads](/en/webhooks-payloads).
:::

### Example

```bash
BODY='{"url":"https://yoursite.com/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"
```

### Success Response (201)

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

::: tip Format of `id`
The webhook `id` is a canonical UUID v4 (36 characters with hyphens). Use this value directly in `DELETE /api/external/webhooks/:id`.
:::

### Error Response (422)

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

::: warning HTTPS only by default
The webhook URL must use HTTPS. URLs with HTTP will be rejected unless `allow_insecure: true` is sent on registration.
:::

::: danger Important - Webhook Secret
The `secret` field returned in the registration response is the key used to sign webhook deliveries (HMAC-SHA256). **Store this value securely** as soon as you receive it - it is what validates that a notification actually came from Monetarie.

**Do NOT confuse with `client_secret`:**
- `client_secret` = authentication for your API requests (Authorization header)
- Webhook `secret` = verification of received delivery signatures (X-Monetarie-Signature header)

If you do not send the `secret` field at registration, a random value will be auto-generated and returned in the response.

See [Webhook Validation](/en/webhooks) for examples of how to verify the signature.
:::

::: tip Retrieving the secret later
The `secret` is returned both in `POST /api/external/webhooks` (creation) and in `GET /api/external/webhooks` and `GET /api/external/webhooks/:id` (query). If you lose the value, simply query the webhook again via GET.

In a future version this behavior may be restricted (display only on creation); we recommend storing the secret in a managed secret (vault, SSM, etc.) at registration time.
:::

### HTTP URLs

By default, webhooks require HTTPS to guarantee data security in transit. To use HTTP, send `allow_insecure: true` on registration.

::: warning Attention
HTTP URLs transmit data without encryption. The security and confidentiality of the information exchanged is the client's full responsibility. Monetarie performs webhook delivery normally but assumes no liability for interception or data leakage on unencrypted connections.
:::

::: warning Private URLs are always blocked
Even with `allow_insecure: true`, URLs pointing to private/internal addresses are rejected:
- `localhost` / `127.x.x.x`
- RFC1918: `10.x.x.x`, `192.168.x.x`, `172.16-31.x.x`
- Internal TLDs: `.local`, `.internal`

Webhooks must point to public URLs accessible over the internet.
:::

---

## List Webhooks

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

### Headers

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | String | Yes | `ApiKey {client_id}:{client_secret}` |

### Example

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

### Success Response (200)

Returns an **array** of objects (not wrapped in `{"worked": true}`). Each item contains the same fields as registration, including `secret`.

```json
[
  {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "url": "https://yoursite.com/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"
  }
]
```

| Field | Type | Description |
|-------|------|-------------|
| `id` | string (UUID) | Webhook identifier |
| `url` | string | Destination URL |
| `events` | array | Subscribed events |
| `description` | string or null | Optional description |
| `account_id` | integer or null | Associated account. `null` = webhook global for the API key (if supported) |
| `is_active` | boolean | `false` = webhook paused, no delivery is dispatched |
| `allow_insecure` | boolean | `true` = HTTP URLs accepted |
| `status` | string | Derived from `is_active` - `"active"` or `"inactive"` |
| `secret` | string | **HMAC-SHA256 key** used to sign deliveries. Returned on LIST to allow recovery if the client lost the original value |
| `created_at` / `updated_at` | string ISO 8601 | Timestamps in UTC, format `NaiveDateTime` (without `Z` suffix, e.g., `"2026-03-07T15:30:00"`). Different from other date fields in webhook payloads (`paid_at`, `returned_at`, `expired_at`) which use `DateTime` ISO 8601 with trailing `Z`. Always assume UTC for the webhook object fields |

---

## Remove Webhook

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

### Headers

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | String | Yes | `ApiKey {client_id}:{client_secret}` |

### Path Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `id` | String (UUID) | Yes | Webhook ID (UUID v4 returned by the creation endpoint) |

### Example

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

### Success Response (204)

**HTTP 204 No Content** - empty body. The webhook was successfully removed; no pending delivery will be dispatched.

::: tip First call: 204. Subsequent: 404
The first successful call returns `204 No Content`. Subsequent calls with the same `id` return `404 { "errors": { "not_found": "webhook not found" } }` - the webhook was already removed. This is **not strict HTTP idempotency** (where every call would return 204) - it is the standard DELETE behavior when the resource ceases to exist. Write your integration to accept both `204` and `404` as "the webhook is no longer active".
:::

### Error Response (400)

Invalid `id` format (not UUID):

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

### Error Response (404)

Webhook does not exist or does not belong to your account:

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