Add stablecoin payins and payouts to an iOS or Android app: keep API keys on your server, onboard users in-app, show live quotes, and push webhook updates.
To add blockchain payments to a mobile fintech app, put your own backend between the app and the payments API. The server holds the API key, creates customers, requests quotes, and executes payins and payouts. The app collects input, shows a live quote with a countdown, and receives push notifications that your server sends after verifying each webhook.
None of this needs the user to understand a blockchain. The blockchain payments vs crypto payments guide covers why: the stablecoin is the settlement layer, and the user sees dollars, reais, or pesos.
Key takeaways
expires_at, and re-quote rather than retry.Three parts: the app, your backend, and the payments API. The app never talks to the payments API directly.
| Component | Runs where | Holds | Does |
|---|---|---|---|
| Mobile app | User's phone | Your own session token | Collects input, shows quotes and statuses |
| Your backend | Your servers | Payments API key, webhook secret | Calls the API, stores state, sends pushes |
| Payments API | Provider | Customers, quotes, payouts | Converts, settles, pays out |
| Push service | Apple or Google | Device tokens | Delivers notifications |
Definition: A backend for frontend is a thin server layer that exists only to serve one app. It authenticates your user, calls third-party APIs with secrets the app must not hold, and returns only the fields a screen needs.
This is the same shape you would use for any card or bank API. The blockchain part sits behind the payments API, so the app code looks like any other fintech app.
Because anything inside an app bundle can be read by someone who downloads it. BlindPay's API key docs say it directly: never expose API keys in client-side code or mobile apps.
The stakes are high for payments. A BlindPay key carries full read and write access to its instance, so a leaked key could create payouts. OWASP ranks improper credential usage as M1 in its 2024 Mobile Top 10, and hardcoded credentials are the first example it gives.
On the server side, two extra controls help. Restrict each key to your servers' IPv4 addresses with ip_whitelist, and keep separate keys for your development and production instances. BlindPay also ships official SDKs for Node.js, Python, Go, PHP, and Swift; whichever one you use, run it where the key is safe.
The user accepts terms of service on a hosted page, then your server creates the customer with the KYC data your app collected. Two screens of your own, one hosted page.
redirect_url you control. On mobile, a verified link such as an iOS universal link or an Android App Link can send the user straight back into your app.tos_id query parameter. Send it to your server.tos_id and the identity fields from your screens.customer.update webhook tells your server when the status changes.Design the waiting state. A user who finishes onboarding at 11pm and gets a manual review needs a screen that says what happens next, and a push when it clears.
Lead with the amount the recipient gets, then fees, then a countdown. Users read the top number and the timer, so put the firm numbers there.
BlindPay payout quotes expire 5 minutes after creation and return expires_at as epoch milliseconds, per the payout quotes docs. Start the countdown from that value, not from when the phone received the response, since a slow network can eat 10 seconds. When the timer hits zero, disable the confirm button and fetch a new quote.
| Screen element | Source field | Note |
|---|---|---|
| Recipient gets | receiver_amount | Minor units, so 10000 means 100.00 |
| You send | sender_amount | Includes fees if the sender covers them |
| Countdown | expires_at | Epoch milliseconds |
| Fee lines | flat_fee, partner_fee_amount, billing_fee_amount | Show each one, not only a total |
| Rate | blindpay_quotation | The rate net of BlindPay's fee |
Two quote settings change the copy. currency_type: receiver fixes the amount that lands, which suits "pay exactly 500 reais". cover_fees: true adds fees on top of what the sender pays. The quote screen guide covers fee disclosure, and handling expired quotes has the re-quote logic in TypeScript.
With an idempotency key per user action, reused on every retry. Phones switch between Wi-Fi and cellular mid-request all the time, and a timeout does not tell you whether the payout was created.
Generate the key when the user taps confirm, send it to your server, and have the server pass it as the Idempotency-Key header. BlindPay replays the original response for the same key and an identical body, for 24 hours, per its idempotency docs. The body check is byte for byte, so your server must store and resend the exact body, not rebuild it.
| Situation | What the server does |
|---|---|
| Request timed out | Retry with the same key and same bytes |
409 idempotency_key_in_flight | Wait for the Retry-After seconds, then retry |
422 idempotency_key_payload_mismatch | Bug: the body changed. Stop and log |
| User taps confirm again after a new quote | New action, new key |
The idempotency keys guide goes deeper on key design.
No. Most mobile fintech users never see one. Bank transfers in and local bank payouts out cover the common flows, and the stablecoin settles between them.
If your app holds stablecoin balances, you pick one of two models. A wallet the user controls means the app asks for an on-chain authorization per payout: an ERC-20 approve on EVM chains, a signed transaction on Stellar, or a token delegation on Solana. A BlindPay-managed wallet (beta) needs no signing on the phone, but it is custodial, so read the custodial vs non-custodial wallet comparison before choosing.
For user-controlled EVM wallets, register the address with a signed message rather than a pasted address. BlindPay recovers the address from the signature, so a typo cannot send funds to the wrong place.
Your server receives the webhook, verifies it, stores the status, and then sends the push. The phone never receives the raw webhook.
Verify the svix-id, svix-timestamp, and svix-signature headers against the raw body before trusting a payload, and use svix-id to drop duplicate deliveries, per the webhook verification docs. Then map events to messages:
| Event | Push copy example |
|---|---|
customer.update | "Your account is verified" (or "We need one more document") |
virtualAccount.complete | "Your US account details are ready" |
payin.complete | "Your deposit arrived" |
payout.new | "Your transfer is on its way" |
payout.update | Read status: on hold, failed, or in progress |
payout.complete | Read status: completed or refunded |
Keep pushes short and let the app fetch details from your server. Lock screens show the first line to anyone nearby, so leave amounts and names out of the notification text. Payout statuses explained lists what each status means for the user.
Look for a server-side API with clear mobile guidance, not a mobile SDK that wants your key on the phone. A short checklist:
BlindPay covers each of these. Development instances auto-approve KYC, use USDB on testnets, and complete payouts automatically. A payout quote with request_amount of 66600 forces a failed payout and 77700 forces a refund, so you can test every push and screen before launch. The blockchain payment API checklist has the full evaluation list.
Sketch your three-box architecture, then wire one flow end to end on a development instance: terms, customer, quote, payout, push. Start with the BlindPay payout quickstart and run it from your backend, not the app.
Seven places blockchain payments beat bank rails: contractor payroll, remittances, B2B suppliers, marketplaces, 24/7 treasury, bill pay, and PSP payouts.
Push means the payer sends an ACH transfer. Pull means you debit a connected account. How each funds a USD to USDC on-ramp, the fees, and the return risk.
AP2, ACP, and x402 each verify that an AI agent had permission to spend. Here is what every protocol covers, who backs it, and the reconciliation gap none of them close.