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

# Onramp webhooks

> Payload schemas for onramp.completed, onramp.released, onramp.failed, and onramp.expired, delivered to your webhook URL and signed with the same X-Minisend-Signature header.

Onramp events are delivered to the same webhook URL you configure in [Settings](/dashboard/settings), signed with the same secret and `X-Minisend-Signature` header as checkout and off-ramp events. [Signature verification](/webhooks/verification) is identical, and failed deliveries retry up to 5 times with exponential backoff.

| Event              | Fires when                                                               |
| ------------------ | ------------------------------------------------------------------------ |
| `onramp.completed` | Payment collected from the customer's phone                              |
| `onramp.released`  | The USDC transfer is confirmed on-chain                                  |
| `onramp.failed`    | The customer cancelled, the prompt timed out, or funds were insufficient |
| `onramp.expired`   | The customer never responded to the prompt                               |

`onramp.completed` and `onramp.released` both fire for a successful order, moments apart. Use `onramp.completed` to know the money was collected, and `onramp.released` to get the on-chain transaction hash once it's available.

***

## onramp.completed

```json theme={null}
{
  "event": "onramp.completed",
  "order_id": "7c1e4f9a-...",
  "external_reference": "collect-2201",
  "status": "completed",
  "currency": "KES",
  "amount_usdc": 7.62,
  "amount_local": 1000,
  "fee": 10,
  "exchange_rate": 129.92,
  "customer_phone": "0712345678",
  "mobile_network": "Safaricom",
  "release_address": "0xyourwalletaddress0000000000000000000000",
  "receipt_number": "SHQ1234ABC",
  "completed_at": "2026-07-23T12:04:41Z",
  "created_at": "2026-07-23T12:00:03Z"
}
```

<ResponseField name="event" type="string" required>
  Always `onramp.completed`.
</ResponseField>

<ResponseField name="order_id" type="string" required>
  Use as an idempotency key. Minisend may redeliver if your server timed out.
</ResponseField>

<ResponseField name="external_reference" type="string">
  Your `reference`, if set at creation.
</ResponseField>

<ResponseField name="amount_usdc" type="number" required>
  USDC released, or being released, to `release_address`.
</ResponseField>

<ResponseField name="amount_local" type="number" required>
  The KES amount the customer was charged, including the fee.
</ResponseField>

<ResponseField name="fee" type="number" required>
  Minisend fee in KES, included in `amount_local`.
</ResponseField>

<ResponseField name="exchange_rate" type="number" required>
  KES per 1 USDC, as executed.
</ResponseField>

<ResponseField name="receipt_number" type="string">
  The M-Pesa confirmation code for the collection.
</ResponseField>

<ResponseField name="completed_at" type="string" required>
  ISO 8601.
</ResponseField>

<ResponseField name="created_at" type="string" required>
  ISO 8601.
</ResponseField>

***

## onramp.released

Same payload shape as `onramp.completed`, sent once the on-chain transfer is confirmed. Adds `release_tx_hash`.

```json theme={null}
{
  "event": "onramp.released",
  "order_id": "7c1e4f9a-...",
  "status": "completed",
  "amount_usdc": 7.62,
  "release_address": "0xyourwalletaddress0000000000000000000000",
  "release_tx_hash": "0x55a572efe1720250e442f38741477a4fc3f7f152e5cd208cc52f8222a1c2a13b",
  "completed_at": "2026-07-23T12:04:41Z",
  "created_at": "2026-07-23T12:00:03Z"
}
```

<ResponseField name="release_tx_hash" type="string" required>
  The Base transaction that delivered the USDC to `release_address`.
</ResponseField>

***

## onramp.failed

```json theme={null}
{
  "event": "onramp.failed",
  "order_id": "7c1e4f9a-...",
  "external_reference": "collect-2201",
  "status": "failed",
  "currency": "KES",
  "amount_usdc": 7.62,
  "amount_local": 1000,
  "fee": 10,
  "exchange_rate": 129.92,
  "customer_phone": "0712345678",
  "mobile_network": "Safaricom",
  "release_address": "0xyourwalletaddress0000000000000000000000",
  "failure_reason": "cancelled by user",
  "created_at": "2026-07-23T12:00:03Z"
}
```

<ResponseField name="failure_reason" type="string">
  Why the collection failed: cancelled, timed out, or insufficient funds.
</ResponseField>

<Note>
  Nothing was collected, so there is nothing to refund. Create a new order if the customer wants to retry.
</Note>

***

## onramp.expired

The customer never responded to the payment prompt.

```json theme={null}
{
  "event": "onramp.expired",
  "order_id": "7c1e4f9a-...",
  "external_reference": "collect-2201",
  "status": "expired",
  "currency": "KES",
  "amount_usdc": 7.62,
  "amount_local": 1000,
  "customer_phone": "0712345678",
  "created_at": "2026-07-23T12:00:03Z"
}
```

***

## Handling pattern

Same as checkout and off-ramp webhooks: respond `200` immediately, process async, and dedupe on `order_id`.

```javascript theme={null}
app.post('/webhooks/minisend', (req, res) => {
  res.status(200).send('OK');

  const payload = req.body;
  if (payload.event?.startsWith('onramp.')) {
    processOnrampEvent(payload); // dedupe on payload.order_id
  } else if (payload.event?.startsWith('offramp.')) {
    processOfframpEvent(payload);
  } else {
    processCheckoutEvent(payload);
  }
});
```
