# Webhooks

Webhooks notify your application, in real time, about the outcome of operations. You register a URL, choose the events and receive a signed POST on each occurrence. Every endpoint is scoped to the partner's credentials, and each delivery carries the `accountId` of the account involved, so you can route it internally.

## Available events

Every event below is actually emitted by the platform. The catalog at `GET /webhooks/events` returns exactly this list.

### PIX

| Event | When it fires |
|---|---|
| `pix.charge.created` | Charge or QR created |
| `pix.charge.paid` | Charge paid, inbound PIX credited |
| `pix.payout.queued` | Outbound PIX queued for processing |
| `pix.payout.processing` | Outbound PIX in processing (intermediate accept) |
| `pix.payout.confirmed` | Outbound PIX settled |
| `pix.payout.failed` | Outbound PIX rejected or expired |
| `pix.payout.returned` | Outbound PIX returned |
| `pix.refund.requested` | PIX refund requested |
| `pix.refund.completed` | PIX refund completed |
| `pix.refund.failed` | PIX refund rejected |
| `pix.return.received` | Return received |
| `pix.received` | PIX received in the account, with no charge issued |

### PIX keys

| Event | When it fires |
|---|---|
| `pix.key.registered` | PIX key registered |
| `pix.key.deleted` | PIX key deleted |
| `pix.key.blocked` | PIX key blocked |
| `pix.key.unblocked` | PIX key unblocked |

### Portability

| Event | When it fires |
|---|---|
| `pix.claim.created` | Portability claim created |
| `pix.claim.acknowledged` | Claim acknowledged |
| `pix.claim.confirmed` | Claim confirmed |
| `pix.claim.cancelled` | Claim cancelled |
| `pix.claim.completed` | Claim completed |

### TED and transfers

| Event | When it fires |
|---|---|
| `ted.confirmed` | TED settled |
| `ted.failed` | TED rejected or expired |
| `ted.received` | TED received in the account |
| `ted.refund.requested` | Refund of a received TED requested |
| `ted.refund.completed` | Refund of a received TED settled |
| `ted.refund.failed` | Refund of a received TED rejected |
| `transfer.confirmed` | Internal transfer completed |
| `transfer.failed` | Internal transfer failed |
| `transfer.received` | Internal transfer received |

### Infractions (MED)

| Event | When it fires |
|---|---|
| `pix.infraction.created` | Infraction/MED opened on an account |
| `pix.infraction.resolved` | Infraction analyzed and resolved |
| `pix.infraction.defense_submitted` | Infraction defense submitted by the client |
| `pix.med.created` | MED intervention opened through the Partner API |
| `pix.med.completed` | MED recovery completed, funds returned |
| `pix.med.cancelled` | MED intervention cancelled |

### Fees

| Event | When it fires |
|---|---|
| `fee.charged` | Fee charged to an account |

### Accounts

| Event | When it fires |
|---|---|
| `account.created` | Account created |
| `account.blocked` | Account blocked |
| `account.unblocked` | Account unblocked |
| `account.closed` | Account closed |

### Test

| Event | When it fires |
|---|---|
| `webhook.test` | Manual test delivery |

The success events (`pix.charge.paid`, `pix.payout.confirmed`, `pix.refund.completed`, `pix.return.received`) are only delivered after the operation is settled.

## Register a webhook

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

{
  "url": "https://your-system.com/webhooks/monetarie",
  "events": ["pix.payout.confirmed", "pix.payout.failed", "pix.charge.paid"],
  "description": "Main integration",
  "is_active": true
}
```

- **Scope:** `webhook:write` · **Required:** `url`, `events`

Response `201`:

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

::: warning Signing secret
Registration does **not** return the signing secret. To obtain (or rotate) the secret, call `POST /webhooks/:id/rotate-secret`, which returns the secret in plain text **once**. Store it securely.
:::

The URL must be public, `http` or `https`, and cannot point to private or internal addresses.

## Get and rotate the secret

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

- **Scope:** `webhook:write`

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

The `new_secret` is shown only in this response. A new rotation invalidates the previous one.

## Manage webhooks

```http
GET    /api/partner/v1/webhooks              # list (webhook:read)
GET    /api/partner/v1/webhooks/{id}         # detail (webhook:read)
PUT    /api/partner/v1/webhooks/{id}         # update url/events/description/is_active (webhook:write)
DELETE /api/partner/v1/webhooks/{id}         # delete, returns 204 (webhook:write)
POST   /api/partner/v1/webhooks/{id}/test    # send a test delivery (webhook:write)
POST   /api/partner/v1/webhooks/{id}/replay  # replay the latest delivery (webhook:write)
GET    /api/partner/v1/webhooks/events       # event catalog (webhook:read)
```

`replay` returns `404` when there is no delivery yet to replay.

## Delivery format

Each event arrives as a `POST` to the configured URL. The body is the JSON of the event, flat (no envelope) and with keys in `camelCase`, and the metadata goes in the headers:

| Header | Description |
|---|---|
| `X-Monetarie-Event-Type` | Event type, for example `pix.payout.confirmed` |
| `X-Monetarie-Event-Id` | Delivery identifier, for deduplication |
| `X-Monetarie-Timestamp` | Signing instant, in Unix seconds |
| `X-Monetarie-Signature` | Signature `sha256=<hex>` |
| `User-Agent` | `Monetarie-Webhook/1.0` |

Identify the event by the `X-Monetarie-Event-Type` header; the body does not repeat the type. Deduplicate by `X-Monetarie-Event-Id`, because the same delivery may arrive more than once.

## Event payloads

All bodies use `camelCase` keys and all monetary values are in **cents**. The fields below are the ones your integration needs; an event may carry additional informational fields, so always locate a value by its key, never by position.

### Outbound PIX

Applies to `pix.payout.queued`, `pix.payout.processing`, `pix.payout.confirmed` and `pix.payout.failed`. What changes between them is the `status`.

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

| Event | `status` |
|---|---|
| `pix.payout.queued` | `queued` |
| `pix.payout.processing` | `processing` |
| `pix.payout.confirmed` | `settled` |
| `pix.payout.failed` | `rejected`, with the reason in `errorReason` |

### Returned outbound PIX

`pix.payout.returned` signals that a PIX you sent was returned by the receiver. It carries the returned value in `amount` (which can be partial) and `status: "returned"`, with the `endToEndId` of the original operation. `returnId` is the identifier of the return within the PIX arrangement.

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

### Refund requested, completed and rejected

`pix.refund.requested` is emitted when you request the refund of a received PIX. The `endToEndId` field carries the identifier of the original transaction being refunded.

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

`pix.refund.completed` has the same format as outbound PIX, with `status: "settled"`, when the refund settles. `pix.refund.failed` signals that the refund was rejected: the value stays in the account and counts again towards the refundable remainder, with the reason in `errorReason`. In both outcomes, correlate by the `transactionId` key, which is the `refundId` returned when the refund was accepted; `endToEndId` may come null.

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

### Charge created

`pix.charge.created` confirms the creation of a charge or QR. It carries the ready-to-pay BR Code in `brcode`.

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

### Inbound PIX

`pix.charge.paid` confirms the payment of one of your charges. Correlate by the charge's `txId` and by the `externalId`, when provided at creation.

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

### Return received

`pix.return.received` signals that a return landed in the account. It can be partial, flagged by `isPartial`.

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

### PIX received with no charge

`pix.received` signals a PIX that landed in the account with no charge issued, such as a direct transfer to one of the account's keys. When the credit comes from a charge of yours, the event is `pix.charge.paid`.

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

### PIX key lifecycle

`pix.key.registered`, `pix.key.deleted`, `pix.key.blocked` and `pix.key.unblocked` track the account's keys and share the same format; `pix.key.blocked` also includes a `reason` field.

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

### Portability claim

`pix.claim.created` signals the creation of a key portability claim. `pix.claim.acknowledged`, `pix.claim.confirmed`, `pix.claim.cancelled` and `pix.claim.completed` have the same format and track the following stages.

```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` and `ted.failed` track the outcome of a sent TED.

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

The `status` is `settled` in `ted.confirmed`, and `rejected` or `timeout` in `ted.failed`, with the reason in `errorReason`.

### TED received

`ted.received` signals that a TED from another institution was credited to the account.

```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"
}
```

### Refund of a received TED

`ted.refund.requested` confirms the acceptance of a refund you commanded for a received TED; `ted.refund.completed` signals that the refund settled in the SPB, and `ted.refund.failed` signals that the refund did not complete and the reserved amount is available in the account again. Correlate by the `numCtrlStr` of the original credit.

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

In `ted.refund.completed` the delivery also carries `spbNumCtrl`, the control number of the return message (STR0010), and does not repeat the `status` field. In `ted.refund.failed`, `errorCode` and `errorMessage` carry the reason reported by the SPB.

### Internal transfer

`transfer.confirmed` and `transfer.failed` track the transfer between two partner accounts.

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

### Internal transfer received

`transfer.received` is the credit leg of an internal transfer, delivered for the receiving account. `sourceAccountId` identifies the source account.

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

### Infraction (MED)

`pix.infraction.created` signals the opening of an infraction or MED on an account, and `pix.infraction.resolved` carries the result of the analysis.

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

The `status` is `created` on opening, and `founded` or `unfounded` on resolution.

### Infraction defense

`pix.infraction.defense_submitted` confirms that the defense submitted by the client for an infraction with a precautionary block was recorded. `blockId` identifies the block; the outcome of the analysis arrives through `pix.infraction.resolved`.

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

### MED intervention

`pix.med.created` confirms the opening of a MED intervention requested through the Partner API. `pix.med.completed` signals that the recovery completed with the funds returned, and `pix.med.cancelled` signals the cancellation. `endToEndId` carries the root transaction of the intervention.

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

The `status` is `CREATED` on opening, `REFUND_COMPLETED` or `COMPLETED` on completion and `CANCELLED` on cancellation. The `amount` field (cents) comes in the outcomes reported by the PIX network; on opening and on cancellations commanded through the API the delivery may come without it.

### Fee charged

`fee.charged` signals that a fee was charged to an account. `originTransactionId` and `originalAmount` point to the transaction that originated the charge.

```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"
}
```

### Account created

`account.created` signals the opening of an account. The `status` can be `active` or `blocked`, depending on the onboarding verification.

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

### Account blocked, unblocked and closed

`account.blocked`, `account.unblocked` and `account.closed` share the same format. The `status` is `blocked` on block, `active` on unblock and `closed` on closure.

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

### Test

`webhook.test` is the manual delivery, useful to validate your URL and the signature verification. `POST /webhooks/{id}/test` delivers the event only to the webhook given in the path, even if it does not subscribe to `webhook.test`; no other webhook receives the test delivery.

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

## Validate the signature

The signature is an HMAC-SHA256, in lowercase hexadecimal, over the string `"{timestamp}.{raw_body}"`, using the webhook secret. The header comes as `sha256=<hex>`. Compare the computed value with the header, dropping the `sha256=` prefix. Use the raw body as received, without reserializing the JSON.

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

function verify(rawBody, signatureHeader, timestamp, secret) {
  const content = `${timestamp}.${rawBody}`
  const expected = crypto.createHmac('sha256', secret).update(content).digest('hex')
  return `sha256=${expected}` === signatureHeader
}
```

Reject the delivery if the signature does not match.

## Automatic retry

An unsuccessful delivery is retried with backoff, up to 8 attempts: immediate, 30s, 2min, 10min, 30min, 1h, 2h and 4h. Respond `2xx` to acknowledge receipt. The responses `400`, `401`, `403`, `404`, `410` and `422` are treated as permanent failure, with no retry.
