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

# Webhooks

# Webhooks

Element Pay notifies your backend when **orders**, **customers**, and **accounts** change. **Treat webhooks as the source of truth** for settlement, KYC / banking readiness, and account credits / sends — HTTP responses only confirm the request was accepted. If a webhook is delayed, poll the matching GET endpoint as a backup.

## Configuration

Set **`webhook_url`** and **`webhook_secret`** on your partner API key (sandbox and production separately). The same endpoint receives order, customer, and account events.

## Order events

| Header `X-Webhook-Event` | When                                   |
| ------------------------ | -------------------------------------- |
| `order.processing`       | Quote accepted; rail execution started |
| `order.settled`          | Order completed successfully           |
| `order.failed`           | Terminal failure                       |
| `order.refunded`         | Refund issued (when applicable)        |

Backup poll: `GET /partner/orders/{order_id}`.

## Customer events

Same signing headers and secret as orders. Backup poll: `GET /partner/customers/{customer_id}`.

| Header `X-Webhook-Event`           | When                                                                                   |
| ---------------------------------- | -------------------------------------------------------------------------------------- |
| `customer.submitted`               | After a successful submit                                                              |
| `customer.approved`                | Element Pay approves the vault case                                                    |
| `customer.rejected`                | Review declines the case                                                               |
| `customer.updated`                 | Material status / product update                                                       |
| `customer.deposit_account.updated` | Deposit-account product status changes (e.g. becomes `ready` so you may open accounts) |

### Customer payload (example)

```json theme={null}
{
  "customer_id": "pcus_…",
  "partner_customer_ref": "partner-cust-001",
  "type": "individual",
  "status": "approved",
  "environment": "sandbox",
  "submitted_at": "2026-08-01T12:00:00+00:00",
  "reviewed_at": "2026-08-01T12:05:00+00:00",
  "products": {
    "deposit_account": {
      "status": "ready"
    }
  }
}
```

### Deposit-account payload (example)

Fired as `customer.deposit_account.updated`:

```json theme={null}
{
  "customer_id": "pcus_…",
  "partner_customer_ref": "partner-cust-001",
  "status": "approved",
  "products": {
    "deposit_account": {
      "status": "ready"
    }
  },
  "can_open_deposit_account": true
}
```

When `products.deposit_account.status` is `ready` (and `can_open_deposit_account` is true), you may call [Accounts](/customers/accounts) open/list/get. See also [Customers quickstart](/customers/quickstart).

## Account events

Same signing headers and secret. These fire for customer deposit-account rails (and related money movement). Backup poll: `GET /partner/customers/{customer_id}/accounts/{account_id}` (and send/payout GET where applicable).

| Header `X-Webhook-Event` | When                                                                 |
| ------------------------ | -------------------------------------------------------------------- |
| `account.opened`         | First successful open of a rail                                      |
| `account.ready`          | Coordinates become usable (bank fields or wallet)                    |
| `account.credited`       | Inbound credit posted (deposit, card charge, or other credit source) |
| `account.send.completed` | Outbound stablecoin send reached a success terminal state            |
| `account.send.failed`    | Outbound stablecoin send reached a failure terminal state            |

Use `account.credited` / account GET balance before spending. For sends, prefer `account.send.completed` / `account.send.failed`; if GET stays non-terminal after debit for a long time, escalate rather than assuming destination credit.

## HTTP delivery

```http theme={null}
POST <your-webhook_url>
Content-Type: application/json
User-Agent: ElementPay/1.0 (+support@elementpay.net)
X-Webhook-Event: order.settled
X-Webhook-Id: <uuid>
X-Webhook-Signature: t=<unix_ts>,v1=<base64_signature>
```

## Order payload (example)

Webhook bodies have no internal routing metadata or upstream PSP blobs. Typical order fields:

```json theme={null}
{
  "order_id": "YC-580e04c2-a136-5cca-be54-b49fcf80970c",
  "status": "settled",
  "amount_fiat": 800.0,
  "currency": "KES",
  "amount_crypto": 5.12,
  "exchange_rate": 156.25,
  "order_type": "OnRamp",
  "wallet_address": "0x4F07419E6bfCCF8D256E8ef803Cc2653dfbB9558",
  "phone_number": "2541111111111",
  "settlement_transaction_hash": "0xabc...",
  "created_at": "2026-07-07T10:00:00.000000Z",
  "updated_at": "2026-07-07T10:00:15.000000Z"
}
```

Fields vary by corridor and rail. `client_metadata` and other internal fields are not included on the public partner API.

## Signature verification

Parse `X-Webhook-Signature`:

```
t=1710000000,v1=<base64_hmac>
```

1. Reject if `t` is older than **5 minutes** (replay protection).
2. Compute HMAC-SHA256 over `{t}.{raw_body}` using your `webhook_secret`.
3. Compare constant-time to `v1`.

## Idempotency

Use **`X-Webhook-Id`** to deduplicate deliveries. Your endpoint should return **2xx** quickly; heavy work can be async.

## Local testing

Use a tunnel (ngrok, Cloudflare Tunnel) pointing at your dev server, or log payloads in sandbox before going live.
