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

# Wallet API webhooks

> Get notified when a wallet you created receives funds, and when a withdrawal or a transfer between your users completes or fails.

We watch every address you create and tell you when money arrives. Set a webhook URL once under [Dashboard → Webhooks](/dashboard/webhooks), and it's used for checkout, off-ramp, onramp, and the Wallet API.

## wallet.deposit.received

Sent once for every deposit to a wallet you created. It's signed like every other Minisend webhook, with the `X-Minisend-Signature` header and your `webhook_secret`. See [webhook verification](/webhooks/verification).

```json theme={null}
{
  "event": "wallet.deposit.received",
  "deposit_id": "7b2e1a3f-...",
  "wallet_id": "3f6b2e1a-...",
  "wallet_ref": "user-9214",
  "address": "0xabc1230000000000000000000000000000dead",
  "chain": "MATIC",
  "token": "USDC",
  "amount": "10.500000",
  "tx_hash": "0x55a572efe1720250e442f38741477a4fc3f7f152e5cd208cc52f8222a1c2a13b",
  "from_address": "0x9f2c...",
  "state": "confirmed",
  "detected_at": "2026-08-07T09:12:03.000Z",
  "created_at": "2026-08-07T09:12:03.000Z"
}
```

<ResponseField name="deposit_id" type="string" required>
  Use this to spot repeats. If we send an update for the same deposit later, it has the same `deposit_id`.
</ResponseField>

<ResponseField name="wallet_ref" type="string" required>
  Your own ID for the wallet, exactly as you sent it when you created the address.
</ResponseField>

<ResponseField name="chain" type="string" required>
  The network the money actually arrived on. An address is the same on every network, so a wallet you created on Base can also receive on Polygon, Arbitrum, or any other supported network. Always check this field rather than assuming it's the network you created the wallet on.
</ResponseField>

<ResponseField name="amount" type="string" required>
  Text, not a number. Store and compare it as an exact decimal, not a floating-point number, so you don't lose precision.
</ResponseField>

<ResponseField name="tx_hash" type="string">
  The blockchain transaction ID. It can be `null` for a moment in the very first message about a deposit.
</ResponseField>

<ResponseField name="state" type="string" required>
  `confirmed` or `complete`. You might get `confirmed` first and `complete` a moment later for the same `deposit_id`. Both mean the money has arrived.
</ResponseField>

<Note>
  A transfer between two of your own users doesn't send `wallet.deposit.received` for the receiving address. It's reported as [`wallet.transfer.completed`](#wallet-transfer-completed-and-wallet-transfer-failed) instead, so if you counted both, you'd count the money twice.
</Note>

<Note>
  You can also look up deposits through the API instead of waiting for a webhook. See [get wallet deposits](/api-reference/wallet-api/get-wallet-deposits) and [list all deposits](/api-reference/wallet-api/list-deposits). They're useful for catching up after your server was down, or if you'd rather check yourself.
</Note>

## wallet.withdrawal.completed and wallet.withdrawal.failed

Sent when a withdrawal you asked for with [POST /api/v1/withdrawals](/api-reference/wallet-api/withdrawals), or a recovery with [POST /api/v1/recover](/api-reference/wallet-api/recover), is finished.

```json theme={null}
{
  "event": "wallet.withdrawal.completed",
  "withdrawal_id": "9f2c4e1a-...",
  "source": "master",
  "chain": "BASE",
  "token": "USDC",
  "amount": "50.000000",
  "destination_address": "0x9f2c000000000000000000000000000000dead",
  "tx_hash": "0x55a572efe1720250e442f38741477a4fc3f7f152e5cd208cc52f8222a1c2a13b",
  "state": "completed"
}
```

<ResponseField name="source" type="string">
  `"master"` if the money came from your master wallet. Otherwise, the `wallet_ref` of the address it came from.
</ResponseField>

<ResponseField name="tx_hash" type="string">
  The blockchain transaction ID. `null` on `wallet.withdrawal.failed`, because nothing was sent.
</ResponseField>

<ResponseField name="state" type="string" required>
  `completed` or `failed`.
</ResponseField>

This message doesn't say why a withdrawal failed. You'll find that in your dashboard. You can also check [get a withdrawal](/api-reference/wallet-api/get-withdrawal) instead of, or as well as, waiting for this webhook.

## wallet.transfer.completed and wallet.transfer.failed

Sent when a [transfer](/api-reference/wallet-api/transfers) between two of your users is finished.

```json theme={null}
{
  "event": "wallet.transfer.completed",
  "transfer_id": "8f21c3d0-...",
  "from": "user-42",
  "to": "user-7",
  "chain": "BASE",
  "token": "USDC",
  "amount": "12.500000",
  "tx_hash": "0x9b3e51c7a2d84f06e1b9c3a7d5f2e8b4c6a0d1f3e5b7c9a2d4f6e8b0c1a3d5f7",
  "state": "completed"
}
```

<ResponseField name="transfer_id" type="string" required>
  Use it together with `event` to spot repeats.
</ResponseField>

<ResponseField name="from" type="string" required>
  The `walletRef` that paid.
</ResponseField>

<ResponseField name="to" type="string" required>
  The `walletRef` that received the money. Credit them when you get this event.
</ResponseField>

<ResponseField name="tx_hash" type="string">
  The blockchain transaction ID. `null` on `wallet.transfer.failed`, because nothing was sent.
</ResponseField>

`wallet.transfer.failed` means nothing moved, and the money is still at the `from` address. In the rare case we can't tell for sure what happened, we don't send `wallet.transfer.failed`, because the money may have moved. [Get a transfer](/api-reference/wallet-api/transfers#getting-one-transfer) shows it as `under_review` while we check.

## wallet.deposit.settled

Sent when a deposit has finished moving under your [settlement](/wallet-api/settlement) setting. It's now USDC, at your user's own address, on your settlement network.

```json theme={null}
{
  "event": "wallet.deposit.settled",
  "deposit_id": "7b2e1a3f-...",
  "wallet_id": "3f6b2e1a-...",
  "wallet_ref": "user-9214",
  "address": "0xabc1230000000000000000000000000000dead",
  "source_chain": "ETH",
  "source_token": "USDT",
  "source_amount": "100.000000",
  "settlement_chain": "MATIC",
  "token": "USDC",
  "amount_settled": "99.410000",
  "tx_hash": "0x55a572efe1720250e442f38741477a4fc3f7f152e5cd208cc52f8222a1c2a13b",
  "settled_at": "2026-09-28T09:14:03.000Z"
}
```

<ResponseField name="source_chain" type="string" required>
  The network the original deposit arrived on.
</ResponseField>

<ResponseField name="source_amount" type="string" required>
  How much the deposit was, in `source_token`, before converting.
</ResponseField>

<ResponseField name="amount_settled" type="string" required>
  What arrived on `settlement_chain`. Usually a little less than `source_amount`, because converting and moving money between networks both cost something. Your business covers that cost, not your user's recorded deposit.
</ResponseField>

## wallet.deposit.settlement\_failed

Sent when we couldn't move a deposit under your settlement setting.

```json theme={null}
{
  "event": "wallet.deposit.settlement_failed",
  "deposit_id": "7b2e1a3f-...",
  "wallet_id": "3f6b2e1a-...",
  "wallet_ref": "user-9214",
  "address": "0xabc1230000000000000000000000000000dead",
  "source_chain": "ETH",
  "source_token": "USDT",
  "source_amount": "100.000000",
  "settlement_chain": "MATIC",
  "funds_location": "source_chain",
  "funds_chain": "ETH",
  "funds_token": "USDT",
  "message": "Could not be settled. The funds are safe at the user's address on Ethereum, as USDT."
}
```

<ResponseField name="funds_location" type="string" required>
  Where the money is now. `source_chain`: safe at the user's address on the network where it arrived. `base`: safe at the user's address on Base, as USDC. `under_review`: we're still confirming where it is. That doesn't mean it's unsafe, only that we won't say until we're sure.
</ResponseField>

<ResponseField name="funds_chain" type="string">
  Set for `source_chain` and `base`. `null` for `under_review`.
</ResponseField>

<ResponseField name="message" type="string" required>
  A sentence you can show as it is, based on `funds_location`. It's the same wording as the deposit's `settlement.error` field.
</ResponseField>

<Note>
  There's no separate "delayed" event. A settlement that's taking longer than usual is still moving and can still finish by itself. It shows as `delayed` on the deposit's `settlement` field, and we never report it as failed while that's the case.
</Note>


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