> ## 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.

# Webhook events

> Full payload schemas for checkout.completed, checkout.failed, and checkout.expired events with field descriptions and best practices for idempotent handling.

| Event                | Fires when                       | Terminal status |
| -------------------- | -------------------------------- | --------------- |
| `checkout.completed` | Payout delivered to your account | `completed`     |
| `checkout.failed`    | Settlement failed post-deposit   | `failed`        |
| `checkout.expired`   | No deposit within 30 minutes     | `expired`       |

***

## checkout.completed

Deposit received, normalised to USDC on Base, converted, and paid out.

```json theme={null}
{
  "event": "checkout.completed",
  "session_id": "cs_7f8a9b2c-...",
  "external_id": "order-4821",
  "amount_usdc": 25.00,
  "amount_local": 3225.00,
  "currency": "KES",
  "exchange_rate": 129.00,
  "receipt": "SHQ1234ABC",
  "status": "completed",
  "completed_at": "2026-04-13T14:32:00Z",
  "created_at": "2026-04-13T14:00:00Z"
}
```

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

<ResponseField name="session_id" type="string" required>
  Use as an idempotency key.
</ResponseField>

<ResponseField name="external_id" type="string">
  Your reference, if set.
</ResponseField>

<ResponseField name="amount_usdc" type="number" required />

<ResponseField name="amount_local" type="number" required>
  Net local currency after the 1% fee.
</ResponseField>

<ResponseField name="currency" type="string" required>
  `KES`, `NGN`, `GHS`, or `UGX`.
</ResponseField>

<ResponseField name="exchange_rate" type="number" required>
  Local currency per 1 USDC at settlement.
</ResponseField>

<ResponseField name="receipt" type="string" required>
  Payout provider receipt.
</ResponseField>

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

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

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

***

## checkout.failed

Deposit received but settlement could not complete. Rare.

```json theme={null}
{
  "event": "checkout.failed",
  "session_id": "cs_7f8a9b2c-...",
  "external_id": "order-4821",
  "amount_usdc": 25.00,
  "currency": "KES",
  "status": "failed",
  "created_at": "2026-04-13T14:00:00Z"
}
```

<ResponseField name="event" type="string" required>
  Always `checkout.failed`.
</ResponseField>

<ResponseField name="session_id" type="string" required />

<ResponseField name="external_id" type="string" />

<ResponseField name="amount_usdc" type="number" required>
  The amount that was deposited but couldn't be settled.
</ResponseField>

<ResponseField name="currency" type="string" required />

<ResponseField name="status" type="string" required>
  Always `failed`.
</ResponseField>

<ResponseField name="created_at" type="string" required />

<Note>
  Contact [support](https://t.me/minisendapp) with the `session_id`. Customer funds are not lost.
</Note>

***

## checkout.expired

Session opened but no deposit arrived in the 30-minute window. Use this to mark abandoned orders.

```json theme={null}
{
  "event": "checkout.expired",
  "session_id": "cs_7f8a9b2c-...",
  "external_id": "order-4821",
  "amount_usdc": 25.00,
  "currency": "KES",
  "status": "expired",
  "created_at": "2026-04-13T14:00:00Z"
}
```

<ResponseField name="event" type="string" required>
  Always `checkout.expired`.
</ResponseField>

<ResponseField name="session_id" type="string" required />

<ResponseField name="external_id" type="string">
  Use this to mark the corresponding order as abandoned.
</ResponseField>

<ResponseField name="amount_usdc" type="number" required>
  The amount the customer never sent.
</ResponseField>

<ResponseField name="currency" type="string" required />

<ResponseField name="status" type="string" required>
  Always `expired`.
</ResponseField>

<ResponseField name="created_at" type="string" required />

<Note>
  Expired sessions cost nothing. The 1% fee is only charged on completed payments.
</Note>

***

## Best practices

### Respond fast, process async

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

### Idempotency

Use `session_id` as the key — Minisend may redeliver if your server timed out.

```javascript theme={null}
async function processWebhookAsync(payload) {
  const seen = await db.events.findOne({ session_id: payload.session_id });
  if (seen) return;

  await db.events.insert({ session_id: payload.session_id, processed_at: new Date() });
  // ...handle event
}
```

### Correlate via external\_id

```javascript theme={null}
if (payload.event === 'checkout.completed') {
  await orders.markPaid(payload.external_id, {
    receipt: payload.receipt,
    payout_amount: payload.amount_local,
    currency: payload.currency,
  });
} else if (payload.event === 'checkout.expired') {
  await orders.markAbandoned(payload.external_id);
}
```
