MisarMisar Docs
MisarMailMisarBlogMisarReachMisarPostMisarSEOMisarDevMisarCoderMisar PlatformMisar SSO
Wallet

Withdraw (Cash out)

Stripe Connect withdrawal — pay out earned wallet balance to a user's bank. Connect onboarding, POST /io/wallet/withdraw, and withdrawal history. 1 credit = $1.

Withdrawal lets a user cash out the eligible part of their wallet balance to their own bank account via Stripe Connect Express. It is available from any Misar product through the central wallet API.

POST https://api.misar.io/io/wallet/connect/onboard
GET  https://api.misar.io/io/wallet/connect/status
POST https://api.misar.io/io/wallet/withdraw
GET  https://api.misar.io/io/wallet/withdrawals

Only EARNED balance is withdrawable

The wallet balance is mostly funded by card top-ups. Allowing those to be cashed out to a bank would enable card-refund/chargeback arbitrage. So a separate withdrawable balance tracks only genuinely earned income (affiliate/referral payouts, creator earnings, received transfers, manual grants) credited via the internal credit_withdrawable_balance RPC. Top-ups, subscription bonuses and migration adjustments never increase it. A withdrawal can never exceed min(withdrawable, balance), and spending clamps the withdrawable figure down — so earned credits, once spent, can't be re-withdrawn after a later top-up.

Policy defaults

PolicyDefault
Minimum withdrawal$10
Maximum per withdrawal$100,000
KYCStripe Connect Express onboarding; payoutsEnabled must be true
Concurrent open requests3
Rolling-24h requested volume$10,000
Eligible balanceEarned only — min(withdrawable, balance)

Authentication

SSO bearer (own wallet) or service key (+ user_id), same as the rest of the wallet API.

Authorization: Bearer <sso_access_token>
# — or —
x-wallet-service-key: <WALLET_SERVICE_KEY>   # with body.user_id / ?user_id=<uuid>

1. Connect onboarding — POST /connect/onboard

Creates (first time) or resumes a Stripe Connect Express account and returns a hosted onboarding link. returnUrl must be an absolute *.misar.io URL.

curl -X POST "https://api.misar.io/io/wallet/connect/onboard" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "content-type: application/json" \
  -d '{ "returnUrl": "https://reach.misar.io/wallet" }'
{ "url": "https://connect.stripe.com/setup/e/acct_.../...", "accountId": "acct_..." }

Redirect the user to url. Identity/bank verification is handled entirely inside Stripe. When Stripe clears the account it fires an account.updated webhook that flips payoutsEnabled on — withdrawals are blocked until then.

2. Status — GET /connect/status

One call returns balances, the cash-out-eligible amount, and Connect state.

{
  "balance": 150,
  "withdrawable": 100,
  "effectiveWithdrawable": 100,
  "minWithdrawalDollars": 10,
  "currency": "usd",
  "connectAccountId": "acct_...",
  "hasConnectAccount": true,
  "detailsSubmitted": true,
  "payoutsEnabled": true,
  "connectStatus": "complete",
  "pendingWithdrawals": 0,
  "totalWithdrawn": 40
}

3. Withdraw — POST /withdraw

curl -X POST "https://api.misar.io/io/wallet/withdraw" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "content-type: application/json" \
  -d '{ "amountDollars": 30, "idempotency_key": "wd-2026-07-31-abc" }'
FieldTypeNotes
amountDollarsnumber1 credit = $1. Whole cents; min $10.
idempotency_keystring?Strongly recommended — a retried request with the same key never double-debits.
user_idstring?Required only with a service key.
{ "status": "processing", "requestId": "…", "amountDollars": 30, "balanceAfter": 120 }

How it settles

  1. The wallet atomically validates policy and debits the ledger first (idempotent on idempotency_key) — money is reserved before it moves.
  2. A Stripe Transfer moves funds platform → the user's connected account (idempotent on the request id). If this fails, the debit is reversed and the balance restored — you get 502 and the user is not charged.
  3. An explicit Payout (connected account → bank) captures a po_ id. If only the payout fails, funds are safe in the user's Connect balance and the request stays processing.
  4. payout.paid / payout.failed / payout.canceled webhooks move the request to its terminal status. A bank-payout failure does not reverse the ledger — the funds remain in the user's Stripe balance to retry.

This ordering makes a double-payout impossible and a negative balance impossible.

Rejections

4xx responses carry a machine-readable reason:

reasonHTTPMeaning
no_connect_account409No payout account — run onboarding first.
kyc_required409payoutsEnabled is false — finish Stripe onboarding.
below_minimum400Below the $10 minimum.
insufficient_withdrawable400Exceeds the earned, cash-out-eligible balance.
too_many_pending429Too many withdrawals already in progress.
daily_cap_exceeded429Rolling-24h withdrawal cap reached.

4. History — GET /withdrawals

Cursor-paginated (same shape as transactions).

{
  "items": [
    {
      "id": "…", "amount_credits": 30, "amount_cents": 3000, "status": "paid",
      "stripe_transfer_id": "tr_…", "stripe_payout_id": "po_…",
      "failure_reason": null, "created_at": "…", "completed_at": "…"
    }
  ],
  "nextCursor": null
}

SDK

import { createWalletClient } from "@misar/wallet"; // ≥ 1.2.0

const wallet = createWalletClient({
  baseUrl: "https://api.misar.io/io/wallet",
  serviceKey: process.env.WALLET_SERVICE_KEY,
});

const summary = await wallet.getWithdrawalSummary(userId);
if (!summary.payoutsEnabled) {
  const { url } = await wallet.startConnectOnboarding({ userId, returnUrl });
  // redirect the user to `url`
} else {
  await wallet.withdraw({ userId, amountDollars: 30, idempotencyKey: "wd-…" });
}
const history = await wallet.listWithdrawals(userId);