Designing a neobank's Add money and Withdraw flows: screens, states, and messages

The screens, statuses, and error messages behind a neobank's Add money and Withdraw buttons, built on on-ramp and off-ramp APIs, with a provider check.

A neobank's Add money and Withdraw flows are on-ramp and off-ramp calls dressed as banking screens. With BlindPay, Add money is a virtual account or a payin quote, and Withdraw is a bank account, a locked payout quote, and a payout. Design a screen for every status the API returns, and confirm only on locked numbers.

An on-ramp turns bank money into a stablecoin, a token that tracks a currency like the dollar. An off-ramp turns it back into money in a bank account. Your users don't need either word. This guide maps both flows screen by screen, with the states and messages each one needs. The product context lives in how to build a neobank.

Key takeaways

  • Add money has two shapes: permanent account details, or a one-time payment instruction from a quote.
  • Withdraw is always three API steps: bank account, quote, payout. The quote locks the rate and fees for 5 minutes on BlindPay.
  • Each payin and payout status needs its own screen copy. "Processing" for everything creates support tickets.
  • Failure copy must say where the money goes. Refund timing differs by rail.
  • Pick a provider on ten checks, and test the failure paths yourself.

What does the Add money flow look like?

Two patterns. Permanent account details for repeat deposits, and a one-time instruction for local-currency top-ups.

PatternWhen to use itWhat the user seesBlindPay object
Account detailsSalaries, client payments, repeat deposits in USDRouting and account number in their own nameUS virtual account
One-time instructionTop-ups in BRL, MXN, COP, or ARSA Pix code, a CLABE, a PSE link, or a Transfers 3.0 accountPayin quote, then payin
Pull from linked bankUS users with a linked account"Add $200 from Chase" style buttonACH pull from a Plaid-connected account (enabled on request)

For account details, the build is in give every customer US account details. For the one-time pattern:

  1. User picks a currency and enters an amount.
  2. Your backend creates a payin quote. Show the amount they'll receive in their balance and the fee.
  3. User confirms. Your backend creates the payin from the quote within its window (5 minutes; BRL OTC quotes last 10 seconds).
  4. Show the payment instruction: the Pix copy-and-paste code, the CLABE for SPEI, the PSE link, or the Transfers 3.0 account.
  5. Show a waiting screen. For Pix, SPEI, PSE, and Transfers 3.0, BlindPay waits up to 30 minutes for the money before marking the payin failed.
  6. Credit pending, then available, from the payin webhooks, as in the deposit-to-balance ledger.

For SPEI, show the CLABE in groups and with a copy button. Banxico's SPEI page tells payers they need the 18-digit CLABE and that banks often make them register a new recipient first. Warn first-time payers about that delay.

What does the Withdraw flow look like?

Five screens, in this order. Each maps to one BlindPay call or webhook.

  1. Choose a bank account. List saved accounts, or add one. Adding is once per recipient.
  2. Enter the amount. Let the user type either what they send or what the recipient gets.
  3. Review. Your backend creates a payout quote. Show rate, fees, the amount that lands, the arrival estimate, and a countdown to expires_at.
  4. Processing. The user confirms and your backend executes the payout. payout.new fires once the funds are captured.
  5. Receipt. Show the final amount and a reference the recipient's bank can trace.

The review screen carries the most risk. How to show a cross-border quote covers the disclosure rules in detail. In short: show the rate, every fee, and the total, and don't paint an estimate as a promise. For US consumer remittances, the Remittance Transfer Rule disclosures require the exchange rate rounded to between two and four decimal places, among other items.

How should the countdown behave?

Show it, warn before it ends, and re-quote on expiry. A silent expiry turns into a failed confirm tap.

WCAG's timing guidance asks that users be warned before a time limit ends and given at least 20 seconds to extend it with a simple action. "Refresh quote" is that action. Never extend by executing on stale numbers.

Illustrative example. Numbers are invented. A user withdraws $500 to a Mexican account.

MomentScreen shows
Quote createdRate 18.40 MXN per USD, fee $3.00, recipient gets MX$ 9,144.80, 5:00 left
4:40 elapsed"Your rate expires in 20 seconds. Refresh to keep going."
Expired, user taps RefreshNew rate 18.36, recipient gets MX$ 9,124.92. Old amount shown struck through
User confirmsPayout created from the new quote

Showing both amounts after a refresh stops the "you changed my rate" ticket before it starts.

Which states and messages does each flow need?

One message per status. Map them once and reuse them in the app, push notifications, and email.

FlowBlindPay statusMessagePush?
Add moneyprocessing"Waiting for your deposit"No
Add moneyon_hold"We're reviewing this deposit"Yes
Add moneycompleted"$X added to your balance"Yes
Add moneyfailed"We didn't receive this deposit"Yes
Add moneyrefunded"This deposit was returned to the sender"Yes
Withdrawprocessing"Sending to your bank"No
Withdrawon_hold"Your transfer is under review"Yes
Withdrawcompleted"Delivered to account ending 1234"Yes
Withdrawfailed"Your transfer didn't go through. Here's what happens to your money"Yes
Withdrawrefunded"Your money is back in your balance"Yes

Failure copy must match how refunds work. On BlindPay, if a payout cannot be delivered after the stablecoins are collected, they are refunded to the quote's refund wallet, or to the sending wallet when none is set. On Pix and other non-USD local rails the refund is automatic. USD payouts, and payouts whose review times out, are refunded after an operator review. Say "usually within" only for the automatic case. Payout statuses explained has the full lifecycle.

Common mistakes

  • Executing on an expired quote. Re-quote and reconfirm.
  • One "Processing" message for every state. Holds and refunds look like bugs.
  • Crediting Add money on the first webhook. Pending first, available on completion.
  • Hiding the fee inside the rate. Show the fee line, even when you pass it through.
  • Promising instant on every rail. BlindPay documents Pix and SPEI payins settling within minutes, while ACH and wire can take up to 5 business days.
  • Asking for bank details at every withdrawal. Save accounts once per recipient.
  • No tracking reference on the receipt. Support needs it. Tracking a payout lists the reference per rail.

How do you choose an on-ramp and off-ramp provider?

Score ten things. Each one shows up on a screen above.

#CriterionScreen it affects
1Quotes lock rate and fees until a stated expiryReview
2Fee breakdown returned before confirmReview
3Named payout rails per countryChoose bank account
4Payin methods per country (account details, Pix, SPEI, pull)Add money
5Clear status model with signed webhooksProcessing, notifications
6Documented refund behavior per railFailure screens
7KYC and KYB in the same APIOnboarding
8Published limits and minimumsAmount entry
9Custody model you can explain to usersBalance, failure copy
10A test environment that can force failed and refunded outcomesAll of them, before launch

The 10-point liquidity scorecard adds depth tests, and where neobanks hold customer funds covers criterion 9.

How BlindPay fits

BlindPay covers both buttons with one API. Add money runs through US virtual accounts in each customer's own name, or through payin quotes that return a Pix code, a CLABE, a PSE link, or a Transfers 3.0 account, with ACH pull from a Plaid-connected account when enabled. Each deposit becomes a payin that settles to the customer's wallet as USDC or USDT.

Withdraw is a saved bank account, a payout quote that locks the rate and fees for 5 minutes, and a payout over Pix, SPEI, ACH, RTP, wire, ACH Colombia, Transfers 3.0, SEPA, or SWIFT (POBO/COBO). Payouts pull the quoted amount at execution, so there is no balance to pre-fund. Statuses map one to one onto the screens above, and Svix-signed webhooks drive your notifications.

KYC, KYB, and sanctions screening run inside the same flows. On a development instance, sentinel amounts force failed and refunded outcomes so you can see every screen before launch.

What to do next

List your screens against the status table above and mark any status with no message. Then run one payin and one payout on a development instance, using the payout quotes docs and the payins docs.

FAQ