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

# Webhooks

> React to Bank Transfer events in real time with webhooks — completed, failed, chargeback, and reconciliation notifications without polling.

Webhooks let your system react to transfer events in real time, without polling. The plugin sends a notification to your endpoint when a transfer completes, fails, or needs attention.

## Available events

***

Each event lists the transfer types it applies to (in parentheses), when it fires, and the recommended action.

### Transfer lifecycle (TED OUT, P2P)

#### `transfer.initiated` (TED OUT)

* **Trigger**: the plugin created the TED OUT transfer record after it confirmed the initiation.
* **Action**: update the transfer status in your system. Show "transfer in progress" to the customer.

#### `transfer.processing_started` (TED OUT)

* **Trigger**: the TED OUT transfer entered processing (status path CREATED to PENDING to PROCESSING).
* **Action**: show the customer that the transfer is in progress.

#### `transfer.rejected` (TED OUT)

* **Trigger**: JD SPB rejected the transfer request before acceptance (invalid data or rule violation).
* **Action**: notify the customer of the rejection. The plugin already cancelled the fund hold.

#### `transfer.completed` (P2P)

* **Trigger**: the P2P transfer settled successfully.
* **Action**: notify the customer. Generate a receipt. Update the balance display.

### Reconciliation (TED OUT, TED IN)

#### `transfer.reconciliation_required`

* **Trigger**: a transfer with an unknown outcome moved to reconciliation.
* **Action**: track the transfer as pending. Do not assume success or failure.

#### `transfer.reconciliation_resolved`

* **Trigger**: reconciliation finished and the transfer reached a final outcome.
* **Action**: update the transfer to its final status.

#### `transfer.reconciliation_exhausted`

* **Trigger**: reconciliation stopped after the maximum number of attempts.
* **Action**: escalate the transfer for manual operator review.

#### `transfer.reconciliation_failed`

* **Trigger**: a reconciliation attempt hit a deterministic error, which failed the transfer.
* **Action**: treat the transfer as failed and investigate.

### Incoming transfers (TED IN)

#### `transfer_incoming.completed`

* **Trigger**: the plugin received an inbound TED, found the recipient, and applied the credit.
* **Action**: notify the recipient that the funds arrived. Update the balance display.

#### `transfer_incoming.chargeback`

* **Trigger**: a chargeback message arrived for a completed TED IN (STR0010R2).
* **Action**: freeze the credited amount. Start a review with your compliance team.

#### `transfer_incoming.undeliverable`

* **Trigger**: the plugin could not credit an inbound TED (for example, it did not find the recipient account).
* **Action**: investigate the transfer. The plugin may return it to the origin bank.

### Returns and initiation

#### `transfer_outgoing.devolution_notified` (TED OUT)

* **Trigger**: a return (devolução) arrived for an outgoing transfer.
* **Action**: reconcile the returned funds against the original transfer.

#### `payment_initiation.created` (TED OUT, P2P)

* **Trigger**: the plugin created a payment initiation (the pre-transfer step).
* **Action**: optional. Track initiations that await confirmation.

<Note>
  For TED OUT, the plugin does not emit `transfer.completed` yet. SPB confirms TED OUT completion asynchronously, and a future release will add this event. Until then, check TED OUT status with the [Get Transfer](/en/reference/midaz/plugins/ted/retrieve-transfer) endpoint or the reconciliation endpoint.
</Note>

## Configuring webhooks

***

Webhooks work per tenant. You register a destination in one of two ways.

**Self-service API (recommended).** Register one or more HTTPS endpoints through the webhook registration API. The server generates a `signingSecret` on creation and returns it **once**. Store it securely. Use it to verify the signature on every delivered event. You can also list, update, disable, and delete registrations, rotate the signing secret, and look up the accepted event types. The plugin derives the owning tenant from the bearer token, never from a request header.

* [Create a webhook registration](/en/reference/midaz/plugins/ted/create-webhook) — `POST /v1/webhooks`
* [List webhook registrations](/en/reference/midaz/plugins/ted/list-webhooks) — `GET /v1/webhooks`
* [Get](/en/reference/midaz/plugins/ted/get-webhook), [update](/en/reference/midaz/plugins/ted/update-webhook), and [delete](/en/reference/midaz/plugins/ted/delete-webhook) a registration
* [Rotate the signing secret](/en/reference/midaz/plugins/ted/rotate-webhook-signing-secret) — `POST /v1/webhooks/{webhookId}/signing-secret/rotate`
* [List supported event types](/en/reference/midaz/plugins/ted/list-webhook-event-types) — `GET /v1/webhooks/event-types`

**Enabling delivery (operator/env).** Set `WEBHOOK_ENABLED=true` to turn on outbound delivery. Delivery also requires RabbitMQ and the streaming outbox (`STREAMING_ENABLED=true`). Destinations come from the registrations above. There is no single static endpoint environment variable. You tune per-delivery behavior — timeout and max retries — at runtime through systemplane, not through env vars. See [Bank Transfer configuration](/en/rails/ted/jd/ted-configuration).

## Payload structure

***

The plugin delivers each event as an HTTPS POST. The request body is the event payload as JSON. The event type and the signature travel in HTTP headers, not in the body.

| Header                | Value                                                                                                                      |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `Content-Type`        | `application/json`.                                                                                                        |
| `X-Webhook-Event`     | The event type, for example `transfer.completed`.                                                                          |
| `X-Webhook-Timestamp` | Delivery time as a Unix timestamp (seconds). The signature covers this value.                                              |
| `X-Webhook-Signature` | HMAC-SHA256 signature over the timestamp and body, keyed with the registration `signingSecret`. Format: `v1,sha256=<hex>`. |

The body fields depend on the event type. Every payload carries `tenantId`, and transfer-scoped events also carry `transferId`. Amounts are decimal strings in the account currency, not centavos (for example, `100.00`).

Here is an example body for `transfer.completed` on a P2P transfer:

```json theme={null}
{
  "transferId": "019c96a0-ab10-7cde-f1a2-0e1f2a3b4c5d",
  "initiationId": "019c96a0-9a01-7bcd-e0f1-2a3b4c5d6e7f",
  "tenantId": "019c96a0-0a98-7287-9a31-786e0809c769",
  "ledgerId": "019c96a0-1b20-7def-a1b2-c3d4e5f60718",
  "senderAccountId": "019c96a0-2c30-7ef0-b2c3-d4e5f6071829",
  "recipientAccountId": "019c96a0-3d40-7f01-c3d4-e5f60718293a",
  "midazTransactionId": "019c96a0-cd10-7eee-bbbb-3333bbbb4444",
  "confirmationNumber": "20260121001",
  "status": "COMPLETED",
  "transferType": "P2P",
  "amount": "100.00",
  "feeAmount": "0.00",
  "totalAmount": "100.00",
  "completedAt": "2026-01-21T17:35:00Z"
}
```

The `transfer.completed` payload carries the amounts, the accounts, and the `midazTransactionId`. For events with a smaller payload, or to read the full transfer record, fetch the transfer from [Get Transfer](/en/reference/midaz/plugins/ted/retrieve-transfer) with its `transferId`.

<Note>
  Payload fields differ per event type. To read every field of a transfer, use the [Get Transfer](/en/reference/midaz/plugins/ted/retrieve-transfer) endpoint.
</Note>

## Handling delivery failures

***

Your endpoint must respond with a 2xx status within 5 seconds (the `webhook.timeout_ms` default). If it does not, the plugin retries the delivery with exponential backoff and full jitter. After the first attempt, the plugin makes up to 3 more attempts (the `webhook.max_retries` default), which is 4 delivery attempts in total. The backoff base is 1 second and doubles per attempt. Full jitter applies to each delay:

| Attempt     | Delay before this attempt |
| ----------- | ------------------------- |
| 1 (initial) | Immediate                 |
| 2           | Random in `[0, 1000 ms]`  |
| 3           | Random in `[0, 2000 ms]`  |
| 4           | Random in `[0, 4000 ms]`  |

After all attempts fail (4 by default), the event moves to a dead-letter queue (DLQ). Set alerts on the DLQ to catch persistent delivery failures early. Tune `webhook.max_retries` through systemplane if your endpoint needs a longer or shorter retry budget. The `webhook.retry_backoff_ms` knob controls the broker reconnect backoff, not the per-delivery HTTP retry schedule above.

For reliable delivery, follow these rules:

* Respond within 5 seconds.
* Use HTTPS with a valid certificate.
* Return 200 even for events you ignore.
* Move heavy processing to a background queue. Keep the webhook handler fast.

## Idempotency

***

<Note>
  Your endpoint may receive the same event more than once. Use the `transferId` from the body and the `X-Webhook-Event` header to deduplicate. If you already processed that combination, return 200 and take no further action.
</Note>

## For developers

***

For signature validation code (JavaScript, Python, Go), retry implementation, and the full integration checklist, see the [Bank Transfer developer guide](/en/rails/ted/jd/ted-developer-guide).
