# Build with an AI coding agent Source: https://docs.minisend.xyz/ai-agent-skill Install a skill that teaches Claude Code, Cursor, Codex, and other AI coding agents to integrate every Minisend API offline. If you're integrating with an AI coding agent, install the Minisend skill instead of pointing your agent at this site. It teaches the agent every API in this documentation, offline, so it can write correct integration code without fetching pages at request time. If you want your agent to call the API rather than write code against it, add the [MCP server](/mcp-server) as well. The skill teaches; the MCP server acts. They work well together. ## Install ```bash Skills CLI theme={null} npx skills add itschrisoketch/minisend-skills ``` ```bash npm theme={null} npm install @minisend/skill ``` The Skills CLI works across roughly 17 agents, including Claude Code, Cursor, Codex, Cline, and Gemini CLI. Once installed, ask your agent to integrate Minisend and it will load the skill automatically. ## What it covers One router file plus dedicated references for each part of the API surface: off-ramp, onramp, the Wallet API, checkout, recipients, webhooks, and error handling, plus a FAQ. It's the same product surface as this site, kept as the single source of truth for agent use so an agent never works from stale or duplicated guidance. ## Where it lives The skill is maintained in [`itschrisoketch/minisend-skills`](https://github.com/itschrisoketch/minisend-skills), a public repository, and published to npm as [`@minisend/skill`](https://www.npmjs.com/package/@minisend/skill). Star or watch the repository to catch updates as the API evolves. Source, install instructions, and the full reference set. # Authentication Source: https://docs.minisend.xyz/api-reference/authentication Authenticate Minisend API requests with your API key, and learn which endpoints are public and require no authentication at all. Two auth models: * **API key**: for creating checkout sessions and all [off-ramp](/offramp/overview) and [onramp](/onramp/overview) endpoints * **None**: for session status, payment link info, and payment link session creation The [Wallet API](/wallet-api/overview) is a separate product with its own `wsk_live_` keys, generated from **Dashboard → Wallets** rather than **API Keys**. See its own docs for details; the rest of this page covers the `ms_live_` key used everywhere else. ## API key All keys use the `ms_live_` prefix. Never put your API key in frontend JS, mobile apps, or public repos. Backend only. ### Header ```text theme={null} Authorization: Bearer ms_live_your_key_here ``` ### Example ```bash theme={null} curl -X POST https://merchant.minisend.xyz/api/merchant/checkout \ -H "Authorization: Bearer ms_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{"amount": 25.00, "description": "Order #4821"}' ``` ### Generate a key 1. Open [merchant.minisend.xyz/dashboard](https://merchant.minisend.xyz/dashboard). 2. Go to **API Keys** → **New Key**. 3. Copy the full value immediately; it's shown once. Minisend stores only a hash. `401` = invalid or missing key. `403` = the key or account can't use this endpoint yet — see key scopes below. ### Key scopes Two independent things gate access to checkout, off-ramp, and onramp: your **key's scope**, and whether the **product is enabled on your account**. Both must be true, or every call to that product returns one friendly `403`: ```json theme={null} { "error": "Your account doesn't have off-ramp access yet. Please contact info@minisend.xyz to request access." } ``` | Scope | Grants | How to get it | | ---------- | ------------------------------ | -------------------------------------------------------------------------------------- | | `checkout` | Checkout session creation | On every key and account by default | | `offramp` | All `/api/offramp/*` endpoints | Email [info@minisend.xyz](mailto:info@minisend.xyz) to enable off-ramp on your account | | `onramp` | All `/api/onramp/*` endpoints | Email [info@minisend.xyz](mailto:info@minisend.xyz) to enable onramp on your account | The error message doesn't distinguish between a missing scope and a disabled account — either way, requesting access is the fix. ## Rate limits Every authenticated request is capped at **60 requests / minute / IP**. The response includes `X-RateLimit-Remaining`; exceeding it returns `429`. On top of that, endpoints that move money or fire a real payment prompt carry their own tighter, per-merchant limit (keyed to your account, not your IP, so it can't be dodged by rotating addresses): | Endpoint | Limit | | ----------------------- | ----------- | | Create checkout session | 30 / minute | | Create off-ramp order | 20 / minute | | Submit off-ramp deposit | 20 / minute | | Create onramp order | 10 / minute | Onramp additionally caps at **5 payment prompts per phone number per 10 minutes**, regardless of which key or account is calling — a `429` here means that specific phone was just sent one recently, not that your account is throttled. Contact support for higher limits. ## Public endpoints No `Authorization` header needed: | Endpoint | Use | | ----------------------------------------- | --------------------------------- | | `GET /api/merchant/checkout/{session_id}` | Session status | | `GET /api/merchant/pay/info?slug={slug}` | Merchant display info + live rate | | `POST /api/merchant/pay` | Create session from payment link | Safe to call from a browser or mobile app. # Create a checkout session Source: https://docs.minisend.xyz/api-reference/create-checkout Create a Minisend checkout session from your backend to get a hosted payment URL. Customers can pay in USDC or USDT across 19+14 supported chains. Creates a checkout session, assigns your deposit address, returns a hosted checkout URL. Expires in 30 minutes. Backend only. Never call from frontend. ## Endpoint ```text theme={null} POST https://merchant.minisend.xyz/api/merchant/checkout ``` ```text theme={null} Authorization: Bearer ms_live_your_key_here ``` See [Authentication](/api-reference/authentication). Capped at 30 session creations per minute per account; see [rate limits](/api-reference/authentication#rate-limits). ## Body USDC amount (>0, two decimals). Customer pays as USDC or USDT-equivalent. Shown to the customer on the checkout page. Your reference (e.g., order ID). Echoed back in webhooks. Stored on the session for receipts. `fiat` or `usdc`. Defaults to your account setting. `usdc` skips conversion and keeps the deposit in your wallet instead of paying it out. Only matters when `settlement_mode` is `usdc`. One of `BASE`, `ARB`, `AVAX`, `OP`, `ETH`, `MATIC`. Defaults to your account setting. An unrecognized value returns a 400. ## Examples ```bash cURL theme={null} curl -X POST https://merchant.minisend.xyz/api/merchant/checkout \ -H "Authorization: Bearer ms_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "amount": 25.00, "description": "Order #4821 - 2x T-shirts", "external_id": "order-4821", "customer_email": "customer@example.com" }' ``` ```javascript Node.js theme={null} const res = await fetch('https://merchant.minisend.xyz/api/merchant/checkout', { method: 'POST', headers: { 'Authorization': 'Bearer ms_live_your_key_here', 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: 25.00, description: 'Order #4821 - 2x T-shirts', external_id: 'order-4821', customer_email: 'customer@example.com', }), }); if (!res.ok) throw new Error((await res.json()).error); const { checkout_url, session_id } = await res.json(); res.redirect(checkout_url); ``` ```python Python theme={null} import requests res = requests.post( "https://merchant.minisend.xyz/api/merchant/checkout", headers={ "Authorization": "Bearer ms_live_your_key_here", "Content-Type": "application/json", }, json={ "amount": 25.00, "description": "Order #4821 - 2x T-shirts", "external_id": "order-4821", "customer_email": "customer@example.com", }, ) res.raise_for_status() data = res.json() ``` ## Response (201) ```json theme={null} { "session_id": "cs_7f8a9b2c-1234-5678-abcd-ef0123456789", "checkout_url": "https://merchant.minisend.xyz/checkout/cs_7f8a9b2c-1234-5678-abcd-ef0123456789", "deposit_address": "0x1234567890abcdef1234567890abcdef12345678", "amount_usdc": 25.00, "settlement_mode": "fiat", "expires_at": "2026-04-13T14:30:00.000Z", "status": "pending" } ``` Unique identifier. Always starts with `cs_`. Hosted checkout page. Redirect the customer here. Accepts USDC on 19 chains and USDT on 14 chains. See [supported networks](/payments/supported-currencies). USDC-equivalent amount. ISO 8601, 30 minutes after creation. After that, `status` becomes `expired`. Always `"pending"` on creation. Track via the [status endpoint](/api-reference/get-checkout) or webhooks. `fiat` or `usdc`, whichever this session ended up with. Present when `settlement_mode` is `usdc`. The chain this payment will land on. # Errors Source: https://docs.minisend.xyz/api-reference/errors Understand the Minisend API error response structure, every HTTP status code your integration may receive, and how to handle each one in your code. Every error response: ```json theme={null} { "error": "Description of what went wrong" } ``` One top-level `error` string. No nested objects. ## Status codes | Status | Meaning | | ------ | -------------------------------------------------------------------- | | `400` | Invalid request: missing required fields or invalid amount | | `401` | Missing or invalid API key | | `403` | Key valid, merchant account inactive | | `404` | Session ID or merchant slug doesn't exist | | `429` | Rate limit: 60 req/min/IP. Response includes `X-RateLimit-Remaining` | | `500` | Transient server error. Retry with backoff | ### 400: Bad request * `amount` must be a positive number, not a string, zero, or negative. * For `POST /api/merchant/pay`, `amount` must be `0.01` to `10000`. ### 401: Unauthorized * Header must be `Authorization: Bearer ms_live_...`. * Verify the key in **API Keys**. ### 403: Forbidden Key authenticated, but the merchant account is inactive. Contact support. ### 404: Not found `session_id` (must start with `cs_`) or `slug` doesn't match any record. ### 429: Rate limit Slow down. `X-RateLimit-Remaining` shows how close you are. Contact support for higher limits. ### 500: Server error Retry with exponential backoff. If persistent, contact support with the `session_id` or request timestamp. ## Handling errors ```javascript Node.js theme={null} const res = await fetch('https://merchant.minisend.xyz/api/merchant/checkout', { method: 'POST', headers: { 'Authorization': 'Bearer ms_live_your_key_here', 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: 25.00 }), }); if (!res.ok) { const { error } = await res.json(); if (res.status === 429) { await new Promise((r) => setTimeout(r, 2000)); } throw new Error(`${res.status}: ${error}`); } const session = await res.json(); ``` ```python Python theme={null} import requests import time def create_checkout(amount): res = requests.post( "https://merchant.minisend.xyz/api/merchant/checkout", headers={"Authorization": "Bearer ms_live_your_key_here"}, json={"amount": amount}, ) if not res.ok: if res.status_code == 429: time.sleep(2) raise RuntimeError(f"{res.status_code}: {res.json().get('error')}") return res.json() ``` # Get checkout session status Source: https://docs.minisend.xyz/api-reference/get-checkout Poll this public Minisend endpoint to track a checkout session from pending through settlement. Returns status, exchange rate, and receipt. Public — no API key needed. Safe to call from a frontend. On every call, this endpoint also expires stale `pending` sessions past their deadline and cross-checks `settling` sessions that have been in flight >2 minutes against the settlement provider. ## Endpoint ```text theme={null} GET https://merchant.minisend.xyz/api/merchant/checkout/{session_id} ``` No auth. ## Path Returned from `POST /api/merchant/checkout`. Starts with `cs_`. ## Examples ```bash cURL theme={null} curl https://merchant.minisend.xyz/api/merchant/checkout/cs_7f8a9b2c-1234-5678-abcd-ef0123456789 ``` ```javascript Node.js theme={null} const res = await fetch( `https://merchant.minisend.xyz/api/merchant/checkout/${sessionId}` ); const session = await res.json(); ``` ```python Python theme={null} import requests res = requests.get( f"https://merchant.minisend.xyz/api/merchant/checkout/{session_id}" ) session = res.json() ``` ## Response ```json theme={null} { "session_id": "cs_7f8a9b2c-1234-5678-abcd-ef0123456789", "status": "completed", "amount_usdc": 25.00, "description": "Order #4821", "deposit_address": "0x1234567890abcdef1234567890abcdef12345678", "expires_at": "2026-04-13T14:30:00.000Z", "created_at": "2026-04-13T14:00:00.000Z", "amount_local": 3225.00, "exchange_rate": 129.00, "settlement_receipt": "SHQ1234ABC", "completed_at": "2026-04-13T14:08:22.000Z", "merchant": { "business_name": "My Store", "logo_url": null } } ``` Current status. See [lifecycle](#status-lifecycle). USDC-equivalent. Customer may pay in USDC or USDT. Accepts USDC (19 chains) and USDT (14 chains). Net local currency after the platform fee. Present when `status` is `"completed"`. The USDC-to-local exchange rate at settlement. Present when `status` is `"completed"`. Payout provider receipt (e.g., M-Pesa code). Present when `status` is `"completed"`. ISO 8601. Present when `status` is `"completed"`. Present on USDC sessions. Which chain this payment settles to. Present on USDC sessions off Base. Tracks the bridge that moves USDC from Base to your chosen chain. `not_required` on Base, since nothing bridges. `not_started` before the payment lands, then `pending` while it's bridging, `completed` once it arrives, or `failed` if it needs a hand. A failed forward doesn't lose anything — the USDC just sits on Base instead. Same as `settlement_chain`. The destination-chain transaction. Only there once `status` is `completed`. Display info. ## Status lifecycle | Status | Meaning | | ------------------ | --------------------------------------- | | `pending` | Waiting for the customer to send | | `deposit_received` | Detected on-chain; settlement initiated | | `settling` | Conversion + payout in progress | | `completed` | Payout delivered | | `failed` | Failed post-deposit — contact support | | `expired` | No deposit within 30 minutes | ## Polling ```javascript theme={null} async function waitForPayment(sessionId, timeoutMs = 30 * 60 * 1000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const res = await fetch( `https://merchant.minisend.xyz/api/merchant/checkout/${sessionId}` ); const session = await res.json(); if (session.status === 'completed') return session; if (session.status === 'failed' || session.status === 'expired') { throw new Error(`Session ended: ${session.status}`); } await new Promise((r) => setTimeout(r, 3000)); } throw new Error('Timed out'); } ``` Prefer webhooks over polling. Configure `webhook_url` in **Settings** to receive `checkout.completed`, `checkout.failed`, and `checkout.expired` events. `completed` means the payment landed, not that bridging finished. On a USDC session off Base, check `forward.status` separately, or listen for [`checkout.forwarded`](/webhooks/events#checkout-forwarded). # Create an order Source: https://docs.minisend.xyz/api-reference/offramp/create-order Create a USDC payout order to any M-Pesa, mobile money, till, paybill, or bank recipient. Returns the deposit address and exact amount to send on Base. Validates the recipient's account, locks a quote, and returns where to send USDC. Nothing moves until you deposit. Backend only. Never call from frontend. ## Endpoint ```text theme={null} POST https://merchant.minisend.xyz/api/offramp/orders ``` ```text theme={null} Authorization: Bearer ms_live_your_key_here Idempotency-Key: payout-8412 ``` Requires the `offramp` scope. Always send an `Idempotency-Key` (any string unique per payout, e.g. your payout ID). Replaying the same key returns the original order with a `200` instead of creating a duplicate, so retries on timeouts are safe. ## Body USDC amount, 0.5 – 50,000. The local equivalent must fall inside the [per-transaction range](/offramp/overview#limits). Payout currency: `KES`, `NGN`, `GHS`, or `UGX`. A `0x` EVM address you control. Failed NGN payouts are refunded here automatically; it is also the return path of record for support cases. Who gets paid. Shape per method in [recipients](/offramp/recipients). Validated before the order is created. Your reference (e.g. internal payout ID). Echoed back as `external_reference` in responses and webhooks. ## Example ```bash cURL theme={null} curl -X POST https://merchant.minisend.xyz/api/offramp/orders \ -H "Authorization: Bearer ms_live_your_key_here" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: payout-8412" \ -d '{ "amount": 10, "currency": "KES", "refund_address": "0xYourWalletAddress0000000000000000000000", "reference": "payout-8412", "recipient": { "method": "MOBILE", "account_name": "Jane Wanjiku", "phone": "0712345678", "mobile_network": "Safaricom" } }' ``` ```javascript Node.js theme={null} const res = await fetch('https://merchant.minisend.xyz/api/offramp/orders', { method: 'POST', headers: { 'Authorization': 'Bearer ms_live_your_key_here', 'Content-Type': 'application/json', 'Idempotency-Key': 'payout-8412', }, body: JSON.stringify({ amount: 10, currency: 'KES', refund_address: '0xYourWalletAddress0000000000000000000000', reference: 'payout-8412', recipient: { method: 'MOBILE', account_name: 'Jane Wanjiku', phone: '0712345678', mobile_network: 'Safaricom', }, }), }); if (!res.ok) throw new Error((await res.json()).error); const order = await res.json(); // send order.total_deposit_usdc USDC on Base to order.deposit_address ``` ```python Python theme={null} import requests res = requests.post( "https://merchant.minisend.xyz/api/offramp/orders", headers={ "Authorization": "Bearer ms_live_your_key_here", "Content-Type": "application/json", "Idempotency-Key": "payout-8412", }, json={ "amount": 10, "currency": "KES", "refund_address": "0xYourWalletAddress0000000000000000000000", "reference": "payout-8412", "recipient": { "method": "MOBILE", "account_name": "Jane Wanjiku", "phone": "0712345678", "mobile_network": "Safaricom", }, }, ) res.raise_for_status() order = res.json() ``` ## Response (201) ```json theme={null} { "order_id": "9b2f6c1e-...", "status": "pending", "amount_usdc": 10, "total_deposit_usdc": 10, "currency": "KES", "rate": 129.45, "amount_local": 1294, "fee": 13, "recipient_amount": 1281, "deposit_address": "0x8005ee53e57ab11e11eaa4efe07ee3835dc02f98", "deposit_chain": "base", "recipient": { "account_name": "Jane Wanjiku", "method": "MOBILE", "phone": "0712345678" }, "refund_address": "0xYourWalletAddress0000000000000000000000", "external_reference": "payout-8412", "expires_at": "2026-07-05T12:30:00.000Z", "created_at": "2026-07-05T12:00:03.000Z", "instructions": "Send exactly 10 USDC (Base) to deposit_address from your own wallet, then submit the transaction hash via POST /api/offramp/orders/9b2f6c1e-.../deposit before expires_at." } ``` Unique order identifier. **The exact USDC amount to send.** Equals `amount_usdc` for KES, GHS, and UGX. For NGN it is `amount_usdc + sender_fee_usdc + transaction_fee_usdc`. NGN orders only. A network fee included in `total_deposit_usdc`. NGN orders only. A network fee included in `total_deposit_usdc`. Where to send USDC on Base. For KES, GHS, and UGX this is a shared settlement address; always follow your transfer with a [hash submission](/api-reference/offramp/submit-deposit). For NGN it is a **single-use** address monitored automatically. Always `base`. USDC sent on other chains is not detected. Quoted rate. For KES, GHS, and UGX the payout executes at the live rate at deposit time, and the order's `rate`, `amount_local`, and `fee` update to the executed values. For NGN the rate is locked. Deposit deadline, ISO 8601. 30 minutes for KES, GHS, UGX; about 5 minutes for NGN. After it passes the order becomes `expired`. Human-readable next step for this specific order. ## Errors | Status | Meaning | | ------ | ----------------------------------------------------------------------------------------------------------------- | | `200` | Replay of an existing `Idempotency-Key`. Returns the original order, not a new one | | `400` | Invalid amount, currency, `refund_address`, or recipient shape; or amount outside the local per-transaction range | | `403` | Key lacks the `offramp` scope or off-ramp is not enabled on your account | | `422` | Recipient failed account validation. Nothing created | | `429` | More than 20 orders created for your account in the last minute | | `502` | Pricing or deposit-address provisioning failed. Retry with a **new**`Idempotency-Key` | # Get an order Source: https://docs.minisend.xyz/api-reference/offramp/get-order Fetch the current state of an off-ramp order: status, deposit details, executed rate, and the payout receipt once completed. Returns the current state of an order you own. Poll it after depositing, or rely on [webhooks](/offramp/webhooks) and use this as the source of truth. ## Endpoint ```text theme={null} GET https://merchant.minisend.xyz/api/offramp/orders/{order_id} ``` ```text theme={null} Authorization: Bearer ms_live_your_key_here ``` Requires the `offramp` scope. Orders you don't own return `404`. ## Example ```bash theme={null} curl https://merchant.minisend.xyz/api/offramp/orders/9b2f6c1e-... \ -H "Authorization: Bearer ms_live_your_key_here" ``` ## Response (200) ```json theme={null} { "order_id": "9b2f6c1e-...", "status": "completed", "amount_usdc": 10, "total_deposit_usdc": 10, "currency": "KES", "rate": 129.52, "amount_local": 1295, "fee": 13, "deposit_address": "0x8005ee53e57ab11e11eaa4efe07ee3835dc02f98", "deposit_chain": "base", "deposit_tx_hash": "0x55a572efe1720250e442f38741477a4fc3f7f152e5cd208cc52f8222a1c2a13b", "recipient": { "account_name": "Jane Wanjiku", "method": "MOBILE", "phone": "0712345678" }, "refund_address": "0xYourWalletAddress0000000000000000000000", "external_reference": "payout-8412", "settlement_receipt": "SHQ1234ABC", "expires_at": "2026-07-05T12:30:00.000Z", "completed_at": "2026-07-05T12:07:41.000Z", "created_at": "2026-07-05T12:00:03.000Z" } ``` `pending`, `settling`, `completed`, `failed`, or `expired`. See the [lifecycle](/offramp/overview#order-lifecycle). A `pending` order past `expires_at` flips to `expired` when fetched. The exact USDC to deposit: `amount_usdc` plus network fees on NGN orders. For `settling` and later on KES, GHS, and UGX orders this is the **executed** rate; before that, the quote. Locked at creation for NGN. Your deposit transaction, once submitted (KES, GHS, UGX) or detected (NGN). Set on `completed`: the payout receipt (an M-Pesa code for KES mobile, an on-chain settlement hash for NGN). ISO 8601, set on `completed`. ## Errors | Status | Meaning | | ------ | ------------------------------------------------------------------------ | | `403` | Key lacks the `offramp` scope or off-ramp is not enabled on your account | | `404` | Unknown order, or it belongs to another account | # List orders Source: https://docs.minisend.xyz/api-reference/offramp/list-orders Paginated list of your off-ramp orders, newest first, filterable by status. Lists your orders, newest first. ## Endpoint ```text theme={null} GET https://merchant.minisend.xyz/api/offramp/orders ``` ```text theme={null} Authorization: Bearer ms_live_your_key_here ``` Requires the `offramp` scope. ## Query parameters Filter by status: `pending`, `settling`, `completed`, `failed`, or `expired`. Page size, max 100. Rows to skip. ## Example ```bash theme={null} curl "https://merchant.minisend.xyz/api/offramp/orders?status=completed&limit=50" \ -H "Authorization: Bearer ms_live_your_key_here" ``` ## Response (200) ```json theme={null} { "orders": [ { "order_id": "9b2f6c1e-...", "status": "completed", "amount_usdc": 10, "currency": "KES", "settlement_receipt": "SHQ1234ABC", "created_at": "2026-07-05T12:00:03.000Z" } ], "total": 132, "limit": 50, "offset": 0 } ``` Full [order objects](/api-reference/offramp/get-order). Total rows matching the filter. Use with `limit`/`offset` to paginate. # Get a quote Source: https://docs.minisend.xyz/api-reference/offramp/quote Price a USDC-to-local-currency payout before creating an order. Returns the rate, local amount, fee, and what the recipient nets, with optional recipient validation. Prices a payout without creating anything. Optionally validates the recipient and returns the registered account name in the same call. ## Endpoint ```text theme={null} POST https://merchant.minisend.xyz/api/offramp/quote ``` ```text theme={null} Authorization: Bearer ms_live_your_key_here ``` Requires the `offramp` scope. See [getting access](/offramp/overview#getting-access). ## Body USDC amount, 0.5 – 50,000. The local equivalent must also fall inside the [per-transaction range](/offramp/overview#limits) for the currency. Payout currency: `KES`, `NGN`, `GHS`, or `UGX`. Optional. When present, the account is validated and the registered name is returned as `recipient_name`. Shape per method in [recipients](/offramp/recipients). An invalid account returns `422`. ## Example ```bash cURL theme={null} curl -X POST https://merchant.minisend.xyz/api/offramp/quote \ -H "Authorization: Bearer ms_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{"amount": 10, "currency": "KES"}' ``` ```javascript Node.js theme={null} const res = await fetch('https://merchant.minisend.xyz/api/offramp/quote', { method: 'POST', headers: { 'Authorization': 'Bearer ms_live_your_key_here', 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: 10, currency: 'KES' }), }); if (!res.ok) throw new Error((await res.json()).error); const quote = await res.json(); ``` ```python Python theme={null} import requests res = requests.post( "https://merchant.minisend.xyz/api/offramp/quote", headers={ "Authorization": "Bearer ms_live_your_key_here", "Content-Type": "application/json", }, json={"amount": 10, "currency": "KES"}, ) res.raise_for_status() quote = res.json() ``` ## Response (200) ```json theme={null} { "amount_usdc": 10, "currency": "KES", "rate": 129.45, "amount_local": 1294, "fee": 13, "recipient_amount": 1281, "expires_at": "2026-07-05T12:05:00.000Z" } ``` Local currency per 1 USDC. Gross local amount (`amount × rate`, floored to whole units). Minisend fee in local units. Charged for KES, GHS, and UGX; `0` for NGN, where the margin is built into the rate instead. What the recipient nets: `amount_local − fee`. The registered account name, when a `recipient` was supplied and a name was resolved. Indicative 5-minute quote validity, ISO 8601. For KES, GHS, and UGX the payout executes at the live rate at deposit time regardless; for NGN the rate is locked when you create the order. ## Errors | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------------------------ | | `400` | Invalid amount, unsupported currency, malformed recipient, or local amount outside the per-transaction range | | `403` | Key lacks the `offramp` scope or off-ramp is not enabled on your account | | `422` | Recipient supplied but failed validation | | `502` | Rate temporarily unavailable. Retry | See [error handling](/api-reference/errors) for the response shape. # Submit a deposit Source: https://docs.minisend.xyz/api-reference/offramp/submit-deposit Report your USDC transfer for a KES, GHS, or UGX order by submitting the Base transaction hash. Triggers the fiat payout. Not used for NGN. After sending the order's USDC to `deposit_address` on Base, submit the transaction hash. Minisend verifies the transfer covers the order amount and starts the payout. **KES, GHS, and UGX orders only.** NGN deposits are detected automatically at the single-use address; calling this on an NGN order returns `409`. ## Endpoint ```text theme={null} POST https://merchant.minisend.xyz/api/offramp/orders/{order_id}/deposit ``` ```text theme={null} Authorization: Bearer ms_live_your_key_here ``` Requires the `offramp` scope. Orders you don't own return `404`. ## Body The Base transaction in which you sent the order's USDC amount to `deposit_address`. `0x`-prefixed 32-byte hex. A hash can pay exactly one order; reusing one returns `409`. ## Example ```bash cURL theme={null} curl -X POST https://merchant.minisend.xyz/api/offramp/orders/9b2f6c1e-.../deposit \ -H "Authorization: Bearer ms_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{"transaction_hash": "0x55a572efe1720250e442f38741477a4fc3f7f152e5cd208cc52f8222a1c2a13b"}' ``` ```javascript Node.js theme={null} const res = await fetch( `https://merchant.minisend.xyz/api/offramp/orders/${orderId}/deposit`, { method: 'POST', headers: { 'Authorization': 'Bearer ms_live_your_key_here', 'Content-Type': 'application/json', }, body: JSON.stringify({ transaction_hash: txHash }), } ); if (res.status === 422) { // transfer could not be verified. Check amount/chain/hash and retry } const order = await res.json(); // status: "settling" on success ``` ## Response (200) The full [order object](/api-reference/offramp/get-order) with `status: "settling"`: ```json theme={null} { "order_id": "9b2f6c1e-...", "status": "settling", "amount_usdc": 10, "deposit_tx_hash": "0x55a572efe1720250e442f38741477a4fc3f7f152e5cd208cc52f8222a1c2a13b", "rate": 129.52, "amount_local": 1295, "fee": 13, "...": "..." } ``` The order's `rate`, `amount_local`, and `fee` now reflect the **executed** payout at the live rate. From here, track `completed` via the [status endpoint](/api-reference/offramp/get-order) or the [`offramp.completed` webhook](/offramp/webhooks). Submitting the same hash again after acceptance is a safe no-op; you get the current order state back. ## Errors | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `400` | `transaction_hash` is not a valid `0x` 32-byte hex hash | | `404` | Unknown order, or not yours | | `409` | NGN order (no hash needed), order already terminal, order expired, or the hash was already used for another order | | `422` | The transfer could not be verified: it didn't cover the amount, used the wrong chain, or went to the wrong address. The order stays `pending`; fix and retry | | `429` | More than 20 deposit submissions for your account in the last minute | A `422` means the payout did **not** start. Verify the transaction sent the exact `total_deposit_usdc` in USDC on Base to the order's `deposit_address`, then resubmit. If the order expires while you debug, contact [support](https://t.me/minisendapp) with the `order_id` and hash. # Validate a recipient Source: https://docs.minisend.xyz/api-reference/offramp/validate-account Check a bank account, mobile number, till, or paybill and resolve the registered account name before creating an order. Checks that a recipient's account exists and resolves its registered name, so you can show a confirmation screen before creating an order. Bank accounts are hard-validated; mobile, till, and paybill lookups are best-effort and may return no name even for a valid number. ## Endpoint ```text theme={null} POST https://merchant.minisend.xyz/api/offramp/validate-account ``` ```text theme={null} Authorization: Bearer ms_live_your_key_here ``` Requires the `offramp` scope. ## Body `KES`, `NGN`, `GHS`, or `UGX`. Determines which recipient fields are required. Recipient shape per method. See [recipients](/offramp/recipients). ## Example ```bash cURL theme={null} curl -X POST https://merchant.minisend.xyz/api/offramp/validate-account \ -H "Authorization: Bearer ms_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "currency": "NGN", "recipient": { "account_name": "Chidi Okafor", "institution": "GTBINGLA", "account_number": "0123456789" } }' ``` ```javascript Node.js theme={null} const res = await fetch('https://merchant.minisend.xyz/api/offramp/validate-account', { method: 'POST', headers: { 'Authorization': 'Bearer ms_live_your_key_here', 'Content-Type': 'application/json', }, body: JSON.stringify({ currency: 'NGN', recipient: { account_name: 'Chidi Okafor', institution: 'GTBINGLA', account_number: '0123456789', }, }), }); const { valid, recipient_name } = await res.json(); ``` ## Response (200) ```json theme={null} { "valid": true, "recipient_name": "CHIDI OKAFOR" } ``` The account exists and can receive payouts. The registered name, or `null` when the account is valid but the name could not be resolved (common for mobile numbers). Show it to your user before they confirm the payout. ## Invalid account (422) ```json theme={null} { "valid": false, "error": "Recipient validation failed. Confirm the account details and try again." } ``` ## Errors | Status | Meaning | | ------ | ------------------------------------------------------------------------- | | `400` | Unsupported currency or malformed recipient | | `403` | Key lacks the `offramp` scope or off-ramp is not enabled on your account | | `422` | Account failed validation. Returns `valid: false` with an `error` message | # Create an order Source: https://docs.minisend.xyz/api-reference/onramp/create-order Create an M-Pesa collection order and send a payment prompt to the customer's phone in one call. USDC is released to the address you specify. Quotes server-side, creates the order, and sends the payment prompt to the customer's phone, all in one call. Backend only. Never call from frontend. ## Endpoint ```text theme={null} POST https://merchant.minisend.xyz/api/onramp/orders ``` ```text theme={null} Authorization: Bearer ms_live_your_key_here Idempotency-Key: collect-2201 ``` Requires the `onramp` scope. Always send an `Idempotency-Key` (any string unique per collection, e.g. your order ID). Replaying the same key returns the original order with a `200` instead of sending a second payment prompt. ## Body Only `KES` is supported. The USDC amount you want to receive. Provide this or `amount_kes`, not both. The exact KES amount to charge the customer. Provide this or `amount_usdc`, not both. The customer's Kenyan mobile number, any common format (`0712345678`, `+254712345678`, `254712345678`). `Safaricom` or `Airtel`. Optional override; the network is detected automatically from the phone number. A `0x` EVM address you control. USDC is released here on Base once payment is collected. Your reference (e.g. internal order ID). Echoed back as `external_reference` in responses and webhooks. ## Example ```bash cURL theme={null} curl -X POST https://merchant.minisend.xyz/api/onramp/orders \ -H "Authorization: Bearer ms_live_your_key_here" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: collect-2201" \ -d '{ "currency": "KES", "amount_kes": 1000, "phone": "0712345678", "address": "0xYourWalletAddress0000000000000000000000", "reference": "collect-2201" }' ``` ```javascript Node.js theme={null} const res = await fetch('https://merchant.minisend.xyz/api/onramp/orders', { method: 'POST', headers: { 'Authorization': 'Bearer ms_live_your_key_here', 'Content-Type': 'application/json', 'Idempotency-Key': 'collect-2201', }, body: JSON.stringify({ currency: 'KES', amount_kes: 1000, phone: '0712345678', address: '0xYourWalletAddress0000000000000000000000', reference: 'collect-2201', }), }); if (!res.ok) throw new Error((await res.json()).error); const order = await res.json(); ``` ```python Python theme={null} import requests res = requests.post( "https://merchant.minisend.xyz/api/onramp/orders", headers={ "Authorization": "Bearer ms_live_your_key_here", "Content-Type": "application/json", "Idempotency-Key": "collect-2201", }, json={ "currency": "KES", "amount_kes": 1000, "phone": "0712345678", "address": "0xYourWalletAddress0000000000000000000000", "reference": "collect-2201", }, ) res.raise_for_status() order = res.json() ``` ## Response (201) ```json theme={null} { "order_id": "7c1e4f9a-...", "status": "pending", "currency": "KES", "amount_usdc": 7.62, "amount_local": 1000, "fee": 10, "rate": 129.92, "customer_phone": "0712345678", "mobile_network": "Safaricom", "release_address": "0xyourwalletaddress0000000000000000000000", "release_chain": "base", "release_asset": "USDC", "external_reference": "collect-2201", "expires_at": "2026-07-23T12:30:00.000Z", "created_at": "2026-07-23T12:00:03.000Z", "instructions": "The customer's phone (0712345678) will receive an M-Pesa prompt for KSh 1,000. On payment, 7.62 USDC (Base) is released to release_address." } ``` Unique order identifier. USDC that will be released to `release_address` on payment. The KES amount the customer's phone is prompted to pay, including the fee. `Safaricom` or `Airtel`, detected from the phone number unless overridden. Where USDC lands on completion. Always the address you provided. Always `base`. Order window, ISO 8601, about 30 minutes. After it passes with no payment the order becomes `expired`. Human-readable summary of what happens next for this order. ## Errors | Status | Meaning | | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `200` | Replay of an existing `Idempotency-Key`. Returns the original order; no second prompt is sent | | `400` | Invalid phone, address, or amount fields; the charged KES amount is outside 20 to 250,000; or the net amount after the fee is below the 100 KES floor | | `403` | Key lacks the `onramp` scope or onramp is not enabled on your account | | `429` | More than 10 orders created for your account in the last minute, or more than 5 payment prompts sent to this specific phone number in the last 10 minutes | | `502` | The payment prompt could not be sent. The order is marked `failed`; create a new order to retry | # Get an order Source: https://docs.minisend.xyz/api-reference/onramp/get-order Fetch the current state of an onramp order: status, the release address, and the receipt once payment is collected. Returns the current state of an order you own. Poll it after creating an order, or rely on [webhooks](/onramp/webhooks) and use this as the source of truth. ## Endpoint ```text theme={null} GET https://merchant.minisend.xyz/api/onramp/orders/{order_id} ``` ```text theme={null} Authorization: Bearer ms_live_your_key_here ``` Requires the `onramp` scope. Orders you don't own return `404`. ## Example ```bash theme={null} curl https://merchant.minisend.xyz/api/onramp/orders/7c1e4f9a-... \ -H "Authorization: Bearer ms_live_your_key_here" ``` ## Response (200) ```json theme={null} { "order_id": "7c1e4f9a-...", "status": "completed", "currency": "KES", "amount_usdc": 7.62, "amount_local": 1000, "fee": 10, "rate": 129.92, "customer_phone": "0712345678", "mobile_network": "Safaricom", "release_address": "0xyourwalletaddress0000000000000000000000", "release_chain": "base", "release_asset": "USDC", "receipt_number": "SHQ1234ABC", "release_tx_hash": "0x55a572efe1720250e442f38741477a4fc3f7f152e5cd208cc52f8222a1c2a13b", "external_reference": "collect-2201", "expires_at": "2026-07-23T12:30:00.000Z", "completed_at": "2026-07-23T12:04:41.000Z", "created_at": "2026-07-23T12:00:03.000Z" } ``` `pending`, `completed`, `failed`, or `expired`. See the [lifecycle](/onramp/overview#order-lifecycle). A `pending` order past `expires_at` flips to `expired` when fetched. The M-Pesa confirmation code, set on `completed`. The Base transaction that delivered the USDC, set shortly after `completed`. Set on `failed`: cancelled, timed out, or insufficient funds. ISO 8601, set on `completed`. ## Errors | Status | Meaning | | ------ | --------------------------------------------------------------------- | | `403` | Key lacks the `onramp` scope or onramp is not enabled on your account | | `404` | Unknown order, or it belongs to another account | # List orders Source: https://docs.minisend.xyz/api-reference/onramp/list-orders Paginated list of your onramp orders, newest first, filterable by status. Lists your orders, newest first. ## Endpoint ```text theme={null} GET https://merchant.minisend.xyz/api/onramp/orders ``` ```text theme={null} Authorization: Bearer ms_live_your_key_here ``` Requires the `onramp` scope. ## Query parameters Filter by status: `pending`, `completed`, `failed`, or `expired`. Page size, max 100. Rows to skip. ## Example ```bash theme={null} curl "https://merchant.minisend.xyz/api/onramp/orders?status=completed&limit=50" \ -H "Authorization: Bearer ms_live_your_key_here" ``` ## Response (200) ```json theme={null} { "orders": [ { "order_id": "7c1e4f9a-...", "status": "completed", "currency": "KES", "amount_usdc": 7.62, "receipt_number": "SHQ1234ABC", "created_at": "2026-07-23T12:00:03.000Z" } ], "total": 84, "limit": 50, "offset": 0 } ``` Full [order objects](/api-reference/onramp/get-order). Total rows matching the filter. Use with `limit`/`offset` to paginate. # Get a quote Source: https://docs.minisend.xyz/api-reference/onramp/quote Price an M-Pesa collection before creating an order. Specify the KES amount or the USDC amount, and get back the rate and fee in both directions. Prices a collection without creating anything or sending a payment prompt. ## Endpoint ```text theme={null} POST https://merchant.minisend.xyz/api/onramp/quote ``` ```text theme={null} Authorization: Bearer ms_live_your_key_here ``` Requires the `onramp` scope. See [getting access](/onramp/overview#getting-access). ## Body Only `KES` is supported. The USDC amount you want to receive. The customer is charged this amount converted to KES, plus the platform fee. Provide this or `amount_kes`, not both. The exact KES amount to charge the customer. The fee is taken from it and the remainder converts to USDC. Provide this or `amount_usdc`, not both. ## Example ```bash cURL theme={null} curl -X POST https://merchant.minisend.xyz/api/onramp/quote \ -H "Authorization: Bearer ms_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{"currency": "KES", "amount_kes": 1000}' ``` ```javascript Node.js theme={null} const res = await fetch('https://merchant.minisend.xyz/api/onramp/quote', { method: 'POST', headers: { 'Authorization': 'Bearer ms_live_your_key_here', 'Content-Type': 'application/json', }, body: JSON.stringify({ currency: 'KES', amount_kes: 1000 }), }); if (!res.ok) throw new Error((await res.json()).error); const quote = await res.json(); ``` ```python Python theme={null} import requests res = requests.post( "https://merchant.minisend.xyz/api/onramp/quote", headers={ "Authorization": "Bearer ms_live_your_key_here", "Content-Type": "application/json", }, json={"currency": "KES", "amount_kes": 1000}, ) res.raise_for_status() quote = res.json() ``` ## Response (200) ```json theme={null} { "currency": "KES", "amount_kes": 1000, "fee_kes": 10, "net_kes": 990, "amount_usdc": 7.62, "rate": 129.92, "expires_at": "2026-07-23T12:05:00.000Z" } ``` The exact figure the customer's phone will be prompted to pay. Minisend fee in KES, included in `amount_kes`. `amount_kes` minus `fee_kes`. What converts to USDC. What your address will receive. KES per 1 USDC. Indicative 5-minute quote validity, ISO 8601. The order executes at the live rate at creation time regardless. ## Errors | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | Neither or both of `amount_usdc`/`amount_kes` provided; the charged KES amount is outside 20 to 250,000; or the net amount after the fee is below the 100 KES floor | | `403` | Key lacks the `onramp` scope or onramp is not enabled on your account | | `502` | Rate temporarily unavailable. Retry | See [error handling](/api-reference/errors) for the response shape. # Payment link endpoints Source: https://docs.minisend.xyz/api-reference/payment-link Share a no-code payment link for USDC and USDT payments, or fetch merchant info plus a live exchange rate with the public slug lookup endpoint. Share a URL. No backend code required. ## Payment link URL ```text theme={null} https://merchant.minisend.xyz/pay/{slug} ``` Set your slug in **Settings**. Example: `mystore` → `https://merchant.minisend.xyz/pay/mystore`. The page shows your business name, logo, payout currency, and a live exchange rate preview. The customer enters a USDC amount ($0.01 to $10,000) and is taken to the hosted checkout. ## Merchant info endpoint For custom payment pages or live rate previews. Public — no auth. ```text theme={null} GET https://merchant.minisend.xyz/api/merchant/pay/info?slug={slug} ``` ### Query Your payment link slug. ### Examples ```bash cURL theme={null} curl "https://merchant.minisend.xyz/api/merchant/pay/info?slug=mystore" ``` ```javascript Node.js theme={null} const res = await fetch( 'https://merchant.minisend.xyz/api/merchant/pay/info?slug=mystore' ); const merchant = await res.json(); ``` ```python Python theme={null} import requests res = requests.get( "https://merchant.minisend.xyz/api/merchant/pay/info", params={"slug": "mystore"}, ) merchant = res.json() ``` ### Response ```json theme={null} { "business_name": "My Store", "logo_url": "https://example.com/logo.png", "tagline": "Coffee, brewed in Nairobi", "slug": "mystore", "payout_currency": "KES", "payout_method": "MOBILE", "indicative_rate": 129.0 } ``` Shown beneath the business name. `KES`, `NGN`, `GHS`, or `UGX`. `MOBILE`, `BUY_GOODS`, `PAYBILL`, or `BANK_TRANSFER`. Local currency per 1 USDC. Best-effort — `null` if the upstream rate provider is unreachable. Returns `404` if the slug doesn't exist or the merchant is inactive. ## Payment link session creation The hosted `/pay/{slug}` page calls this endpoint when the customer submits the form. Documented for transparency; you don't call it directly. ```text theme={null} POST https://merchant.minisend.xyz/api/merchant/pay ``` ### Body | Field | Type | Required | Description | | ------------- | ------ | -------- | ----------------------- | | `slug` | string | Yes | Merchant's slug | | `amount` | number | Yes | USDC, `0.01` to `10000` | | `description` | string | No | Optional note | ### Response (201) Same shape as [create checkout](/api-reference/create-checkout): ```json theme={null} { "session_id": "cs_7f8a9b2c-1234-5678-abcd-ef0123456789", "checkout_url": "https://merchant.minisend.xyz/checkout/cs_7f8a9b2c-1234-5678-abcd-ef0123456789", "deposit_address": "0x1234567890abcdef1234567890abcdef12345678", "amount_usdc": 25.00, "expires_at": "2026-04-13T14:30:00.000Z" } ``` 30-minute expiry, same webhook events as API-created sessions. # Create a wallet Source: https://docs.minisend.xyz/api-reference/wallet-api/create-wallet Issue an on-chain address for one of your users, or return their existing one. Idempotent on your own reference string. Creates an address for your `walletRef`, or returns the existing one if you've already created it. Never mints a second address for the same reference and chain. Backend only. Never call from frontend. ## Endpoint ```text theme={null} POST https://merchant.minisend.xyz/api/v1/wallets ``` ```text theme={null} Authorization: Bearer wsk_live_your_key_here ``` ## Body Your own identifier for this user or wallet: 1 to 128 characters, letters, numbers, and `_ : . -`. Your own user ID works well. Unique per your account and chain. `BASE`, `ARB`, `AVAX`, `ETH`, `OP`, or `MATIC`. Must already be activated on your account — see [activating a chain](/wallet-api/overview#master-wallets-and-addresses). Any JSON object. Stored alongside the wallet and returned when you fetch it later. ## Example ```bash cURL theme={null} curl -X POST https://merchant.minisend.xyz/api/v1/wallets \ -H "Authorization: Bearer wsk_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{"walletRef": "user-9214"}' ``` ```javascript Node.js theme={null} const res = await fetch('https://merchant.minisend.xyz/api/v1/wallets', { method: 'POST', headers: { 'Authorization': 'Bearer wsk_live_your_key_here', 'Content-Type': 'application/json', }, body: JSON.stringify({ walletRef: 'user-9214' }), }); if (!res.ok) throw new Error((await res.json()).error); const { wallet } = await res.json(); ``` ```python Python theme={null} import requests res = requests.post( "https://merchant.minisend.xyz/api/v1/wallets", headers={ "Authorization": "Bearer wsk_live_your_key_here", "Content-Type": "application/json", }, json={"walletRef": "user-9214"}, ) res.raise_for_status() wallet = res.json()["wallet"] ``` ## Response (201) ```json theme={null} { "wallet": { "id": "3f6b2e1a-...", "tenant_id": "a7c9e2d0-...", "master_wallet_id": "d4f81b3c-...", "wallet_ref": "user-9214", "address": "0xabc1230000000000000000000000000000dead", "chain": "BASE", "status": "active", "mode": "live", "metadata": null, "created_at": "2026-07-29T09:00:00.000Z" } } ``` Minisend's identifier for this wallet. Use it with [get wallet](/api-reference/wallet-api/get-wallet). Your account identifier. The same on every wallet you create. The master wallet this address was issued under, for the chain you requested. Echoes the reference you provided. The on-chain address, lowercased. The chain this address lives on. `active` or `frozen`. Always `live`. The Wallet API has no sandbox today. Whatever you passed at creation, or `null`. ## Errors | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------------------------------------ | | `400` | Invalid `walletRef`, unsupported `chain`, `metadata` isn't a JSON object, or `chain` isn't activated on your account yet | | `401` | Missing or invalid API key | | `403` | Account inactive | | `429` | Rate limit exceeded (60 requests/minute) | # Get a wallet's balance Source: https://docs.minisend.xyz/api-reference/wallet-api/get-balance Read a wallet's live USDC balance directly from the chain it was issued on. Returns a wallet's current USDC balance, read live from the chain rather than summed from deposits Minisend has recorded — the chain is always the source of truth, so this figure can never drift from what's actually there. This only reports the balance on the chain the wallet was **issued** on. Because an address is identical across every EVM chain, a wallet issued on Base can still receive funds on Polygon or another chain — those funds exist, but this endpoint won't show them. The [`wallet.deposit.received` webhook](/wallet-api/webhooks) and the deposit endpoints report the chain a transfer actually landed on; use those to know about activity on chains other than the one you issued. ## Endpoint ```text theme={null} GET https://merchant.minisend.xyz/api/v1/wallets/{walletId}/balance ``` ```text theme={null} Authorization: Bearer wsk_live_your_key_here ``` ## Example ```bash theme={null} curl https://merchant.minisend.xyz/api/v1/wallets/3f6b2e1a-.../balance \ -H "Authorization: Bearer wsk_live_your_key_here" ``` ## Response (200) ```json theme={null} { "wallet_id": "3f6b2e1a-...", "wallet_ref": "user-9214", "address": "0xabc1230000000000000000000000000000dead", "chain": "BASE", "token": "USDC", "amount": "10.500000" } ``` The chain this balance covers — always the wallet's issued chain. A string, not a number. Compare and store it as a decimal. ## Errors | Status | Meaning | | ------ | ------------------------------------------------ | | `401` | Missing or invalid API key | | `404` | Unknown wallet, or it belongs to another account | | `502` | The balance couldn't be read right now. Retry | # Get a wallet Source: https://docs.minisend.xyz/api-reference/wallet-api/get-wallet Fetch a single wallet you created, by its Minisend-assigned id. Returns a wallet you created, looked up by the `id` from its creation response. ## Endpoint ```text theme={null} GET https://merchant.minisend.xyz/api/v1/wallets/{walletId} ``` ```text theme={null} Authorization: Bearer wsk_live_your_key_here ``` The path segment is the wallet's `id` — a UUID — **not** its `wallet_ref`. Wallets belonging to another account return `404`, and so does a value that isn't a well-formed UUID, so a `404` handler catches the "I passed a ref by mistake" case too. There is no lookup-by-`wallet_ref` endpoint. Since the ref is your own identifier, re-POST it to [create wallet](/api-reference/wallet-api/create-wallet) instead: that call is create-or-get and returns the existing wallet. ## Example ```bash theme={null} curl https://merchant.minisend.xyz/api/v1/wallets/3f6b2e1a-... \ -H "Authorization: Bearer wsk_live_your_key_here" ``` ## Response (200) ```json theme={null} { "wallet": { "id": "3f6b2e1a-...", "tenant_id": "a7c9e2d0-...", "master_wallet_id": "d4f81b3c-...", "wallet_ref": "user-9214", "address": "0xabc1230000000000000000000000000000dead", "chain": "BASE", "status": "active", "mode": "live", "metadata": null, "created_at": "2026-07-29T09:00:00.000Z" } } ``` Same shape as [create wallet](/api-reference/wallet-api/create-wallet)'s response. ## Errors | Status | Meaning | | ------ | ------------------------------------------------------------------------------------- | | `401` | Missing or invalid API key | | `404` | Unknown wallet, one belonging to another account, or a path segment that isn't a UUID | # Get a wallet by reference Source: https://docs.minisend.xyz/api-reference/wallet-api/get-wallet-by-ref Look a wallet up by your own identifier instead of Minisend's id. Looks up a wallet using the `walletRef` you supplied at creation, so you never need to persist Minisend's `id` alongside your own identifier. ## Endpoint ```text theme={null} GET https://merchant.minisend.xyz/api/v1/wallets/by-ref/{walletRef} ``` ```text theme={null} Authorization: Bearer wsk_live_your_key_here ``` Scoped to your account: two tenants can each use the same `walletRef` (e.g. `user_1`) without colliding. ## Example ```bash theme={null} curl https://merchant.minisend.xyz/api/v1/wallets/by-ref/user-9214 \ -H "Authorization: Bearer wsk_live_your_key_here" ``` ## Response (200) ```json theme={null} { "wallet": { "id": "3f6b2e1a-...", "tenant_id": "a7c9e2d0-...", "master_wallet_id": "d4f81b3c-...", "wallet_ref": "user-9214", "address": "0xabc1230000000000000000000000000000dead", "chain": "BASE", "status": "active", "mode": "live", "metadata": null, "created_at": "2026-07-29T09:00:00.000Z" } } ``` Same shape as [create wallet](/api-reference/wallet-api/create-wallet)'s response. ## Errors | Status | Meaning | | ------ | --------------------------------------------------------------- | | `400` | `walletRef` isn't 1-128 chars of letters, numbers, or `_ : . -` | | `401` | Missing or invalid API key | | `404` | No wallet with this reference on your account | # Get a wallet's deposits Source: https://docs.minisend.xyz/api-reference/wallet-api/get-wallet-deposits Paginated deposit history for one wallet, newest first. Returns the deposit history for one wallet — the polling counterpart to the [`wallet.deposit.received` webhook](/wallet-api/webhooks), and how you catch up after your endpoint was down longer than the retry window. ## Endpoint ```text theme={null} GET https://merchant.minisend.xyz/api/v1/wallets/{walletId}/deposits ``` ```text theme={null} Authorization: Bearer wsk_live_your_key_here ``` ## Query parameters Page size, max 100. Rows to skip. ## Example ```bash theme={null} curl "https://merchant.minisend.xyz/api/v1/wallets/3f6b2e1a-.../deposits?limit=50" \ -H "Authorization: Bearer wsk_live_your_key_here" ``` ## Response (200) ```json theme={null} { "deposits": [ { "id": "7b2e1a3f-...", "tenant_id": "a7c9e2d0-...", "wallet_id": "3f6b2e1a-...", "wallet_ref": "user-9214", "address": "0xabc1230000000000000000000000000000dead", "chain": "MATIC", "token": "USDC", "amount": "10.500000", "tx_hash": "0x55a572efe1720250e442f38741477a4fc3f7f152e5cd208cc52f8222a1c2a13b", "from_address": "0x9f2c...", "state": "complete", "detected_at": "2026-08-07T09:12:03.000Z", "created_at": "2026-08-07T09:12:03.000Z" } ], "total": 3, "limit": 50, "offset": 0 } ``` Same deposit shape as the [`wallet.deposit.received`](/wallet-api/webhooks) webhook. ## Errors | Status | Meaning | | ------ | ------------------------------------------------ | | `401` | Missing or invalid API key | | `404` | Unknown wallet, or it belongs to another account | # List all deposits Source: https://docs.minisend.xyz/api-reference/wallet-api/list-deposits Every deposit across every wallet on your account, newest first — the reconciliation endpoint. Returns deposits across **every** wallet on your account, newest first. Sweep this to reconcile after downtime instead of walking each wallet individually. ## Endpoint ```text theme={null} GET https://merchant.minisend.xyz/api/v1/deposits ``` ```text theme={null} Authorization: Bearer wsk_live_your_key_here ``` ## Query parameters Narrow to one wallet's Minisend id. To filter by your own reference instead, first resolve it with [get by reference](/api-reference/wallet-api/get-wallet-by-ref). Page size, max 100. Rows to skip. ## Example ```bash theme={null} curl "https://merchant.minisend.xyz/api/v1/deposits?limit=50" \ -H "Authorization: Bearer wsk_live_your_key_here" ``` ## Response (200) Same shape as [get a wallet's deposits](/api-reference/wallet-api/get-wallet-deposits), just not scoped to one wallet. ## Errors | Status | Meaning | | ------ | ----------------------------------- | | `400` | `wallet_id` isn't a valid id (UUID) | | `401` | Missing or invalid API key | # List wallets Source: https://docs.minisend.xyz/api-reference/wallet-api/list-wallets Paginated list of every wallet you've created, newest first. Lists your wallets, newest first. ## Endpoint ```text theme={null} GET https://merchant.minisend.xyz/api/v1/wallets ``` ```text theme={null} Authorization: Bearer wsk_live_your_key_here ``` ## Query parameters Page size, max 100. Rows to skip. ## Example ```bash theme={null} curl "https://merchant.minisend.xyz/api/v1/wallets?limit=50" \ -H "Authorization: Bearer wsk_live_your_key_here" ``` ## Response (200) ```json theme={null} { "wallets": [ { "id": "3f6b2e1a-...", "tenant_id": "a7c9e2d0-...", "master_wallet_id": "d4f81b3c-...", "wallet_ref": "user-9214", "address": "0xabc1230000000000000000000000000000dead", "chain": "BASE", "status": "active", "mode": "live", "created_at": "2026-07-29T09:00:00.000Z" } ], "total": 41, "limit": 50, "offset": 0 } ``` Full wallet objects, same shape as [create wallet](/api-reference/wallet-api/create-wallet). Total wallets matching your account. Use with `limit`/`offset` to paginate. The page size actually applied, not the one you asked for. Requests above 100 come back as `100`, and a missing or unparseable value comes back as the `20` default. The offset actually applied. Negative and unparseable values come back as `0`. # Business verification Source: https://docs.minisend.xyz/business-verification Submit your incorporation documents, founder details, and proof of address to activate live payouts on Minisend. Before we send fiat to your bank or M-Pesa account, we need to verify your business. This is a regulatory requirement from our settlement partners and protects both sides: it confirms the legal entity, the people behind it, and where the business actually operates. Most reviews finish within **2 business days** once we have everything. ## Where to send Email every document below to [info@minisend.xyz](mailto:info@minisend.xyz) from an email tied to your business domain. Use the subject line: ```text theme={null} KYB: {Your Business Name} ``` One email with everything attached is easier to process than several follow-ups. ## What we need Three groups of documents. PDFs, JPGs, or PNGs are all fine. ### 1. Incorporation documents Proof that your business is a registered legal entity. * **Certificate of incorporation** (or equivalent business registration certificate) * **Memorandum and articles of association** (or operating agreement for LLCs) * **Tax identification**: TIN, KRA PIN, FIRS TIN, GRA TIN, or URA TIN, whichever applies to your country * **Current business license** if your activity requires one (e.g., financial services, lending) ### 2. Founder and director details For each director and any beneficial owner holding **25% or more**: * **Government-issued photo ID**: passport, national ID, or driver's license * **Full legal name, date of birth, and nationality** * **Role at the business** (director, CEO, shareholder, etc.) * **Shareholder register or cap table** if the company has more than one owner ### 3. Proof of address We need two: * **Business address**: a recent utility bill, bank statement, lease, or government correspondence in the company's name, **dated within the last 3 months**. * **Director's residential address**: the same kinds of documents, in the director's personal name, also dated within the last 3 months. If your business operates from a virtual or coworking address, include the agreement showing your registered business address there. ## What happens next Expect a reply confirming receipt and flagging anything obviously missing. Our compliance team verifies each document and runs the standard checks. We may email you for clarification; please reply on the same thread. You receive an approval email. Your dashboard switches to verified status, payouts unlock, and you can move your integration to production. ## Until you're verified You can still build and test end to end. The API, dashboard, payment links, and webhooks all work in full; settlement to your local-currency payout account is the only thing gated by verification. ## FAQ Businesses registered in Kenya, Nigeria, Ghana, and Uganda are supported today. If you're incorporated elsewhere but want to pay out to one of these currencies, [reach out](https://t.me/minisendapp) before submitting. Yes. Send the equivalent registration certificate (business name registration, single business permit) plus the owner's full ID and proof of address. The structure section is just shorter for single-owner businesses. Issued within the **last 3 months**. Older documents get rejected automatically. Submit your ID, residential proof, and percentage ownership the same way directors do. Anyone holding 25%+ counts as a beneficial owner. Documents are stored encrypted, used only for verification and ongoing compliance monitoring, and never shared outside Minisend and our settlement partners. We delete them on request if you close your account. Yes. Reply on the original email thread with the corrected document. There's no penalty and no waiting period. Questions before you submit? [Message us on Telegram](https://t.me/minisendapp) and we'll walk you through it. # API keys Source: https://docs.minisend.xyz/dashboard/api-keys Generate, copy, and revoke API keys to authenticate checkout session requests from your backend. Keys use the ms_live_ prefix and are shown only once. API keys authorise your backend to create checkout sessions. **Dashboard → API Keys**. Never put keys in frontend code, mobile apps, or public repos. Backend env vars only. ## Format ```text theme={null} ms_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` The list shows a shortened preview (e.g., `ms_live_abc123...`). The full key is shown **once** at creation — Minisend stores only a hash. ## Generate Sidebar → **API Keys**. Top-right button. e.g., `Production` or `Staging`. Blank defaults to `Default`. The key appears in a banner. Copy and store it — closing or dismissing the banner means generating a new key. Create multiple keys (one per environment). Revoking one doesn't affect the others. ## Use in requests ```text theme={null} Authorization: Bearer ms_live_{your_key} ``` ```bash theme={null} curl -X POST https://merchant.minisend.xyz/api/merchant/checkout \ -H "Authorization: Bearer ms_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "amount": 25.00, "description": "Order #123", "external_id": "order_123" }' ``` ## Revoke Hover the row → click the trash icon. Deactivated immediately — subsequent requests return `401`. Revocation is permanent. If a production server uses the key, update its env var first. ## Best practices * Rotate periodically, immediately if exposed. * Separate keys per environment. * Set as a server-side env var (`MINISEND_API_KEY`), never hardcode. * Accidentally committed to a public repo → revoke and regenerate. # Dashboard overview Source: https://docs.minisend.xyz/dashboard/overview Your wallet balance, USDC volume, collections split, revenue chart, and recent transactions on one screen. The landing page when you sign in at [merchant.minisend.xyz/dashboard](https://merchant.minisend.xyz/dashboard). Everything updates automatically. ## Wallet card Your business wallet holds the USDC from M-Pesa collections and anything you top up yourself. The card shows your live balance and two actions: * **Top up**: send USDC on Base to the address shown. * **Withdraw**: send funds to any M-Pesa number. Payouts arrive in about ten seconds. See [Wallet](/dashboard/wallet) for the full flow. ## Stats cards These figures cover activity across checkout, [off-ramp](/offramp/overview), and [onramp](/onramp/overview) together, if you use more than one. | Metric | What it shows | | ---------------- | --------------------------------------------------------------------------------------- | | **Volume** | Total USDC-equivalent from **completed** payments only (USDC and USDT inflows combined) | | **Transactions** | Total sessions and orders created, all statuses | | **Success rate** | Completed as a share of completed plus failed | | **Fees** | Cumulative platform fees on checkout, in USDC-equivalent | Collections are split by method, so you can see how much came in through crypto and how much through M-Pesa. **Volume only ever counts completed payments.** A pending, failed, or expired session or order never inflates it, no matter how large. Volume and fees are USDC-equivalent, with USDT inflows normalised to USDC on Base. Local-currency fee totals reflect checkout only, since off-ramp and onramp orders can be in whatever currency you chose per order. ## Revenue chart 7-day area chart of daily volume. Hover for the exact value. ## Payment link card If you've set a slug in **Settings**, your full URL appears here. **Copy Link** copies it to the clipboard, and **Preview** opens your customer-facing page. ```text theme={null} https://merchant.minisend.xyz/pay/your-slug ``` ## Recent transactions Your last six sessions. Each row shows amount, reference (your `external_id` or a shortened ID), date, and status badge. Click a row for the inspector. **View all** opens [Transactions](/dashboard/transactions). ## Status badges | Badge | Meaning | | ------------- | ------------------------------------------------- | | **Pending** | Waiting for the customer to send | | **Received** | Detected on-chain; settlement started | | **Settling** | Conversion and payout in progress | | **Completed** | Payout delivered | | **Failed** | Failed after the deposit arrived. Contact support | | **Expired** | No deposit in 30 minutes | Mostly **Completed** means healthy. Frequent **Failed** means check your payout details in **Settings**. ## Explore Balance, top-ups, and instant M-Pesa withdrawals. Pay your whole team to M-Pesa in one run. Full history with search, filters, and receipts. Business info, payout, slug, webhook URL. # Payroll Source: https://docs.minisend.xyz/dashboard/payroll Pay your whole team to M-Pesa in one run, funded from your wallet balance. Import employees by CSV, review the total, and track every payment. Pay salaries to M-Pesa in one run, funded from your [wallet](/dashboard/wallet) balance. **Dashboard → Payroll**. ## Create a run Click **New pay run** and optionally name the employer entity. Add rows manually or import a CSV with name, M-Pesa number, and KES amount. Up to 5,000 employees per run; each payment between KES 10 and KES 250,000. The run shows the employee count and total. Fund it from your wallet balance; the rate is locked when the run is funded. Payments fan out to each M-Pesa number automatically. Track each one on the run page. ## Run statuses | Status | Meaning | | ------------------- | ------------------------------------------------- | | **Draft** | Being set up, nothing committed | | **Awaiting funds** | Waiting for the wallet balance to cover the total | | **Funded** | Paid for, queued to execute | | **Paying out** | Payments in flight | | **Completed** | Everyone paid | | **Needs attention** | Some payments failed. Review and retry | | **Cancelled** | Abandoned before funding | Individual payments move through **Queued → Sending → Paying → Paid**, with **Failed** for numbers that could not be paid. ## Pricing Payroll is **free** during the introductory period. Conversions use the rate locked at funding time. Failed payments never disappear silently: the run lands on **Needs attention** and the unpaid rows stay listed until resolved. # Settings Source: https://docs.minisend.xyz/dashboard/settings Configure your business name, payout currency and method, payment link slug, and webhook URL so Minisend knows exactly where to send your money. Complete these before accepting payments. **Dashboard → Settings**. Click **Save Changes** when done. ## Business | Field | Purpose | | ----------------- | ---------------------------- | | **Business name** | Shown on the checkout page. | | **Email** | Account communications. | | **Website** | Optional. Shown on checkout. | ## Payout Currency determines which payout methods are available. | Currency | Country | Methods | | -------- | ------- | -------------------------------- | | **KES** | Kenya | M-Pesa, Buy Goods, Paybill, Bank | | **NGN** | Nigeria | Bank | | **GHS** | Ghana | Mobile Money, Bank | | **UGX** | Uganda | Mobile Money, Bank | ### M-Pesa mobile (KES) Phone number (`07XXXXXXXX`) + network (Safaricom). ### Buy Goods till (KES) Your till number. Settlements show as customer payments in your till statement. ### Paybill (KES) Paybill number + account number. Both required. ### Bank Transfer (KES, NGN, GHS, UGX) Account number, bank code, bank name. **Account Name** must match the bank record exactly — mismatches cause failed disbursements. ### Mobile Money (GHS, UGX) Phone number + network (MTN, Airtel, etc.). **Account Name** is required for all methods. It must match the name on your bank, M-Pesa, or mobile money account. ## Settlement mode Pick how payments settle by default. Local currency converts automatically and pays out to M-Pesa, mobile money, or your bank, same as always. USDC skips that step, costs less, and leaves the deposit sitting in your [wallet](/dashboard/wallet). This sets the default for new sessions. You can also override it per session from [checkout sessions](/payments/checkout-sessions#settlement-mode) instead. Once a session is created its mode is locked in, so changing this setting later won't touch anything already in progress. ## Destination chain If you're settling in USDC, pick which chain your payments land on: Base, Arbitrum, Avalanche, Optimism, Ethereum, or Polygon. Base needs nothing extra, since that's where the deposit already sits. Any other chain gets forwarded there automatically after each payment, and you cover a small bridge fee out of the amount. Switching chains only changes where the next payment lands, not USDC you're already holding. Your [wallet](/dashboard/wallet) shows a separate balance for every chain you've received on. ## Payment link Set a unique slug → get a shareable URL: ```text theme={null} https://merchant.minisend.xyz/pay/your-slug ``` Slugs are unique across Minisend. If yours is taken, try a variation. ## Webhook URL Minisend POSTs `checkout.completed`, `checkout.failed`, and `checkout.expired` events here when a session reaches a terminal state. HMAC-SHA256 signed. Up to 5 retries with exponential backoff on non-2xx. ```text theme={null} https://yourbusiness.com/webhooks/minisend ``` Leave blank to skip server-side notifications. You can poll the status endpoint instead. See [Webhooks](/webhooks/overview) for signature verification and event handling. ## Webhook secret Your webhook secret signs every event with the `X-Minisend-Signature` header. It's set automatically when your account is created, but the value is never shown again after that point unless you generate a new one here. Click **Generate secret** (or **Regenerate secret** if one already exists) to reveal a new value. It's displayed once, in a copy-once banner, exactly like an API key. Copy it immediately and store it wherever your server reads its webhook configuration from. Regenerating immediately invalidates the previous secret. Every signature Minisend computes afterward uses the new one, so update your server before or right after you regenerate. ## Saving Click **Save Changes** (or **Create Account** if new). Required fields are marked with `*`. # Team Source: https://docs.minisend.xyz/dashboard/team Invite teammates to your dashboard by email, with owner, admin, and read-only member roles. Invite teammates to manage your account alongside you. **Dashboard → Team**. ## Roles | Role | Can do | | ---------- | ------------------------------------------------------------------------------------------------- | | **Owner** | Everything. The account that signed up; always exactly one, never invited | | **Admin** | Everything except owner-only actions: invite and remove teammates, manage API keys, edit settings | | **Member** | View only. Cannot edit settings, manage API keys, or invite or remove anyone | Owners and admins can invite; members cannot invite anyone, even other members. ## Invite a teammate Choose **Admin** or **Member**. They receive an email with an accept link. Nothing changes on your account until they accept. They sign in (or create an account) with the invited email address and land on your dashboard with the role you set. The accept link only works for the email address it was sent to — someone can't forward it to sign in as themselves instead. ## Managing the team Change a member's role or remove them from the list at any time. Removing someone revokes their access immediately; they keep their own account, just lose access to yours. You can't demote or remove the owner. Ownership doesn't transfer from this page. # Transactions Source: https://docs.minisend.xyz/dashboard/transactions Browse your full Minisend payment history, filter by status, search by reference ID or amount, and inspect individual transaction details in the panel. Every checkout session, off-ramp order, and onramp order on your account: successes, failures, expirations, in-flight. **Dashboard → Transactions**. If you integrate only with the [off-ramp](/offramp/overview) or [onramp](/onramp/overview) API and never touch checkout, your activity still shows up here. ## Type filter * **All** * **Checkout** * **Off-ramp** * **Onramp** Rows from off-ramp and onramp carry a small type badge so you can tell them apart from checkout at a glance; checkout rows (the majority for most merchants) are unbadged. ## Search Filters the current page by: * `external_id` (your reference) * Internal transaction ID * USDC-equivalent amount Case-insensitive. ## Status filters * **All** * **Pending** * **Received** * **Settling** * **Completed** * **Failed** * **Expired** Changing the filter resets to page 1. **Refresh** reloads with the latest data. ## Columns | Column | Contents | | ------------- | ------------------------------------------------------------------------------------------------ | | **Amount** | USDC-equivalent. Local currency below when completed. | | **Reference** | Your `external_id` or a shortened transaction ID. | | **Method** | How the customer paid: a chain and asset (USDC on Base, USDT on Polygon) or an **M-Pesa** badge. | | **Type** | Checkout, off-ramp, or onramp badge (checkout is unbadged). | | **Status** | Current badge. | | **Date** | Session creation timestamp. | ## Status reference | Status | Meaning | | ------------------ | -------------------------------------------------- | | `pending` | Waiting for the customer. Expires in 30 minutes. | | `deposit_received` | Detected; settlement initiated. | | `settling` | Conversion and payout in progress. | | `completed` | Payout delivered. | | `failed` | Failed after the deposit arrived. Contact support. | | `expired` | No deposit in 30 minutes. | `failed` doesn't mean lost funds. Contact support with the transaction ID. ## Inspector panel Click any row to open it. Shows: * **Amount** in USDC-equivalent, plus local currency when completed * **Status** * **Method**: chain and asset, or M-Pesa * **Reference**: your `external_id` or transaction ID * **Transaction ID**: the full internal ID * **Created**, **Completed**, and **Duration** * **Receipt**: the payout reference, with a **Download receipt** button for a shareable PDF * **Timeline**: step-by-step progression Use the receipt to reconcile with your M-Pesa or bank statement. The downloaded PDF is customer-shareable; customers can also download it themselves from the checkout page. ## Pagination 20 rows per page. The counter shows `1–20 of 143`. # Wallet Source: https://docs.minisend.xyz/dashboard/wallet Your business USDC balance: top up on Base, withdraw to M-Pesa in about ten seconds, and download receipts for every payout. Your business wallet is where USDC lands from M-Pesa collections and stays until you move it. It lives on the dashboard [overview](/dashboard/overview). ## Balance The card shows your live USDC balance on Base. That's headlined because M-Pesa collections always land there and withdrawals always pull from it, no matter which [destination chain](/dashboard/settings#destination-chain) you've picked for checkout. If you're settling checkout payments in USDC on another chain, you'll see a balance for that too. Everything updates automatically as collections settle, forwards land, and withdrawals go out. ## Top up Send USDC on **Base** to the address shown on the card. Funds appear as soon as the transfer confirms. Send USDC on Base only. Other assets or chains sent to this address are not detected. ## Withdraw to M-Pesa Click **Withdraw**, enter the KES amount and the receiving M-Pesa number, and confirm. The modal shows what the recipient receives, the amount leaving your wallet, and the live rate before you commit. Payouts arrive in about ten seconds. The withdrawal fee is paid by you: the recipient gets exactly the amount you type, and your wallet is debited slightly more to cover it. ## Receipts Every withdrawal produces a receipt with the M-Pesa confirmation code. Click **Download receipt** for a PDF you can file or share. ## What the wallet is for * **M-Pesa collections** park here as USDC instead of auto-settling, so you choose when to cash out. * [**Payroll**](/dashboard/payroll) runs are funded from this balance. * **Treasury**: hold USDC as a dollar balance and withdraw to KES when the rate suits you. # Wallets Source: https://docs.minisend.xyz/dashboard/wallets Activate chains and generate keys for the Wallet API, and choose which networks your checkout accepts deposits on. **Dashboard → Wallets.** Two unrelated things live on this page: the [Wallet API](/wallet-api/overview)'s master wallets and keys, and which chains your checkout accepts. ## Master wallets A master wallet is your container on one chain for the [Wallet API](/wallet-api/overview) — every address you issue for your own users on that chain is created under it. Each card shows a chain you can activate. Base is available on every plan; the others require a premium plan and show **Requires a premium plan** until then. Click a card to open its details: address, live balance, and the child addresses issued under it. ## Activity Three stat tiles (master wallets, addresses generated, and API keys) followed by a chronological feed of everything that's happened across the Wallet API: activations, addresses created, deposits received, and keys generated and revoked. ## API keys Generate a `wsk_live_` key for the Wallet API here. There is no test-key option — the Wallet API has no sandbox. Click **New API key**, give it a label (e.g. `Production`), and create it. The full key appears once, in a banner. Copy it immediately — Minisend stores only a hash. **Revoke** deactivates a key immediately. Requests using it afterward return `401`. If you haven't created a key yet, this section shows a prompt instead of a list: create a key to start generating wallets for your users. ## Checkout deposits At the bottom of the page, a separate section from the Wallet API above: which chains your **checkout** accepts USDC deposits on (the product covered in [Payment links](/payments/payment-links) and [Checkout sessions](/payments/checkout-sessions)). Toggle a chain off to stop accepting deposits on it; Base cannot be disabled. See [supported networks](/payments/supported-currencies#restricting-accepted-networks). # FAQ Source: https://docs.minisend.xyz/help/faq Common questions about accepting USDC and USDT, receiving local currency payouts, settlement speed, fees, payment link setup, and testing your integration. USDC on 19 chains, USDT on 14 chains. Same deposit address for both. Settles to KES, NGN, GHS, UGX via M-Pesa, Buy Goods, Paybill, mobile money, or bank transfer depending on the currency. Configure once in **Settings**. **USDC (19):** Base (primary), Arbitrum, Avalanche, Codex, EDGE, Ethereum, HyperEVM, Ink, Linea, Monad, Morph, Optimism, Plume, Polygon, Sei, Sonic, Unichain, World Chain, XDC. **USDT (14):** ADI, Arbitrum, Aurora, Avalanche, Bera, BNB Chain, Ethereum, Gnosis, Monad, Optimism, Plasma, Polygon, Scroll, XLayer. See [supported networks](/payments/supported-currencies). * **USDC on Base** — fastest. Settlement starts immediately. * **USDC on another chain** — CCTP bridge to Base, then settle. A few minutes. * **USDT on any chain** — swap to USDC on Base in **30–50 seconds**, then settle. After USDC is on Base, the M-Pesa/bank/mobile money transfer follows provider timing. You receive a `checkout.completed` webhook and see `completed` in the dashboard. No. Customers send stablecoins; you receive local currency. Your only interactions are the Minisend dashboard and your existing bank or mobile money account. Sessions expire after **30 minutes** with no deposit. Expired sessions appear with **Expired** status and are not charged. Minisend also sends a `checkout.expired` webhook. If a customer says they paid but the session expired, ask for the transaction hashand address. They may have sent to the wrong address or on an unsupported chain. Your own reference passed at session creation (e.g., `order-4821`). Echoed back in every webhook for that session, so you can match payments to orders without maintaining a session-to-order mapping. Optional, but strongly recommended for any system with order records. Rare. Status moves to **Failed** and Minisend sends `checkout.failed`. Contact [support](https://t.me/minisendapp) with the `session_id`. Customer funds are not lost. Minisend will investigate and contact you. Yes. Set a slug in **Settings** and share your payment link: ```text theme={null} https://merchant.minisend.xyz/pay/your-slug ``` Customers visit, enter an amount, send USDC or USDT. You receive local currency. 1. Create a session via API with `amount: 1.00`. 2. Open the `checkout_url`. 3. Pay from a real wallet. 4. Confirm your webhook gets `checkout.completed`. 5. Check the dashboard for **Completed**. 6. Verify funds in your payout account. There is no sandbox; tests use real funds. Use the smallest practical amount. Default is **60 req/min/IP**. If you're hitting `429`s, contact support. # How it works Source: https://docs.minisend.xyz/how-it-works Follow a payment from stablecoin deposit to local currency payout, and the status your session moves through at each step. ## The flow One deposit address accepts both assets across all supported chains. The customer doesn't pick a network from a menu — they just send. The session status moves to `deposit_received`. If nothing arrives within **30 minutes**, it expires instead. USDC sent on another chain bridges to Base through Circle CCTP. USDT on any chain routes through Minisend's swap layer and lands as spendable USDC on Base within **30 to 50 seconds**. Cross-chain rounding is tolerated when matching the amount. Converted at the live rate. The platform fee was already included in your customer's total, so you receive the full amount you set. Status moves to `settling`. M-Pesa, mobile money, or bank transfer. Status moves to `completed` and a webhook fires. ## Status lifecycle | Status | Meaning | | ------------------ | ------------------------------------------------------------ | | `pending` | Waiting for the customer to send | | `deposit_received` | Detected on-chain; normalisation started | | `settling` | Conversion + payout in progress | | `completed` | Payout delivered | | `failed` | Settlement failed after the deposit arrived. Contact support | | `expired` | No deposit within 30 minutes | ## Multi-asset, multi-chain USDC on 19 chains, USDT on 14 chains, all to the same address. Base settles fastest because no bridging step is required. See the [full network list](/payments/supported-currencies). # One Tap USDT/USDC for your business. Source: https://docs.minisend.xyz/introduction Your customer pays in **USDC or USDT**. You get paid in **KES, NGN, GHS, or UGX** — straight to M-Pesa, mobile money, or a bank account. No wallets to manage, no price exposure, nothing to swap. Minisend handles every step in between: detection across 33 chains, normalising USDT to USDC on Base, conversion at the live rate, and the local payout. Install the Minisend skill and your agent (Claude Code, Cursor, Codex, and others) integrates every API in this site correctly, offline. ## How it looks One API call from your backend: ```bash theme={null} curl -X POST https://merchant.minisend.xyz/api/merchant/checkout \ -H "Authorization: Bearer ms_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{"amount": 25.00, "external_id": "order-4821"}' ``` You get back a hosted `checkout_url`. Redirect the customer, they pay, you receive a webhook when the money lands. ## Pick your path Server-side checkout sessions, webhooks, full control. **Five minutes to your first payment.** A permanent URL tied to your business. Share it anywhere. Zero engineering. Follow a payment from on-chain deposit to your bank account, status by status. ## What you'll find in here USDC on 19 chains. USDT on 14 chains. KES, NGN, GHS, UGX out. Authentication, endpoints, request and response shapes, error codes. Signed events when a checkout completes, fails, or expires. Send USDC, pay out M-Pesa, mobile money, or bank accounts. For payroll, remittances, and supplier payments. Collect M-Pesa payments from a customer's phone and receive USDC in your own wallet. Generate a real on-chain address for every user of your own product. Install a skill that teaches Claude Code, Cursor, and other agents this entire API surface. # Connect your agent with MCP Source: https://docs.minisend.xyz/mcp-server Add the Minisend MCP server and let your coding agent quote payouts, create orders, collect M-Pesa payments, and provision wallets directly. The Minisend MCP server gives your coding agent tools it can call against your account. Instead of writing the request yourself, you can say "pay 5,000 KES to 0712345678 on M-Pesa" and the agent quotes it, validates the recipient, and creates the order. It also answers questions about the API. The knowledge tools work without a key, so an agent can learn the recipient rules and webhook behaviour before you have provisioned anything. ## Add it ```bash Claude Code theme={null} claude mcp add --transport http minisend https://mcp.minisend.xyz/mcp \ --header "Authorization: Bearer ms_live_..." ``` ```json Cursor, VS Code, Windsurf theme={null} { "mcpServers": { "minisend": { "url": "https://mcp.minisend.xyz/mcp", "headers": { "Authorization": "Bearer ms_live_..." } } } } ``` That is the whole install. Nothing to download, nothing to keep up to date. Get your key from the dashboard under [API keys](/dashboard/api-keys). ### Wallet API tools The Wallet API uses a separate key namespace, so it takes a second header. Add it only if you use that product. ```bash Claude Code theme={null} claude mcp add --transport http minisend https://mcp.minisend.xyz/mcp \ --header "Authorization: Bearer ms_live_..." \ --header "X-Minisend-Wallet-Key: wsk_live_..." ``` ```json Cursor, VS Code, Windsurf theme={null} { "mcpServers": { "minisend": { "url": "https://mcp.minisend.xyz/mcp", "headers": { "Authorization": "Bearer ms_live_...", "X-Minisend-Wallet-Key": "wsk_live_..." } } } } ``` A `ms_live_` key will not work on the Wallet API, and a `wsk_live_` key will not work anywhere else. ## What your agent can do | Product | Tools | | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | Off-ramp | `offramp_quote`, `offramp_validate_account`, `offramp_create_order`, `offramp_submit_deposit`, `offramp_get_order`, `offramp_list_orders` | | Onramp | `onramp_quote`, `onramp_create_order`, `onramp_get_order`, `onramp_list_orders` | | Checkout | `checkout_create_session`, `checkout_get_session` | | Wallet API | `wallet_create`, `wallet_get`, `wallet_get_by_ref`, `wallet_balance`, `wallet_deposits`, `wallet_list_deposits` | | Account | `minisend_whoami` | ### Knowledge tools These answer from data built into the server. No key, no network call, no rate limit. | Tool | What it answers | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `minisend_api_schema` | The exact request and response fields for an endpoint, including which headers change its behaviour | | `minisend_recipient_rules` | Required recipient fields for a currency and payout method, accepted phone formats, exact mobile network names, and the per-transaction limits | | `minisend_explain_error` | What a status code and error message actually mean, and whether retrying can help | | `minisend_webhook_spec` | Event names, the signature scheme, and the ordering rules a naive handler gets wrong | They exist because these are the questions where a wrong answer produces a failed payout rather than a confused reader. Recipient fields differ by both currency and method, and mobile network names are case-sensitive exact strings. ## You only see the tools your key can use The server asks the API what your key is authorised for and lists tools accordingly. If your account does not have off-ramp access yet, no off-ramp tools appear, so your agent never burns a turn on a call that was always going to fail. Wallet tools appear only when you supply the wallet key header. If you expected payment tools and only see knowledge tools, ask your agent to run `minisend_diagnose_key`. The usual cause is a key that was truncated on the way into your config, or a placeholder that was never replaced. After changing headers, restart your client. Tool lists are read once when the connection opens. ## These tools move real money There is no sandbox. Every call goes to production. `offramp_create_order` creates a real payout order, and `onramp_create_order` sends a real payment prompt to a real phone. MCP clients ask for approval before each tool call. Leave that on. Read what the agent is about to do, particularly the amount and the recipient, before approving. Two behaviours worth knowing, because they are the ones an agent can get wrong: Creating an off-ramp order does not pay anyone. The flow is non-custodial, so you send the USDC yourself, and for KES, GHS, and UGX you then report the transaction hash. The order response tells you which path applies. Creating an onramp order is one-shot. The prompt fires immediately, and if the customer cancels it, the order fails. Create a new order rather than retrying the same one, which is what stops a customer being charged twice. ## Your keys Keys are read from the request headers, passed to the Minisend API, and discarded. Nothing is stored, cached, or written to logs. The server holds no credentials of its own. Standard rate limits apply and are counted against your account, not against the MCP server, so other users cannot consume your budget. ## MCP or the skill? Both, if you like. They solve different problems. The [agent skill](/ai-agent-skill) teaches your agent the API offline so it writes correct integration code. Nothing is called, and it works with no key and no network. The MCP server lets your agent operate your account live. Use it to try a payout before writing any code, to check an order's status while debugging, or to run one-off operations without building a script. Generate the key you will paste into the header. # Off-ramp API Source: https://docs.minisend.xyz/offramp/overview Send USDC from your own wallet and Minisend pays out KES, NGN, GHS, or UGX to any recipient: M-Pesa, mobile money, till, paybill, or bank account. The off-ramp API turns USDC into local currency payouts to **any recipient**: a phone number, a till, a paybill, or a bank account. Checkout collects money from your customers; the off-ramp API sends money out. Use it for remittances, payroll, supplier payments, or to add an off-ramp feature to your own product. ## Non-custodial by design Minisend never holds your funds. USDC moves directly from your wallet into settlement, and Minisend delivers the local currency payout. The deposit flow depends on the payout currency: | Payout currency | Deposit flow | | --------------- | ---------------------------------------------------------------------- | | KES, GHS, UGX | Send USDC to the settlement address, then submit your transaction hash | | NGN | Send USDC to a single-use deposit address. Detected automatically | Every order includes a `refund_address`. If a payout cannot be delivered, funds return there. Minisend never keeps them. ## How it works `POST /api/offramp/quote` returns the rate, the fee, and what the recipient will receive. `POST /api/offramp/orders` validates the recipient's account, then returns a `deposit_address` and the exact amount to send. Transfer `total_deposit_usdc` from your wallet to `deposit_address` before `expires_at`. `POST /api/offramp/orders/{order_id}/deposit` with your transaction hash. NGN deposits are detected automatically. The order completes and an `offramp.completed` webhook delivers the payout receipt. ## Order lifecycle | Status | Meaning | | ----------- | -------------------------------------------------------------------------- | | `pending` | Waiting for your USDC deposit | | `settling` | Deposit accepted, payout in flight | | `completed` | Recipient paid. `settlement_receipt` holds the payout reference | | `failed` | Payout failed. NGN deposits are refunded to `refund_address` automatically | | `expired` | No deposit before `expires_at`. Nothing was sent, nothing is owed | `completed`, `failed`, and `expired` are final. Track orders by [polling](/api-reference/offramp/get-order) or with [webhooks](/offramp/webhooks). ## Fees and rates | Currency | Fee | Rate | | ------------- | ---------------------------------------------- | ------------------------------------------------------------------- | | KES, GHS, UGX | Platform fee, deducted from the local amount | Executed live when your deposit is processed; quotes are indicative | | NGN | No separate fee. Margin is built into the rate | Locked when the order is created | The recipient receives `amount_local − fee`. Every quote includes `recipient_amount`, so you can show the exact payout before creating an order. ## Limits Orders accept **0.5 – 50,000 USDC**, and the local equivalent must fall inside the per-transaction range for the currency: | Currency | Per-transaction range | | -------- | --------------------- | | KES | 20 – 250,000 | | GHS | 5 – 5,000 | | UGX | 500 – 5,000,000 | Amounts outside these ranges are rejected at quote time with a `400`, before any funds move. ## Getting access Off-ramp access is enabled per account. Email [info@minisend.xyz](mailto:info@minisend.xyz) to activate it. Once enabled, your existing `ms_live_` API key carries the `offramp` scope and every endpoint in this section works. Calls made before that return a `403` telling you to request access. See [key scopes](/api-reference/authentication#key-scopes). Quote, create, deposit, and confirm an order end to end. # Off-ramp quickstart Source: https://docs.minisend.xyz/offramp/quickstart Convert USDC to an M-Pesa or bank payout in four API calls: quote the amount, create an order, send USDC on Base, and submit the transaction hash. This guide pays 10 USDC to a Kenyan M-Pesa number, then covers what changes for NGN. **Before you start, you need:** * An API key with the `offramp` scope. See [getting access](/offramp/overview#getting-access). * A wallet you control holding USDC on **Base**. Include the recipient to validate the account and get the registered name in the same call. ```bash theme={null} curl -X POST https://merchant.minisend.xyz/api/offramp/quote \ -H "Authorization: Bearer ms_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "amount": 10, "currency": "KES", "recipient": { "method": "MOBILE", "account_name": "Jane Wanjiku", "phone": "0712345678", "mobile_network": "Safaricom" } }' ``` ```json theme={null} { "amount_usdc": 10, "currency": "KES", "rate": 129.45, "amount_local": 1294, "fee": 13, "recipient_amount": 1281, "recipient_name": "JANE WANJIKU", "expires_at": "2026-07-05T12:05:00.000Z" } ``` `recipient_amount` is what lands in the recipient's account. For KES, GHS, and UGX the quote is indicative; the payout executes at the live rate when your deposit is processed. Pass an `Idempotency-Key` header so a network retry can never create two orders. ```bash theme={null} curl -X POST https://merchant.minisend.xyz/api/offramp/orders \ -H "Authorization: Bearer ms_live_your_key_here" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: payout-8412" \ -d '{ "amount": 10, "currency": "KES", "refund_address": "0xYourWalletAddress0000000000000000000000", "reference": "payout-8412", "recipient": { "method": "MOBILE", "account_name": "Jane Wanjiku", "phone": "0712345678", "mobile_network": "Safaricom" } }' ``` ```json theme={null} { "order_id": "9b2f6c1e-...", "status": "pending", "amount_usdc": 10, "total_deposit_usdc": 10, "currency": "KES", "rate": 129.45, "amount_local": 1294, "fee": 13, "recipient_amount": 1281, "deposit_address": "0x8005ee53e57ab11e11eaa4efe07ee3835dc02f98", "deposit_chain": "base", "expires_at": "2026-07-05T12:30:00.000Z", "instructions": "Send exactly 10 USDC (Base) to deposit_address from your own wallet, then submit the transaction hash via POST /api/offramp/orders/9b2f6c1e-.../deposit before expires_at." } ``` Minisend validates the recipient's account before creating anything. An invalid account returns `422`. From your own wallet, transfer `total_deposit_usdc` USDC on **Base** to `deposit_address` before `expires_at` (30 minutes for KES, GHS, and UGX). Send the exact amount, in USDC, on Base. A short transfer or a transfer on another chain cannot be verified in the next step. ```bash theme={null} curl -X POST https://merchant.minisend.xyz/api/offramp/orders/9b2f6c1e-.../deposit \ -H "Authorization: Bearer ms_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{"transaction_hash": "0x55a572efe1720250e442f38741477a4fc3f7f152e5cd208cc52f8222a1c2a13b"}' ``` A `200` with `"status": "settling"` means the payout is in flight. A `422` means the transfer could not be verified; check the amount, chain, and hash, then retry the same call. Poll the order, or listen for the webhook: ```bash theme={null} curl https://merchant.minisend.xyz/api/offramp/orders/9b2f6c1e-... \ -H "Authorization: Bearer ms_live_your_key_here" ``` When the payout lands, `status` becomes `completed`, `settlement_receipt` holds the payout receipt (an M-Pesa code for KES), and an `offramp.completed` webhook fires. See [off-ramp webhooks](/offramp/webhooks). ## NGN payouts NGN orders pay out to bank accounts, and the deposit flow differs in two ways: 1. **The recipient shape** uses a bank `institution` code and `account_number`. See [recipients](/offramp/recipients). 2. **There is no hash submission.** The order returns a **single-use** `deposit_address`. Send `total_deposit_usdc` (the order amount plus any `sender_fee_usdc` and `transaction_fee_usdc`) before `expires_at`, about **5 minutes**, and the deposit is detected automatically. ```bash theme={null} curl -X POST https://merchant.minisend.xyz/api/offramp/orders \ -H "Authorization: Bearer ms_live_your_key_here" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: payout-8413" \ -d '{ "amount": 100, "currency": "NGN", "refund_address": "0xYourWalletAddress0000000000000000000000", "recipient": { "account_name": "Chidi Okafor", "institution": "GTBINGLA", "account_number": "0123456789" } }' ``` The 5-minute NGN window is short because the rate is locked at creation. Create the order only when you are ready to send, and automate the transfer. If the window lapses the order expires; create a new one. If a payout fails after you deposit, the funds are refunded to your `refund_address` automatically. # Recipients Source: https://docs.minisend.xyz/offramp/recipients The recipient object shape for every payout method: M-Pesa and mobile money numbers, Kenyan tills and paybills, and bank accounts in Kenya and Nigeria. Every order and quote carries a `recipient` object. Its required fields depend on the payout currency and method. ## Methods per currency | Currency | Methods | | -------- | -------------------------------------------------------- | | KES | `MOBILE`, `BUY_GOODS` (till), `PAYBILL`, `BANK_TRANSFER` | | GHS | `MOBILE` | | UGX | `MOBILE` | | NGN | Bank transfer (no `method` field, see below) | ## MOBILE: KES, GHS, UGX ```json theme={null} { "method": "MOBILE", "account_name": "Jane Wanjiku", "phone": "0712345678", "mobile_network": "Safaricom" } ``` Recipient's name as you know it. Used on the payout record. Local format `0XXXXXXXXX`. International formats (`+254712345678`, `254712345678`) are accepted and normalised. The recipient's mobile money network: | Currency | Networks | | -------- | -------------------------------------------------- | | KES | `Safaricom` (alias: `m-pesa`), `Airtel` | | GHS | `MTN`, `Vodafone` (alias: `Telecel`), `AirtelTigo` | | UGX | `MTN`, `Airtel` | ## BUY\_GOODS: KES tills ```json theme={null} { "method": "BUY_GOODS", "account_name": "Mama Njeri Shop", "till": "832909" } ``` M-Pesa till number, 5–7 digits. ## PAYBILL: KES paybills ```json theme={null} { "method": "PAYBILL", "account_name": "Nairobi Water", "paybill": "888880", "paybill_account": "254712345678" } ``` Paybill business number, 5–7 digits. The account reference the paybill expects (meter number, invoice, phone, etc.). ## BANK\_TRANSFER: KES banks ```json theme={null} { "method": "BANK_TRANSFER", "account_name": "Acme Supplies Ltd", "account_number": "0102030405060", "bank_code": "68", "bank_name": "Equity Bank" } ``` Bank account number. Kenyan bank code. Display name for the bank. Optional. ## NGN bank transfer NGN recipients have no `method` field. All NGN payouts are bank transfers. ```json theme={null} { "account_name": "Chidi Okafor", "institution": "GTBINGLA", "account_number": "0123456789" } ``` The recipient bank's institution code (`bank_code` is accepted as an alias). For example `GTBINGLA` for GTBank. NUBAN account number. ## Validation Minisend validates every recipient **before creating an order**. An invalid account returns `422` and nothing is created. * **Bank accounts (KES and NGN)** are hard-validated: the account must exist and resolve to a registered name. * **Mobile, till, and paybill lookups** are best-effort: the registered name is returned when available, but a missing name does not block the order. Use [`POST /api/offramp/validate-account`](/api-reference/offramp/validate-account) to check an account and show the registered name on a confirmation screen before creating the order. # Off-ramp webhooks Source: https://docs.minisend.xyz/offramp/webhooks Payload schemas for offramp.completed, offramp.failed, and offramp.expired, delivered to your webhook URL and signed with the same X-Minisend-Signature header. Off-ramp 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 events. [Signature verification](/webhooks/verification) is identical, and failed deliveries retry up to 5 times with exponential backoff. | Event | Fires when | Terminal status | | ------------------- | --------------------------------- | --------------- | | `offramp.completed` | Payout delivered to the recipient | `completed` | | `offramp.failed` | Payout failed after your deposit | `failed` | | `offramp.expired` | No deposit before `expires_at` | `expired` | *** ## offramp.completed ```json theme={null} { "event": "offramp.completed", "order_id": "9b2f6c1e-...", "external_reference": "payout-8412", "status": "completed", "amount_usdc": 10, "amount_local": 1294, "payout_currency": "KES", "exchange_rate": 129.45, "fee": 13, "recipient_account_name": "Jane Wanjiku", "settlement_receipt": "SHQ1234ABC", "completed_at": "2026-07-05T12:07:41Z", "created_at": "2026-07-05T12:00:03Z" } ``` Always `offramp.completed`. Use as an idempotency key. Minisend may redeliver if your server timed out. Your `reference`, if set at creation. The order's USDC amount. Gross local amount at the executed rate. The recipient received `amount_local − fee`. `KES`, `NGN`, `GHS`, or `UGX`. Local currency per 1 USDC, as executed. Minisend fee in local units. `0` for NGN. Payout receipt: an M-Pesa code for KES mobile payouts, an on-chain settlement hash for NGN. ISO 8601. ISO 8601. *** ## offramp.failed The payout could not complete after your deposit. ```json theme={null} { "event": "offramp.failed", "order_id": "9b2f6c1e-...", "external_reference": "payout-8412", "status": "failed", "amount_usdc": 10, "amount_local": 1294, "payout_currency": "KES", "exchange_rate": 129.45, "fee": 13, "recipient_account_name": "Jane Wanjiku", "created_at": "2026-07-05T12:00:03Z" } ``` NGN deposits are refunded automatically to the order's `refund_address`. For KES, GHS, and UGX failures, contact [support](https://t.me/minisendapp) with the `order_id`. Funds are not lost. *** ## offramp.expired No deposit arrived before `expires_at`. Nothing was sent and nothing is owed. Create a new order to retry. ```json theme={null} { "event": "offramp.expired", "order_id": "9b2f6c1e-...", "external_reference": "payout-8412", "status": "expired", "amount_usdc": 10, "payout_currency": "KES", "recipient_account_name": "Jane Wanjiku", "created_at": "2026-07-05T12:00:03Z" } ``` If you sent USDC but the order still expired (e.g. the transfer confirmed after the window, or you never submitted the hash), contact [support](https://t.me/minisendapp) with the `order_id` and transaction hash. *** ## Handling pattern Same as checkout 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('offramp.')) { processOfframpEvent(payload); // dedupe on payload.order_id } else { processCheckoutEvent(payload); // dedupe on payload.session_id } }); ``` # Onramp API Source: https://docs.minisend.xyz/onramp/overview Collect KES from a customer's phone and receive USDC in your own wallet. The reverse of the off-ramp API, non-custodial and provider-agnostic. The onramp API collects money from a customer's phone and delivers USDC to **your own wallet**. It is the mirror of the [off-ramp API](/offramp/overview): off-ramp sends USDC out to a recipient, onramp brings local currency in as USDC. Use it to accept cash-like payments from people who don't hold crypto: top up a wallet balance, fund an account, or collect on behalf of your own customers, without ever touching the currency yourself. ## Non-custodial by design Minisend never holds the USDC. The customer pays with a mobile money prompt on their phone, and the equivalent USDC is released directly to the Base address you specify when creating the order. If a payout cannot be delivered, nothing was collected in the first place, so there is nothing to refund. ## How it works `POST /api/onramp/quote` returns the rate and the fee for a given amount, in either direction. `POST /api/onramp/orders` creates the order and sends a payment prompt to the customer's phone in the same call. The customer confirms the prompt on their phone. No further action from you. Once payment is confirmed, USDC lands at the address you provided. An `onramp.completed` webhook fires immediately, and `onramp.released` follows once the on-chain transfer is confirmed. ## Order lifecycle | Status | Meaning | | ----------- | ------------------------------------------------------------------------ | | `pending` | Payment prompt sent, waiting for the customer to confirm | | `completed` | Payment collected. USDC released to your address | | `failed` | The customer cancelled, the prompt timed out, or funds were insufficient | | `expired` | No payment within the order window | `completed`, `failed`, and `expired` are final. Each order is one-shot: a failed or expired order is never retried automatically. Create a new order if the customer wants to try again. ## Fees and rates A platform fee is always collected on top of the amount you receive. You can quote either direction: | You specify | Customer is charged | You receive | | ------------------------------------------- | ------------------------------------------ | ----------------------------------------------- | | The USDC amount you want to receive | That amount converted to KES, plus the fee | Exactly the USDC amount you asked for | | The exact KES amount to charge the customer | That amount as typed | The KES amount minus the fee, converted to USDC | Every quote returns both figures so you can show the customer the exact prompt amount before creating the order. ## Limits The KES amount charged to the customer must fall between 20 and 250,000 per transaction. There's a second check too: once the fee comes out, what's left (the `net_kes` figure) has to be at least **100 KES**, or the quote is rejected even if the charged total was fine on its own. Both checks happen at quote time. Only KES through M-Pesa is supported right now. ## Getting access Onramp access is enabled per account. Email [info@minisend.xyz](mailto:info@minisend.xyz) to activate it. Once enabled, your existing `ms_live_` API key carries the `onramp` scope and every endpoint in this section works. Calls made before that return a `403` telling you to request access. See [key scopes](/api-reference/authentication#key-scopes). Quote, create an order, and confirm a collection end to end. # Onramp quickstart Source: https://docs.minisend.xyz/onramp/quickstart Collect your first M-Pesa payment and receive USDC in your own wallet in two API calls: quote the amount, then create the order. This guide collects KES 1,000 from a customer's phone and releases USDC to your own Base address. **Before you start, you need:** * An API key with the `onramp` scope. See [getting access](/onramp/overview#getting-access). * A Base address you control to receive the USDC. Specify either the KES amount to charge the customer, or the USDC amount you want to receive. This example charges the customer an exact KES figure. ```bash theme={null} curl -X POST https://merchant.minisend.xyz/api/onramp/quote \ -H "Authorization: Bearer ms_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "currency": "KES", "amount_kes": 1000 }' ``` ```json theme={null} { "currency": "KES", "amount_kes": 1000, "fee_kes": 10, "net_kes": 990, "amount_usdc": 7.62, "rate": 129.92, "expires_at": "2026-07-23T12:05:00.000Z" } ``` `amount_kes` is the exact figure the customer's phone will be prompted to pay. `amount_usdc` is what your address receives. Pass an `Idempotency-Key` header so a network retry can never send a second payment prompt. ```bash theme={null} curl -X POST https://merchant.minisend.xyz/api/onramp/orders \ -H "Authorization: Bearer ms_live_your_key_here" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: collect-2201" \ -d '{ "currency": "KES", "amount_kes": 1000, "phone": "0712345678", "address": "0xYourWalletAddress0000000000000000000000", "reference": "collect-2201" }' ``` ```json theme={null} { "order_id": "7c1e4f9a-...", "status": "pending", "currency": "KES", "amount_usdc": 7.62, "amount_local": 1000, "fee": 10, "rate": 129.92, "customer_phone": "0712345678", "mobile_network": "Safaricom", "release_address": "0xyourwalletaddress0000000000000000000000", "release_chain": "base", "release_asset": "USDC", "external_reference": "collect-2201", "expires_at": "2026-07-23T12:30:00.000Z", "created_at": "2026-07-23T12:00:03.000Z", "instructions": "The customer's phone (0712345678) will receive an M-Pesa prompt for KSh 1,000. On payment, 7.62 USDC (Base) is released to release_address." } ``` The payment prompt is sent to the customer's phone the moment this call succeeds. There is no separate trigger step. The customer enters their PIN on the prompt. Nothing further is required from your side. Each order is one-shot. If the customer cancels or the prompt times out, the order moves to `failed` and cannot be retried. Create a new order. Poll the order, or listen for the webhook: ```bash theme={null} curl https://merchant.minisend.xyz/api/onramp/orders/7c1e4f9a-... \ -H "Authorization: Bearer ms_live_your_key_here" ``` When payment is collected, `status` becomes `completed` and `receipt_number` holds the M-Pesa confirmation code. An `onramp.completed` webhook fires at that moment; `onramp.released` follows once the on-chain transfer is confirmed and carries `release_tx_hash`. See [onramp webhooks](/onramp/webhooks). ## Quoting the other direction If you'd rather guarantee a specific USDC amount and let the KES figure float, pass `amount_usdc` instead of `amount_kes`: ```bash theme={null} curl -X POST https://merchant.minisend.xyz/api/onramp/quote \ -H "Authorization: Bearer ms_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "currency": "KES", "amount_usdc": 10 }' ``` The response's `amount_kes` is then the figure the customer is charged, always your requested `amount_usdc` plus the platform fee. Pass the same field to `POST /api/onramp/orders` in place of `amount_kes`. # Onramp webhooks Source: https://docs.minisend.xyz/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" } ``` Always `onramp.completed`. Use as an idempotency key. Minisend may redeliver if your server timed out. Your `reference`, if set at creation. USDC released, or being released, to `release_address`. The KES amount the customer was charged, including the fee. Minisend fee in KES, included in `amount_local`. KES per 1 USDC, as executed. The M-Pesa confirmation code for the collection. ISO 8601. ISO 8601. *** ## 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" } ``` The Base transaction that delivered the USDC to `release_address`. *** ## 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" } ``` Why the collection failed: cancelled, timed out, or insufficient funds. Nothing was collected, so there is nothing to refund. Create a new order if the customer wants to retry. *** ## 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); } }); ``` # Checkout sessions Source: https://docs.minisend.xyz/payments/checkout-sessions Create Minisend checkout sessions server-side to embed crypto payments into any app. Customers get a hosted page; you receive a webhook on settlement. A checkout session is a payment intent created from your backend. You get a hosted checkout URL, redirect the customer, and receive a webhook on settlement. Use checkout sessions when you have a developer and want payments embedded in your existing app. ## Flow `POST` to `/api/merchant/checkout` with the USDC amount. Receive `checkout_url` + `session_id`. Send them to `checkout_url`. The page shows your business name, the amount, and a deposit address. Any of 19 USDC chains or 14 USDT chains. Same address. USDC on another chain bridges to Base via CCTP. USDT swaps to USDC on Base first, taking 30 to 50 seconds. From there it converts to local currency and pays out. Webhook on `completed`, `failed`, or `expired`. Or poll the status endpoint at any time. Customers paying in KES can also choose **M-Pesa** on the checkout page. The payment prompt includes the platform fee, and the equivalent USDC is credited to your [wallet](/dashboard/wallet) when the payment completes. ## Create a session **`POST https://merchant.minisend.xyz/api/merchant/checkout`** **Auth:** `Authorization: Bearer ms_live_...` ```bash cURL theme={null} curl -X POST https://merchant.minisend.xyz/api/merchant/checkout \ -H "Authorization: Bearer ms_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "amount": 25.00, "description": "Order #4821", "external_id": "order-4821", "customer_email": "customer@example.com" }' ``` ```javascript JavaScript theme={null} const res = await fetch('https://merchant.minisend.xyz/api/merchant/checkout', { method: 'POST', headers: { 'Authorization': 'Bearer ms_live_your_key_here', 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: 25.00, description: 'Order #4821', external_id: 'order-4821', customer_email: 'customer@example.com', }), }); const session = await res.json(); ``` ```python Python theme={null} import requests res = requests.post( 'https://merchant.minisend.xyz/api/merchant/checkout', headers={ 'Authorization': 'Bearer ms_live_your_key_here', 'Content-Type': 'application/json', }, json={ 'amount': 25.00, 'description': 'Order #4821', 'external_id': 'order-4821', 'customer_email': 'customer@example.com', }, ) session = res.json() ``` ### Request fields | Field | Type | Required | Description | | ------------------ | ------ | -------- | ---------------------------------------------------------------------------------------------------------------- | | `amount` | number | Yes | USDC amount (>0). Customer pays this as USDC or USDT-equivalent. | | `description` | string | No | Shown on the checkout page. | | `external_id` | string | No | Your reference (e.g., order ID). Echoed back in webhooks. | | `customer_email` | string | No | For receipt purposes. | | `settlement_mode` | string | No | `fiat` or `usdc`. Overrides your account default for this session only. See [Settlement mode](#settlement-mode). | | `settlement_chain` | string | No | `BASE`, `ARB`, `AVAX`, `OP`, `ETH`, or `MATIC`. Only matters when `settlement_mode` is `usdc`. | ### Response (201) ```json theme={null} { "session_id": "cs_7f8a9b2c-...", "checkout_url": "https://merchant.minisend.xyz/checkout/cs_7f8a9b2c-...", "deposit_address": "0x1234...5678", "amount_usdc": 25.00, "settlement_mode": "fiat", "settlement_chain": null, "expires_at": "2026-04-13T14:30:00Z", "status": "pending" } ``` Sessions expire after **30 minutes**. Create them at the moment of checkout, not earlier. ## Settlement mode Every session settles the way your account is set up in [Settings](/dashboard/settings#settlement-mode), unless you override it with `settlement_mode`. Fiat converts to your payout currency and pays out as usual. USDC skips that and leaves the deposit in your wallet. Once a session is created, its mode is locked in. Changing your account default later won't touch it. If you're settling in USDC, `settlement_chain` picks where the payment lands. Base needs no bridging. Anything else forwards there automatically once the deposit confirms, and `checkout.completed` fires before that forward finishes, so check the session's `forward` field (or listen for [`checkout.forwarded`](/webhooks/events#checkout-forwarded)) before treating the money as landed on its destination chain. A failed forward stays quiet rather than sending a webhook, so poll `forward.status` if you need certainty. Two more endpoints round this out. `GET /api/merchant/settlement` returns your current mode, chain, fee rate, and which chains you can switch to. `GET /api/merchant/balances` returns your USDC balance on every chain you've received on, with `null` meaning a chain's balance couldn't be read right now, not that it's empty. Both use the same API key and `checkout` scope you already have. ## Using external\_id Pass your order ID as `external_id` when you create the session. Minisend echoes it back in every webhook so you can reconcile without maintaining a session-to-order mapping. ```javascript theme={null} // Create body: JSON.stringify({ amount: 49.99, external_id: 'order-8837' }) // Handle webhook if (req.body.event === 'checkout.completed') { markOrderPaid(req.body.external_id, { receipt: req.body.receipt, local_amount: req.body.amount_local, }); } ``` ## Check session status **`GET https://merchant.minisend.xyz/api/merchant/checkout/{session_id}`** — no auth. ```bash theme={null} curl https://merchant.minisend.xyz/api/merchant/checkout/cs_7f8a9b2c-... ``` ```json theme={null} { "session_id": "cs_7f8a9b2c-...", "status": "completed", "amount_usdc": 25.00, "description": "Order #4821", "deposit_address": "0x1234...5678", "expires_at": "2026-04-13T14:30:00Z", "created_at": "2026-04-13T14:00:00Z", "amount_local": 3225.00, "exchange_rate": 129.00, "settlement_receipt": "SHQ1234ABC", "completed_at": "2026-04-13T14:08:22Z", "merchant": { "business_name": "My Store", "logo_url": null } } ``` ### Status values | Status | Meaning | | ------------------ | --------------------------------------- | | `pending` | Waiting for the customer to send | | `deposit_received` | Detected on-chain; settlement initiated | | `settling` | Conversion + payout in progress | | `completed` | Payout delivered | | `failed` | Failed post-deposit — contact support | | `expired` | No deposit within 30 minutes | ## Full example ```javascript theme={null} // 1. Create session const res = await fetch('https://merchant.minisend.xyz/api/merchant/checkout', { method: 'POST', headers: { 'Authorization': 'Bearer ms_live_your_key_here', 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: 25.00, description: 'Order #4821 - 2x T-shirts', external_id: 'order-4821', }), }); const { checkout_url } = await res.json(); // 2. Redirect res.redirect(checkout_url); // 3. Handle webhook app.post('/webhooks/minisend', (req, res) => { const signature = req.headers['x-minisend-signature']; if (!verifyWebhook(req.body, signature, WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } if (req.body.event === 'checkout.completed') { markOrderPaid(req.body.external_id, { receipt: req.body.receipt, local_amount: req.body.amount_local, currency: req.body.currency, }); } res.status(200).send('OK'); }); ``` See [Webhook verification](/webhooks/verification) for the signature check. # Payment links Source: https://docs.minisend.xyz/payments/payment-links Share a URL to collect USDC or USDT payments with no code. Minisend converts every payment to local currency and sends it to your M-Pesa or bank account. A permanent URL tied to your business. Share it anywhere, accept USDC or USDT, receive local currency. ## Your URL ```text theme={null} https://merchant.minisend.xyz/pay/{your-slug} ``` Set your slug in **Settings**. Example: `acme-store` → `https://merchant.minisend.xyz/pay/acme-store`. Once you share a link, avoid changing the slug. Old links will break. ## Setup **Settings** → Payment link section → enter a short, recognisable slug → **Save**. Your full URL appears in **Settings** once the slug is saved. Website, invoice, WhatsApp, social bio, email. Anywhere a URL works. ## How customers pay The page shows your business name, logo, payout currency, and a **live exchange rate preview**. As the customer types a USDC amount, they see the local currency equivalent before confirming. When they tap **Pay**, Minisend creates a checkout session and shows a deposit address. The customer sends USDC (19 chains) or USDT (14 chains) from any wallet. You receive local currency. They see a receipt. Amount range: $0.01 to $**10,000 USDC-equivalent**. Contact support for higher limits. ## What your customer pays The amount the customer enters in crypto is what **you** receive. The platform fee is added to their total automatically, and there is no gross-up for NGN payouts. **Paying with M-Pesa (KES merchants):** the payment page also offers an M-Pesa tab. There the customer pays exactly the KES they type, and the platform fee comes out of your settlement instead; you receive the typed amount minus the fee. Price accordingly, or use a [checkout session](/payments/checkout-sessions), where the customer's M-Pesa prompt includes the fee. ## Where to share The link is permanent. One link, unlimited payments; a fresh session is created on each visit. * **Website**: a "Pay with crypto" button * **Invoices**: paste the URL on PDF or email invoices * **WhatsApp**: send directly to customers * **Social**: bio, pinned posts, story links * **Email**: signature or reply # Supported networks and currencies Source: https://docs.minisend.xyz/payments/supported-currencies Minisend accepts USDC on 19 chains and USDT on 14 chains, then settles to KES, NGN, GHS, or UGX via M-Pesa, mobile money, and bank transfer. ## Stablecoins | Asset | Routing | Once detected | | -------- | -------------------------------------- | ------------------------ | | **USDC** | Native on Base; CCTP from other chains | Seconds to a few minutes | | **USDT** | Swap layer → USDC on Base | **30–50 seconds** | Both paths converge: USDC on Base → fiat → payout. ## USDC networks (19) The same deposit address accepts every chain. Non-Base deposits are bridged automatically. | Network | | ------------------ | | **Base** (primary) | | Arbitrum | | Avalanche | | Codex | | EDGE | | Ethereum | | HyperEVM | | Ink | | Linea | | Monad | | Morph | | Optimism | | Plume | | Polygon | | Sei | | Sonic | | Unichain | | World Chain | | XDC | ## USDT networks (14) USDT inflows are swapped to USDC on Base in 30–50 seconds. | Network | | --------- | | ADI | | Arbitrum | | Aurora | | Avalanche | | Bera | | BNB Chain | | Ethereum | | Gnosis | | Monad | | Optimism | | Plasma | | Polygon | | Scroll | | XLayer | Customers don't pick a chain from a menu. Minisend identifies the inflow and routes it. ### Restricting accepted networks By default your checkout accepts USDC on all 19 chains. In **Wallets**, you can disable individual chains if you'd rather not receive deposits on them — the checkout page then only shows the networks you've accepted. Base always stays enabled. A deposit on a chain you've disabled is never swept or converted; it's flagged for manual review instead of being silently accepted or dropped. ## Payout currencies | Currency | Country | Methods | | -------- | ------- | --------------------------------------- | | **KES** | Kenya | M-Pesa, Buy Goods (till), Paybill, bank | | **NGN** | Nigeria | Bank | | **GHS** | Ghana | Mobile money, bank | | **UGX** | Uganda | Mobile money, bank | Configure once in **Settings**. Same configuration applies to payment links and checkout sessions. ## Fees A platform fee applies to the local currency amount after conversion, the same whether the customer paid in USDC or USDT. See your dashboard for your current rate. # Accept your first payment Source: https://docs.minisend.xyz/quickstart Accept your first USDC or USDT payment and receive local currency settlement in five steps, from sign-up to a working checkout session. Five steps from sign-up to a working checkout. Create an account at [merchant.minisend.xyz](https://merchant.minisend.xyz) with email or Google. You'll land on your merchant dashboard. In **Settings**, set your business name, payout currency (KES, NGN, GHS, or UGX), and payout method (M-Pesa, mobile money, or bank). This is where Minisend sends your local currency after every successful payment. Open **API Keys** → **New Key**. Copy the `ms_live_...` value immediately; it's shown only once. Never expose your API key in frontend code or public repos. Call the checkout endpoint from your backend only. `amount` is in USDC. The customer can pay it as USDC or USDT-equivalent. ```bash cURL theme={null} curl -X POST https://merchant.minisend.xyz/api/merchant/checkout \ -H "Authorization: Bearer ms_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "amount": 25.00, "description": "Order #4821", "external_id": "order-4821" }' ``` ```javascript JavaScript theme={null} const res = await fetch('https://merchant.minisend.xyz/api/merchant/checkout', { method: 'POST', headers: { 'Authorization': 'Bearer ms_live_your_key_here', 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: 25.00, description: 'Order #4821', external_id: 'order-4821', }), }); const session = await res.json(); // Redirect your customer to session.checkout_url ``` Response (`201`): ```json theme={null} { "session_id": "cs_7f8a9b2c-...", "checkout_url": "https://merchant.minisend.xyz/checkout/cs_7f8a9b2c-...", "deposit_address": "0x1234...5678", "amount_usdc": 25.00, "expires_at": "2026-04-13T14:30:00Z", "status": "pending" } ``` Sessions expire after **30 minutes** with no deposit. Send the customer to `checkout_url`. They can pay in USDC on 19 chains, USDT on 14 chains, or M-Pesa (KES). When settlement completes, Minisend POSTs to your configured webhook URL: ```json theme={null} { "event": "checkout.completed", "session_id": "cs_7f8a9b2c-...", "amount_usdc": 25.00, "amount_local": 3225.00, "currency": "KES", "exchange_rate": 129.00, "receipt": "SHQ1234ABC", "status": "completed", "external_id": "order-4821", "completed_at": "2026-04-13T14:08:22Z", "created_at": "2026-04-13T14:00:00Z" } ``` Or poll the [session status endpoint](/api-reference/get-checkout), which needs no authentication. # Wallet API Source: https://docs.minisend.xyz/wallet-api/overview Give every user of your product a real on-chain address, without building or running wallet infrastructure yourself. The Wallet API lets you give every user of your own product a dedicated on-chain address. You call one endpoint with a reference for your user, and get back a real address on the chain you choose. Use it to add deposit addresses to your app, issue a wallet per customer or per order, or build on-chain functionality without becoming a wallet provider yourself. ## No keys to manage Creating an address through this API doesn't hand you or your end users a private key or seed phrase to store. There's nothing to generate, back up, or lose on your side of the integration. ## Master wallets and addresses Every address you generate belongs to a **master wallet**: one container you activate per chain. Activate Base (or another chain) from your dashboard, and every subsequent address on that chain is issued under it. From **Dashboard → Wallets**, activate the chain you want to issue addresses on. Base is available on every plan. Create a key from the same page. Keys use the `wsk_live_` prefix. `POST /api/v1/wallets` with a reference for your user. Calling it again with the same reference returns the same address; it never mints a second one. ## Detecting deposits Every address you create is watched. When one receives funds, Minisend records the deposit and delivers a [`wallet.deposit.received` webhook](/wallet-api/webhooks) to the same webhook URL configured in **Settings**. You can also read deposits directly, and check a wallet's live balance, without waiting on a webhook: A wallet's current USDC balance, read live from the chain. Deposit history for one wallet. Every deposit across your account, for reconciliation. There's no transfer or sweep capability yet: funds are detected and reported, not moved. See [Wallet API webhooks](/wallet-api/webhooks) for the event and its known chain caveat. ## Pricing | Plan | Price | Addresses included | Extra addresses | Chains active | Tokens | | ---------- | ---------- | ------------------ | --------------- | ------------- | ---------- | | Free | \$0 | 100/month | Blocked | 1 (Base only) | USDC, USDT | | Growth | \$199/mo | 2,000/month | \$0.50 each | Any 3 of 6 | USDC, USDT | | Enterprise | Contracted | Unlimited | n/a | All 6 | USDC, USDT | The address allowance resets every calendar month and counts addresses **created** that month, not your running total. On Growth, going over 2,000 in a month never blocks you: extra addresses are billed at \$0.50 each rather than rejected. Free has no billing behind it, so it blocks once the 100 included addresses are used. "Any 3 of 6" is a count, not a fixed set: pick which three of the six supported chains (Base, Ethereum, Polygon, Arbitrum, Optimism, Avalanche) are active, and swap one out for another later. Creating an address on a chain you haven't activated, or activating a fourth chain on Growth, returns a `400` naming the limit. [Contact Minisend](https://t.me/minisendapp) to choose a plan or move to Enterprise. ## No test mode There is no sandbox for the Wallet API today. Keys are live-only, and every address you create is a real address on mainnet. Build against small real amounts, and use a throwaway `walletRef` prefix if you want your experiments easy to identify later. If a sandbox would block your integration, [tell us](https://t.me/minisendapp) — it helps us prioritise. Activate a chain, generate a key, and issue an address end to end. # Wallet API quickstart Source: https://docs.minisend.xyz/wallet-api/quickstart Activate a chain, generate a key, and issue your first user address in two API calls. This guide issues one address on Base for a user in your own product. Open **Dashboard → Wallets** and activate Base. This only needs to happen once per chain — every address you create afterward on that chain is issued under it. On the same page, create a new API key. It's shown once, in a copy-once banner: ```text theme={null} wsk_live_a1b2c3d4e5f6... ``` Store it as a backend environment variable. Call the create-wallet endpoint with a reference for your user, any string you'll recognize later (your own user ID works well). ```bash theme={null} curl -X POST https://merchant.minisend.xyz/api/v1/wallets \ -H "Authorization: Bearer wsk_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{"walletRef": "user-9214"}' ``` ```json theme={null} { "wallet": { "id": "3f6b2e1a-...", "wallet_ref": "user-9214", "address": "0xabc1230000000000000000000000000000dead", "chain": "BASE", "status": "active", "mode": "live", "metadata": null, "created_at": "2026-07-29T09:00:00.000Z" } } ``` Store `address` against your user. It's the address they can send to or hold funds at. Calling the same request again, with the same `walletRef`, returns the same wallet instead of creating a second one — safe to call from a signup flow without checking first whether the user already has an address. ## Issuing on another chain Once a chain is activated, pass it explicitly: ```bash theme={null} curl -X POST https://merchant.minisend.xyz/api/v1/wallets \ -H "Authorization: Bearer wsk_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{"walletRef": "user-9214", "chain": "ARB"}' ``` A `walletRef` can have one address per chain — creating it on Base and then on Arbitrum gives your user two independent addresses, not one shared across chains. Attaching `metadata` (any JSON object) to a wallet at creation is useful for storing your own context — an order ID, a plan tier, anything you'll want back later when you fetch the wallet. # Wallet API webhooks Source: https://docs.minisend.xyz/wallet-api/webhooks Get notified the moment a wallet you created receives funds, with wallet.deposit.received. Minisend detects deposits to every address you create and notifies you. Configure a webhook URL once in **Settings** and it's shared across checkout, off-ramp, onramp, and the Wallet API. ## wallet.deposit.received Fires once for every deposit to a wallet you created, signed the same way as every other Minisend webhook: HMAC-SHA256 in `X-Minisend-Signature`, using 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" } ``` Use as an idempotency key. A late finality update redelivers the same `deposit_id`, not a new one. Your own identifier for the wallet, exactly as you passed it when you created the address. The chain the funds actually landed on. Addresses from this API are identical across every EVM chain, so a wallet you created on Base can still receive on Polygon, Arbitrum, or anywhere else supported. Always read this field instead of assuming it matches the chain you issued the wallet on. A string, not a number. Compare and store it as a decimal in your own system rather than parsing it as a float. The on-chain transaction. May briefly be `null` on the very first notification for a deposit. `confirmed` or `complete`. You may receive `confirmed` first and `complete` moments later for the same `deposit_id` — both are safe to treat as "funds have arrived." Deposits are also readable over the API without 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) — useful for catching up after downtime, or if you'd rather poll. ## What this doesn't cover yet The Wallet API detects and reports incoming funds. It does not yet move them: there is no transfer or sweep endpoint, so funds stay at the address until that capability ships. Plan your integration around reading balances and deposits, not withdrawing programmatically. # Webhook events Source: https://docs.minisend.xyz/webhooks/events Full payload schemas for checkout.completed, checkout.failed, checkout.expired, and checkout.forwarded, 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.forwarded` | USDC bridged to your chosen chain | session was already `completed` | *** ## 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_expected_usdc": 25.00, "amount_received_usdc": 25.00, "amount_matched": true, "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" } ``` Always `checkout.completed`. Use as an idempotency key. Your reference, if set. The amount the session asked for. **Not necessarily the amount that arrived** — see `amount_received_usdc`. Same value as `amount_usdc`, named explicitly so the pair reads unambiguously alongside `amount_received_usdc`. What actually landed on-chain. **Reconcile on this field, not `amount_usdc`.** A customer who sends slightly more or less than the session asked for is still settled, for the amount they actually sent — the alternative would strand their funds. So a `completed` session can carry an `amount_usdc` that was never received. Absent when no deposit was attributed. `false` when the received amount fell outside ±\$0.01 of the expected one. Treat `false` as needing review: the payout is real, but it isn't the figure you quoted. Absent when no deposit was attributed. Net local currency after the platform fee. Derived from the amount actually received. `KES`, `NGN`, `GHS`, or `UGX`. Local currency per 1 USDC at settlement. Payout provider receipt. Always `completed`. ISO 8601. ISO 8601. *** ## 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" } ``` Always `checkout.failed`. The amount the session asked for. `amount_received_usdc` carries what was actually deposited, when a deposit was attributed before the failure. Always `failed`. Contact [support](https://t.me/minisendapp) with the `session_id`. Customer funds are not lost. *** ## 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" } ``` Always `checkout.expired`. Use this to mark the corresponding order as abandoned. The amount the customer never sent. Always `expired`. Expired sessions cost nothing. The platform fee is only charged on completed payments. *** ## checkout.forwarded Only fires for USDC sessions settling off Base. The bridge finished, and the USDC landed on the chain you picked. ```json theme={null} { "event": "checkout.forwarded", "session_id": "cs_7f8a9b2c-...", "external_id": "order-4821", "amount_usdc": 25.00, "settlement_chain": "ARB", "forward_tx_hash": "0x9f2c...", "status": "completed", "completed_at": "2026-04-13T14:08:22Z", "created_at": "2026-04-13T14:00:00Z" } ``` Always `checkout.forwarded`. Where the USDC landed. The destination-chain transaction. A failed bridge sends nothing. Your USDC stays safe on Base either way, so poll `forward.status` on the [checkout session](/api-reference/get-checkout) if you need to know for sure. *** ## 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 } ``` ### Reconcile on what arrived, not what you asked for `amount_usdc` is the figure the session was created with. If you book revenue from it, an underpayment records money you never received. ```javascript theme={null} if (payload.event === 'checkout.completed') { const received = payload.amount_received_usdc ?? payload.amount_usdc; await ledger.record(payload.session_id, received); if (payload.amount_matched === false) { await ops.flagForReview(payload.session_id, { expected: payload.amount_expected_usdc, received: payload.amount_received_usdc, }); } } ``` ### 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); } ``` # Webhooks Source: https://docs.minisend.xyz/webhooks/overview Minisend POSTs a signed request to your server when a session completes, fails, or expires. Configure your webhook URL, verify signatures, and understand retries. Minisend POSTs to your server when a session reaches a terminal state: `completed`, `failed`, or `expired`. [Off-ramp orders](/offramp/overview) emit `offramp.*` events and [onramp orders](/onramp/overview) emit `onramp.*` events, both to the same URL with the same signature and retry behavior. Payloads are documented in [off-ramp webhooks](/offramp/webhooks) and [onramp webhooks](/onramp/webhooks). ## Setup **Settings** → **Webhook URL** → paste your HTTPS endpoint (e.g., `https://yourbusiness.com/webhooks/minisend`). Minisend starts delivering events. Your `webhook_secret` is shown in **Settings**. Use it to verify incoming signatures. Always verify `X-Minisend-Signature` before processing. See [Verification](/webhooks/verification). ## Request format | Header | Value | | ---------------------- | ------------------------------------ | | `Content-Type` | `application/json` | | `X-Minisend-Signature` | HMAC-SHA256 hex of the raw JSON body | | `User-Agent` | `Minisend-Webhooks/1.0` | Your endpoint must return a `2xx` within **10 seconds**. ## Sample payload ```json theme={null} { "event": "checkout.completed", "session_id": "cs_7f8a9b2c-...", "external_id": "order-4821", "amount_usdc": 25.00, "amount_expected_usdc": 25.00, "amount_received_usdc": 25.00, "amount_matched": true, "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" } ``` ### Fields `checkout.completed`, `checkout.failed`, or `checkout.expired`. Use as an idempotency key. Your reference, if set. USDC-equivalent amount the session asked for. Not necessarily what arrived — reconcile on `amount_received_usdc`. Same value as `amount_usdc`, named explicitly to pair with `amount_received_usdc`. What actually landed on-chain. Absent when no deposit was attributed (`expired`). See [Events](/webhooks/events) for why this can differ from `amount_usdc`. `false` when the received amount fell outside ±\$0.01 of the expected one. Absent when no deposit was attributed. Net local currency after the platform fee. `completed` only. Your payout currency: `KES`, `NGN`, `GHS`, or `UGX`. Always present. Local currency per 1 USDC at settlement. `completed` only. Settlement receipt (e.g., M-Pesa code). `completed` only. `completed`, `failed`, or `expired`. ISO 8601. `completed` only. ISO 8601, when the session was created. ## Retry policy Non-`2xx` or no response in 10s = retry. Up to **5 attempts** with exponential backoff: | Attempt | Delay after previous | | ------- | -------------------- | | 1 | Immediate | | 2 | 30 seconds | | 3 | 2 minutes | | 4 | 8 minutes | | 5 | 32 minutes | Total window ≈ 42 minutes. Every delivery attempt is logged. Contact support with a `session_id` to inspect the log. # Verify webhook signatures Source: https://docs.minisend.xyz/webhooks/verification Use HMAC-SHA256 and the X-Minisend-Signature header to confirm webhook requests came from Minisend. Includes examples in JavaScript, Python, and PHP. Each request includes `X-Minisend-Signature`: an HMAC-SHA256 hex of the raw JSON body, keyed with your `webhook_secret`. Verify it before processing. Find your `webhook_secret` in **Settings**. Treat it like a password. Use a timing-safe comparison. `===` is vulnerable to timing attacks. ## Sign over the raw body The signature covers the **exact bytes Minisend sent**. Re-stringifying the parsed body (`JSON.stringify(req.body)`) can produce different bytes and break verification. Capture the raw body before parsing. In Express, use `express.raw({ type: 'application/json' })` for the webhook route. ## Verify ```javascript JavaScript theme={null} const crypto = require('crypto'); function verifyWebhook(rawBody, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(rawBody, 'utf8') .digest('hex'); const a = Buffer.from(signature, 'hex'); const b = Buffer.from(expected, 'hex'); if (a.length !== b.length) return false; return crypto.timingSafeEqual(a, b); } ``` ```python Python theme={null} import hmac, hashlib def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool: expected = hmac.new( secret.encode('utf-8'), raw_body, hashlib.sha256, ).hexdigest() return hmac.compare_digest(signature, expected) ``` ```php PHP theme={null} ## Full Express handler ```javascript theme={null} const express = require('express'); const crypto = require('crypto'); const app = express(); // Raw body required for signature verification app.use( '/webhooks/minisend', express.raw({ type: 'application/json' }), ); function verifyWebhook(rawBody, signature, secret) { const expected = crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex'); const a = Buffer.from(signature, 'hex'); const b = Buffer.from(expected, 'hex'); if (a.length !== b.length) return false; return crypto.timingSafeEqual(a, b); } app.post('/webhooks/minisend', (req, res) => { const signature = req.headers['x-minisend-signature']; if (!signature) return res.status(401).json({ error: 'Missing signature' }); if (!verifyWebhook(req.body, signature, process.env.MINISEND_WEBHOOK_SECRET)) { return res.status(401).json({ error: 'Invalid signature' }); } const payload = JSON.parse(req.body.toString('utf8')); // Acknowledge immediately, then process res.status(200).send('OK'); switch (payload.event) { case 'checkout.completed': markOrderPaid(payload.external_id, { session_id: payload.session_id, receipt: payload.receipt, amount_local: payload.amount_local, currency: payload.currency, }); break; case 'checkout.failed': handleFailedPayment(payload.external_id, payload.session_id); break; case 'checkout.expired': handleExpiredSession(payload.external_id, payload.session_id); break; } }); app.listen(3000); ``` Return `2xx` before running business logic. Anything taking >10s triggers a retry.