Skip to main content

Withdraw (GBP / EUR back to user's bank)

Send the user's Infinia balance back to their own UK / EU bank account. Same currency in and out — no FX, no on-chain step.

Sequence

Payload detail for the calls above:

  • POST /bank-details{ currency, accountHolderName, accountNumber + sortCode | iban + bic }
  • POST /withdraws{ amount } (currency inferred from user's home currency)
  • Infinia payouts uses originId = "grng-wd-" + txId, sourceAccountId = user's GBP or EUR Infinia account, destinationType = UK CHAPS/FPS or Europe SEPA.

Total time: ~10s for FPS; same-day for SEPA (bounded by the receiving bank's settlement window).

Screens

  • apps/mobile/app/withdraw/index.tsx — entry.
  • apps/mobile/app/withdraw/bank-details.tsx — one-time capture per currency.
  • apps/mobile/app/withdraw/amount.tsx — amount input, shows current balance ceiling.
  • apps/mobile/app/withdraw/confirm.tsx — review + Pay button.
  • apps/mobile/app/withdraw/receipt.tsx — polls status.

Fees & timing

  • Infinia payout fee: TBC (part of the open Infinia fee schedule question).
  • GrinGo fee: 0% on withdrawals for MVP.
  • FPS timing: near-instant, typically seconds.
  • SEPA timing: same-day or next-day depending on the receiving bank.

Bank details

  • We cache bank details on our side (user_bank_details, one row per (user, currency)). User enters once; every subsequent withdraw reuses. If they withdraw to both GBP and EUR banks, that's two rows.
  • No pre-registration is needed at Infinia — unlike the previous Bridge/liquidation-address model where we cached a per-bank liquidation address id, Infinia payouts take the bank details inline on every POST /v2/payouts call. There's no beneficiary token to save.
  • Sensitive columns (account_number, sort_code, iban, bic) are encrypted at rest via pgcrypto. Full values are shown in the UI only during in-app edit; elsewhere we mask to ****1234. See privacy.
  • Updating bank details is trivial — replace the row, next payout uses the new details.
  • Bank details are our data, not Infinia's. They're passed through on each payout request.

What we're storing

transactions row (type=withdraw):

  • source_currency = user's home currency (matches the payout destination)
  • source_amount = amount debited (in pence / cent)
  • dest_currency = same as source (no FX)
  • dest_amount = same as source
  • provider = "infinia"
  • provider_payout_id = Infinia's payout id
  • status progresses pending → settling → completed | failed

Failure modes

What brokeWhere it surfacesRecovery
No bank details for the requested currencyPOST /withdraws → 409User adds details, retries
Balance insufficientPOST /withdraws → 402 (or 409)Mobile disables the button when amount > balance
Infinia payout rejected synchronouslyPOST /withdraws → 502; transactions row is failed immediatelyManual retry; user's balance is untouched
Payout webhook says FAILEDtransactions.status = failed; failure_reason stored; balance restored (debit reverses on failed)User sees failure reason on receipt
No payout webhook arrivesPayment stuck in settling. Reconciliation cron polls GET /v2/payouts/{payout_id}/ to detect.Automatic resolution
Recipient bank rejects the transferInfinia surfaces failure via payout.updated FAILEDSame as above

Not supported at MVP

  • Cross-currency withdrawals (e.g. holding GBP but withdrawing as EUR). Home currency is fixed; withdrawals match. If a user wants EUR out, they'd need a EUR home currency — currently a support ticket.
  • Partial withdrawals with FX (e.g. converting £45 to EUR before payout). Same reason as above.
  • Scheduled / recurring withdrawals. Manual only.