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

# Webhooks

> How webhook events drive your application's business logic — what to do when each event fires.

Webhooks are how your application **reacts to what happens at Etherfuse**. A transaction settles, a customer passes KYC, a bank account goes active — Etherfuse sends a signed `POST` to your endpoint the moment it happens, and your app does the corresponding work in your own system: credit a balance, unlock a feature, notify a user, reconcile an order.

The key mental model: **a webhook is the trigger for a state change in *your* product.** You don't poll Etherfuse asking "is it done yet?" — you let the event tell you, then act on the `status` in the payload.

> [!NOTE]
>
> **Webhooks vs. polling.** Webhooks fire the instant a state changes. Polling the list endpoints is slower (newly created orders take a moment to index) and wasteful (most requests return "no change"). Use webhooks for anything time-sensitive.

## What to do on each event

This is the part most integrations care about: when event X arrives, what should your app do? Each flow below maps the lifecycle to the actions you take in your own system.

> [accordion] Onramp — customer buys tokens with MXN
>
> Subscribe to **`order_updated`**.
>
> | Status | What happened | What your app does |
> | - | - | - |
> | `created` | Order created; awaiting the customer's MXN deposit to the `depositClabe` | Show the customer the `depositClabe` to pay; mark the order pending |
> | `funded` | MXN deposit received and confirmed | Optionally surface "payment received, delivering tokens" |
> | `completed` | Tokens delivered to the wallet (`confirmedTxSignature` has the tx hash) | **Mark fulfilled, credit the user, send confirmation** |
> | `failed` | Order couldn't be completed | Notify the customer; reconcile |
> | `refunded` | Deposit didn't match the order amount; MXN returned | Reconcile and tell the customer funds were returned |
> | `canceled` | Canceled before funding | Close the order out in your system |

> [accordion] Offramp — customer sells tokens for MXN
>
> Subscribe to **`order_updated`**.
>
> | Status | What happened | What your app does |
> | - | - | - |
> | `created` | `burnTransaction` is ready for the customer to sign | Prompt the customer to sign and submit it (or use [anchor mode](https://docs.etherfuse.com/guides/testing-offramps.md#anchor-mode-stellar-only) on Stellar) |
> | `funded` | The customer's transaction is confirmed on-chain | Surface "selling, sending MXN" |
> | `completed` | MXN sent to the customer's bank account | **Mark the payout as sent** |
> | `finalized` | Reversal window has passed — funds can no longer be returned | Finalize your accounting for the order |
> | `failed` | Couldn't be completed | Notify the customer; reconcile |

> [accordion] Swap — token to token
>
> Subscribe to **`swap_updated`**.
>
> | Status | What happened | What your app does |
> | - | - | - |
> | `pending` | `sendTransaction` is ready for the customer to sign | Prompt the customer to sign and submit it |
> | `funds_received` | The source transaction is confirmed on-chain. Fires on Stellar and Monad only; on Solana the swap goes straight from `pending` to `completed` | Surface "swap in progress" |
> | `completed` | Target tokens delivered to the wallet | **Update the user's balances** |
> | `failed` | Couldn't be completed | Notify the customer; reconcile |
>
> **Reading the payload (not the status sequence):** `sendTransaction` is always present in the payload. It contains the transaction the customer needs to sign and submit — once signed, the field remains but is no longer actionable. On completion you get two hashes: `sendTransactionHash` (the customer's send to Etherfuse) and `receiveTransactionHash` (Etherfuse's delivery to the wallet). Which intermediate statuses fire **varies by chain** (some emit a distinct `funds_received` update, others don't), so key your logic off the **fields present in the payload** rather than expecting a particular status to arrive.

> [accordion] Onboarding & compliance — KYC, KYB, bank accounts
>
> Subscribe to **`kyc_updated`**, **`customer_updated`**, and **`bank_account_updated`**.
>
> The **`kyc_updated`** webhook carries a `status` field (one of the five values below), plus `approved: true|false` for back-compat:
>
> | `kyc_updated` status | What happened | What your app does |
> | - | - | - |
> | `not_started` | No KYC started yet | None |
> | `in_progress` | The customer started but hasn't submitted everything | Show "verification in progress" |
> | `submitted` | Submitted, awaiting review (manual or automated) | Show "verification pending" |
> | `approved` | The customer is cleared (`approved: true`) | **Unlock their ability to transact** |
> | `denied` | Rejected, or a prior approval was revoked (`updateReason` explains why) | Block transacting; show the reason |
>
> Bank accounts (`bank_account_updated`):
>
> | Status | What happened | What your app does |
> | - | - | - |
> | not yet `active` | The bank isn't usable until the SPEI provider accepts it | Wait for `bank_account_active` before enabling payouts; see [Bank account status](https://docs.etherfuse.com/guides/onboarding.md) |
> | `active` | Bank account accepted and usable | **Enable offramps/payouts to that account** |
>
> Gating access on these events is the most common compliance pattern: don't let a customer onramp, offramp, or swap until you've received `kyc_updated` with `status: "approved"` (or `customer_verified`) for them.
>
> **Webhook first, then ask what's left.** `kyc_updated` tells you a customer's verification moved. When you need to know *what specifically* is outstanding, say because you are driving the [KYC API](https://docs.etherfuse.com/guides/kyc-api.md) flow, read [`GET /ramp/customer/{customer_id}/kyc?requirements=true`](https://docs.etherfuse.com/api-reference/kyc/get-kyc-status) in response to the webhook rather than polling it on a timer: it resolves against the verification provider on every call. A `kyc_updated` that moves the customer from `submitted` back to `in_progress` means the next stage has arrived and the list holds new requirements. See [Requirements unlock in stages](https://docs.etherfuse.com/guides/onboarding.md#requirements-unlock-in-stages).

## How delivery works

**1. Register an endpoint**

Call [POST /ramp/webhook](https://docs.etherfuse.com/api-reference/webhooks/create-webhook.md) with your **HTTPS** URL and the event type. The response includes a signing **`secret`**, returned **only once** — store it securely.

**2. Receive the event**

When the event fires, Etherfuse sends a `POST` with the JSON payload to your URL, plus an `X-Signature` header.

**3. Verify the signature**

Recompute the HMAC-SHA256 signature with your secret and compare. See [Verifying Webhooks](https://docs.etherfuse.com/guides/verifying-webhooks.md) for Node and Python code.

**4. Respond 2xx, then do the work**

Acknowledge with a `2xx` immediately, then run your business logic **asynchronously**. A slow handler triggers unnecessary retries.

## Event types

| Event | Fires when | Payload reference |
| - | - | - |
| `order_updated` | An onramp/offramp order changes status | [order\_updated payload](https://docs.etherfuse.com/api-reference/webhooks/order_updated.md) |
| `swap_updated` | A swap changes status | [swap\_updated payload](https://docs.etherfuse.com/api-reference/webhooks/swap_updated.md) |
| `customer_updated` | A customer's verification status changes | [customer\_updated payload](https://docs.etherfuse.com/api-reference/webhooks/customer_updated.md) |
| `kyc_updated` | A customer's KYC status changes (e.g. approved or rejected after `/idv`) | [kyc\_updated payload](https://docs.etherfuse.com/api-reference/webhooks/kyc_updated.md) |
| `kyb_updated` | A business organization's KYB status changes | [kyb\_updated payload](https://docs.etherfuse.com/api-reference/webhooks/kyb_updated.md) |
| `bank_account_updated` | A bank account's verification status changes | [bank\_account\_updated payload](https://docs.etherfuse.com/api-reference/webhooks/bank_account_updated.md) |

**Full status reference (transition diagrams + entity statuses)**

**Onramp:**

```
created → funded → completed
    ↓        ↓
  failed   refunded
    ↓
 canceled
```

**Offramp:**

```
created → funded → completed → finalized
    ↓        ↓
  failed   failed
    ↓
 canceled
```

**Swap:**

```
created → funded → completed
    ↓        ↓
  failed   failed
```

**Customer statuses** (`customer_updated`): `customer_pending`, `customer_verified`, `customer_failed`

**Bank account statuses** (`bank_account_updated`): `bank_account_pending`, `bank_account_awaiting_deposit_verification`, `bank_account_active`, `bank_account_inactive`

**KYC statuses** (`kyc_updated`, in the `status` field): `not_started`, `in_progress`, `submitted`, `approved` (on-chain marking follows for Solana), `denied` (includes `updateReason`)

**KYB statuses** (`kyb_updated`): `not_started`, `in_progress` (the customer has something left to submit, including a note Etherfuse asked them to answer), `submitted` (everything the customer can do is done, Etherfuse's turn), `approved`, `denied`

## Payloads

Every webhook payload is a JSON object with a single key matching the event type; the value is the entity that changed, signed with your webhook secret (see [Verifying Webhooks](https://docs.etherfuse.com/guides/verifying-webhooks.md)). The full schema and a worked example for each event live on its reference page, linked from the [Event types](#event-types) table above.

Fields are omitted when `null` or not applicable to the entity's current state, so drive your logic off the fields **present** in the payload rather than expecting a fixed shape. A few specifics worth calling out:

* **`order_updated`** carries `burnTransaction`, `depositClabe`, and `confirmedTxSignature` only when relevant to the order type and status. For **embedded (non-custodial) wallets** it also carries the pending `approval` object (`approvalMessageId`, `approvalMessage`, `summary`) while the order awaits the owner's signature; there is no separate embedded-wallet transaction event. Re-read the order right before signing so the `approvalMessage` timestamp is fresh.
* **`swap_updated`** — `sendTransaction` is always present (the transaction to sign; it stays in the payload but is no longer actionable once signed); `receiveTransactionHash` appears only on completion.
* **`bank_account_updated`**: the bank is usable once its status is `active`; earlier statuses (`bank_account_pending`, `bank_account_awaiting_deposit_verification`) mean the SPEI provider hasn't accepted it yet (see [Bank account status](https://docs.etherfuse.com/guides/onboarding.md)).
* **`kyc_updated`**: on approval, `approved` is `true` and `updateReason` / `needsWork` / `userMessage` are omitted. `needsWork: true` can also arrive with no change to `status`, when a verification write you sent was accepted and then failed to apply; `userMessage` names what to send again (see [KYC API error handling](https://docs.etherfuse.com/guides/kyc-api.md#error-handling)). That pair is **point-in-time**: it reports one write that did not land, and is not stored. `GET /kyc` will not echo the `userMessage` afterwards, so act on the delivery or fall back to re-reading the requirements breakdown.
* **`kyb_updated`** carries the business's `organizationId`, `status`, and `approvedAt`. Unlike the fields above, `approvedAt` is always present: it is `null` until the business is approved, rather than being omitted, so read its value rather than its presence. It fires only when the status actually changes, never on individual document or answer edits.

## Reliability & best practices

* **Retries.** Failed deliveries (a non-`2xx` response or connection error) are retried up to **3 times** with **5-second** delays. Return a `2xx` promptly to avoid them.
* **Acknowledge fast, process async.** Hand the payload to a queue and return `2xx` right away; don't block the response on downstream work.
* **Handle events idempotently.** The same event can arrive more than once (e.g. a retry after your `2xx` was lost). Dedupe on the resource ID plus its status so reprocessing is harmless — this pairs naturally with the [client-generated UUIDs](https://docs.etherfuse.com/idempotency.md) you already use for orders.
* **Don't rely on ordering.** Drive your logic off the `status` *in the payload*, not the order in which deliveries arrive.
* **Expose a public HTTPS endpoint.** Register it by domain name, not IP address; Etherfuse only delivers to public addresses, so a URL that resolves to a private or loopback address (or redirects to one) gets no deliveries. For local development, use a tunnel such as [ngrok](https://ngrok.com) so Etherfuse can reach your handler.

## Verifying signatures

Every delivery is signed so you can confirm it's authentic: `X-Signature: sha256={hex}`, computed as HMAC-SHA256 over the [RFC 8785](https://datatracker.ietf.org/doc/html/rfc8785)-canonicalized JSON body using your webhook secret. Full walkthrough with Node/Python code: **[Verifying Webhooks](https://docs.etherfuse.com/guides/verifying-webhooks.md)**.

## Managing webhooks

* [Create a subscription](https://docs.etherfuse.com/api-reference/webhooks/create-webhook.md) — `POST /ramp/webhook` (returns the one-time secret)
* [List subscriptions](https://docs.etherfuse.com/api-reference/webhooks/list-webhooks.md) — `POST /ramp/webhooks`
* [Get a subscription](https://docs.etherfuse.com/api-reference/webhooks/get-webhook.md) — `GET /ramp/webhook/{id}`
* [Delete a subscription](https://docs.etherfuse.com/api-reference/webhooks/delete-webhook.md) — `DELETE /ramp/webhook/{id}`

> [!NOTE]
>
> If a transaction expires before the customer signs it, fetch a fresh one with [POST /ramp/order/\{id}/regenerate\_tx](https://docs.etherfuse.com/api-reference/orders/regenerate-tx.md) or [POST /ramp/swap/\{id}/regenerate\_tx](https://docs.etherfuse.com/api-reference/swaps/regenerate-swap-tx.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.