Guide

React Native · Custodial cash-out · Solana / USDC

Custodial off-ramp in React Native: WebView bridge, customerIdentifier sessions, RAMPS_DEPOSIT_ADDRESS, and server-side hot-wallet transfers.

1–2 hoursCustodialReact NativeUSDC · off-ramp

MoneyGram Ramps: Custodial Cash-Out (React Native / Solana / USDC)

Version 1.0 · Custodial partners · React Native · Cash-out only · August 2026

Overview

This guide is for custodial partners building a React Native app. You hold customer USDC in a shared omnibus or hot wallet; end users do not sign on-chain transactions themselves.

For non-custodial React Native (user wallet signing via RAMPS_SIGN_TRANSACTION), use the React Native · Solana (non-custodial) guide.

For the web iframe SDK (createRamps + onDepositAddress), see Web · Custodial cash-out.

Scope: Custodial cash-out (off-ramp) only. For custodial cash-in, use the Custodial cash-in guide. The WebView message contract is Embed the widget.

How it works:

  1. Your backend creates a session with customerIdentifier for the logged-in user.
  2. Your app opens the MoneyGram widget in a WebView and sends RAMPS_CONFIG with walletType: 'custodial'.
  3. When the user commits, the widget sends RAMPS_DEPOSIT_ADDRESS (not RAMPS_SIGN_TRANSACTION).
  4. Your app calls your internal transfer API, then replies with RAMPS_SIGN_SUCCESS and the on-chain txHash.
  5. The widget polls until confirmed and returns a reference number for cash pickup.

The widget and API are the same as web custodial. Only the postMessage bridge differs.


These apply to both web and React Native (sessions, omnibus wallet, profiles):

TopicDetails
Custody modeSet at onboarding (walletCustody: 'custodial'), cannot switch per session
customerIdentifierRequired in POST /v1/sessions: stable internal user ID (max 255 chars)
Omnibus walletcustodianCashOutWallet: USDC source for off-ramp; set in partner portal
Profile APIsOptional POST /v1/profiles, GET /v1/profiles/me with Bearer session token

Full detail for all four lives in the Custodial integration overview, which is shared by every custodial integration. See Web · Custodial cash-out for backend transfer orchestration patterns.


Pass customerIdentifier from your authenticated user. Never trust a raw client-supplied ID without verification.

TypeScript
// Your mobile backend: POST /api/moneygram-sessionasync function createCustodialSession(userId: string) {  const res = await fetch('https://playground.xramps.moneygram.com/api/v1/sessions', {    method: 'POST',    headers: {      'Content-Type': 'application/json',      'x-api-key': process.env.MONEYGRAM_SK!,  // ramps_sk_sbox_*, server only    },    body: JSON.stringify({      customerIdentifier: userId,      walletAddress: process.env.PARTNER_OMNIBUS_WALLET,      chain: 'solana',    }),  })  if (!res.ok) throw new Error(`Session failed: ${res.status}`)  return res.json() as Promise<{    sessionToken: string    sessionId: string    walletType: 'custodial'    widgetUrl: string  }>}

Session TTL: 1 hour. Create a fresh session each time the user opens the widget.


Use the same WebView + postMessage pattern as the non-custodial React Native guide, with these custodial differences:

Non-custodialCustodial
walletType: 'non-custodial'walletType: 'custodial' (from session response)
wallet.address = user's pubkeywallet.address = your omnibus / hot wallet
Handle RAMPS_SIGN_TRANSACTIONHandle RAMPS_DEPOSIT_ADDRESS
Sign with user wallet adapterCall internal transfer API → return txHash

RAMPS_CONFIG (custodial)

On RAMPS_READY, inject config with custodial wallet context:

TypeScript
post('RAMPS_CONFIG', {  sessionToken: session.sessionToken,   wallet: {    address:    PARTNER_OMNIBUS_WALLET,  // custodianCashOutWallet    chain:      'solana',    asset:      'USDC',    walletType: session.walletType,       // 'custodial'  },   devConfig: {    mockMode:   false,    apiBaseUrl: process.env.EXPO_PUBLIC_RAMPS_API_URL ?? 'https://playground.xramps.moneygram.com/api',  },   transaction: {    type:  'off-ramp',    asset: 'USDC',    // optional: destinationCountry, amount, ...  },})

Add this case to your handleMessage switch (alongside RAMPS_SIGN_TRANSACTION, which you do not use for custodial cash-out):

TypeScript
case 'RAMPS_DEPOSIT_ADDRESS': {  const address = payload?.address as string  const memo    = payload?.memo as string | undefined  const amount  = payload?.amount as string  const chain   = payload?.chain as string   // 'solana'  const asset   = payload?.asset as string   // 'USDC'   if (!address || !amount) {    post('RAMPS_SIGN_ERROR', { error: 'Missing deposit address or amount' })    break  }   setSigning(true)  try {    // Prefer server-side transfer: debit internal balance + hot-wallet send    const res = await fetch(INTERNAL_TRANSFER_URL, {      method:  'POST',      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${appAuthToken}` },      body:    JSON.stringify({ to: address, memo, amount, chain, asset }),    })    if (!res.ok) throw new Error(`Transfer failed: ${res.status}`)    const { txHash } = await res.json()     post('RAMPS_SIGN_SUCCESS', {      txHash,      walletAddress: PARTNER_OMNIBUS_WALLET,    })  } catch (err) {    post('RAMPS_SIGN_ERROR', { error: (err as Error).message })  } finally {    setSigning(false)  }  break}

Payload shape:

TypeScript
interface DepositAddressPayload {  address: string  memo?: string  chain: 'solana'  asset: 'USDC'  amount?: string}

Critical: Always send RAMPS_SIGN_SUCCESS with a real txHash string. If you omit it, the widget stays on "Transfer in progress…".

Do not handle RAMPS_SIGN_TRANSACTION for custodial off-ramp: the widget emits RAMPS_DEPOSIT_ADDRESS instead.


Your backend (not the mobile app alone) should:

  1. Resolve the customer's active cash-out transaction and verify destination, asset, chain, and amount against the Ramps API (do not trust values forwarded from the WebView)
  2. Verify the user's internal USDC balance and debit the ledger atomically, keyed on the Ramps transaction id so retries do not pay out twice
  3. Send USDC from your hot wallet to MoneyGram's deposit address (include memo if provided), reversing the debit if the send fails
  4. Return { txHash }

See the backend example in Web · Custodial cash-out: Step 5.

Never embed hot-wallet private keys in the mobile binary. Route transfers through your API.


TEXT
Widget (WebView)                    Your React Native app  │                                      │  │──── RAMPS_READY ────────────────────▶│  │◀─── RAMPS_CONFIG (custodial) ────────│  │                                      │  │──── RAMPS_CHECK_BALANCE ────────────▶│  optional: omnibus USDC balance  │◀─── RAMPS_BALANCE_RESULT ────────────│  │                                      │  │     [ user: KYC, quote, commit ]     │  │                                      │  │──── RAMPS_DEPOSIT_ADDRESS ──────────▶│  { address, memo, amount, chain, asset }  │         (not SIGN_TRANSACTION)       │  → POST internal-transfer API  │◀─── RAMPS_SIGN_SUCCESS ──────────────│  { txHash }  │◀─── RAMPS_SIGN_ERROR ────────────────│  on failure  │                                      │  │──── RAMPS_TRANSACTION_COMPLETE ─────▶│  save referenceNumber  │──── RAMPS_CLOSE ────────────────────▶│

Testing checklist

  • Partner account is custodial; custodianCashOutWallet configured and funded (sandbox USDC)
  • Session endpoint sends customerIdentifier per user
  • RAMPS_CONFIG uses walletType: 'custodial' and omnibus wallet.address
  • RAMPS_DEPOSIT_ADDRESS handler implemented; returns txHash via RAMPS_SIGN_SUCCESS
  • RAMPS_SIGN_TRANSACTION not used for custodial cash-out
  • Hot-wallet signing happens server-side (not in the app binary)
  • Bundle ID registered with MoneyGram (the partner portal allowlist covers web origins only)
  • Secret key (ramps_sk_*) only on your backend

Troubleshooting

SymptomLikely cause
Widget stuck on transfer screenNo txHash in RAMPS_SIGN_SUCCESS
400 on sessionMissing customerIdentifier
RAMPS_SIGN_TRANSACTION receivedwalletType not set to 'custodial' in RAMPS_CONFIG
Transfer fails on-chainWrong USDC mint, insufficient hot-wallet balance, missing memo

GuideUse when
React Native · Solana (non-custodial)User-controlled wallet signing
Web · Custodial cash-outiframe SDK, shared custodial API details
Web · Solana (non-custodial)Web non-custodial

Coming soon

  • Custodial cash-in (on-ramp): receiving USDC into your omnibus wallet and crediting internal balances. See Custodial cash-in.
  • Webhooks: outbound transaction status notifications. Partner URL configuration, payload, signing, and delivery are documented. See Transaction status & webhooks.