Guide
React Native · Stellar / USDC
WebView wiring for cash-in and cash-out: postMessage bridge and Stellar USDC signing for cash-out.
MoneyGram Ramps: React Native Integration Guide (Stellar / USDC)
Preview · Widget + API keys · Stellar / USDC
MoneyGram Ramps lets partners embed a crypto-to-cash off-ramp directly inside their mobile wallet. Users send USDC on Stellar and pick up cash at any MoneyGram agent location worldwide.
How this works end-to-end: Your app opens a WebView containing the MoneyGram widget. The widget walks the user through amount selection, KYC, and disclosure acceptance. When ready, it asks your app to sign and submit a Stellar USDC payment. Once the transfer is confirmed on-chain, the widget issues a reference number the recipient redeems at any MoneyGram agent location.
Two modes:
- New transaction. The user starts a cash-out. Optionally pre-fill the amount, destination country, and KYC fields.
- View mode. Reopen an existing transaction in
RAMPS_CONFIGwithmode: 'view'andtransactionIdset to the id fromRAMPS_TRANSACTION_COMPLETE. On the web SDK, passviewTransaction: { id }tocreateRamps. The widget shows current status, pickup instructions, and refund requests when the transfer is eligible.
React Native embeds the widget in a WebView using react-native-webview. Communication uses a postMessage-style bridge: your app injects JavaScript into the WebView to send messages in, and receives messages via the onMessage prop.
Host: The message contract is Embed the widget. This guide covers the Stellar payment, including Memo.id and the USDC issuer.
Supported asset: USDC on Stellar only.
Stellar memos: When memo is present on RAMPS_SIGN_TRANSACTION, attach it with Memo.id. The value is a numeric string. A text memo does not match the deposit record.
Integration time: 30–60 minutes for the complete setup
Chain: Stellar
Asset: USDC
For credentials and environments, see Getting started.
The secret key must never live in the app binary. Create a server endpoint that calls MoneyGram's session API and returns only what the app needs.
async function createMoneyGramSession(req, res) { const origin = req.headers.origin const allowedOrigins = ['https://yourapi.com'] if (allowedOrigins.includes(origin)) { res.setHeader('Access-Control-Allow-Origin', origin) res.setHeader('Vary', 'Origin') } res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS') res.setHeader('Access-Control-Allow-Headers', 'Content-Type') if (req.method === 'OPTIONS') return res.status(204).end() if (req.method !== 'POST') return res.status(405).end() const secretKey = process.env.MONEYGRAM_SK // ramps_sk_sbox_... if (!secretKey) return res.status(500).json({ error: 'Secret key not configured' }) const { walletAddress } = req.body ?? {} const mgiRes = await fetch('https://playground.xramps.moneygram.com/api/v1/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': secretKey }, body: JSON.stringify({ walletAddress, // G... address chain: 'stellar', }), }) const data = await mgiRes.json() if (!mgiRes.ok) return res.status(mgiRes.status).json(data) return res.status(200).json({ sessionToken: data.sessionToken, sessionId: data.sessionId, widgetUrl: data.widgetUrl, })}
Session response example:
{ "sessionToken": "eyJhbGci...", "sessionId": "b4641a16-2b96-4afe-acb6-b71682305d08", "widgetUrl": "https://playground.xramps.moneygram.com/widget.html?mode=off-ramp"}
Session TTL: Tokens expire after 1 hour. Create a fresh session each time the user opens the widget.
npm install react-native-webview @stellar/stellar-sdk # iOS: install native podnpx pod-install
Android (AndroidManifest.xml):
<uses-permission android:name="android.permission.INTERNET" />Use Embed the widget. The injected script must end with true;. RAMPS_TRANSACTION_CREATED fires after validate. Do not treat RAMPS_TRANSACTION_COMPLETE as settlement.
Call this from your RAMPS_SIGN_TRANSACTION handler. Use your wallet's internal keypair and signing infrastructure.
import { Asset, BASE_FEE, Horizon, Keypair, Memo, Networks, Operation, TransactionBuilder,} from '@stellar/stellar-sdk' const TESTNET_USDC_ISSUER = 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5'const MAINNET_USDC_ISSUER = 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN' function usdcIssuer(requiredNetwork: 'mainnet' | 'testnet'): string { if (requiredNetwork === 'mainnet') return MAINNET_USDC_ISSUER return TESTNET_USDC_ISSUER} /** Settlement memos are Stellar ID memos (unsigned 64-bit), not text memos. */function settlementMemo(memo: string) { if (!/^[0-9]+$/.test(memo)) { throw new Error('Settlement memo must be a numeric Stellar ID memo') } return Memo.id(memo)} async function sendStellarUsdc( sourceSecret: string, to: string, amount: string, // exact string from RAMPS_SIGN_TRANSACTION: do not round memo?: string, tokenAddress = 'USDC', requiredNetwork?: 'mainnet' | 'testnet', issuer?: string,): Promise<string> { if (requiredNetwork !== 'mainnet' && requiredNetwork !== 'testnet') { throw new Error('Sign payload is missing requiredNetwork') } const assetIssuer = issuer ?? usdcIssuer(requiredNetwork) const networkPassphrase = requiredNetwork === 'mainnet' ? Networks.PUBLIC : Networks.TESTNET const horizon = requiredNetwork === 'mainnet' ? new Horizon.Server('https://horizon.stellar.org') : new Horizon.Server('https://horizon-testnet.stellar.org') const sourceKeypair = Keypair.fromSecret(sourceSecret) const usdc = new Asset(tokenAddress, assetIssuer) const account = await horizon.loadAccount(sourceKeypair.publicKey()) let builder = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase, }).addOperation( Operation.payment({ destination: to, asset: usdc, amount, }), ) if (memo?.trim()) { builder = builder.addMemo(settlementMemo(memo.trim())) } const transaction = builder.setTimeout(180).build() transaction.sign(sourceKeypair) const result = await horizon.submitTransaction(transaction) return result.hash}
Memo: When memo is set, attach it with Memo.id. The value is a numeric string. Memo.text fails deposit verification. Omitting a present memo fails it too.
Issuer: tokenAddress is the asset code (USDC). Use issuer when the sign payload includes it. If it is absent, usdcIssuer follows requiredNetwork. Do not fall back to the testnet issuer when the payload says mainnet. Widgets deployed before issuer was added omit the field.
XLM transaction fees
Every Stellar transaction requires a small amount of XLM for network fees (typically 0.00001 XLM per operation). RAMPS_CHECK_BALANCE asks for USDC balance only. If the user has USDC but insufficient XLM, submission fails after KYC.
Extend your balance handler to also verify XLM balance, or sponsor fees from a partner fee account if your wallet supports it.
case 'RAMPS_READY': post('RAMPS_CONFIG', { sessionToken: session.sessionToken, wallet: { address: walletGAddress, chain: 'stellar', asset: 'USDC', walletType: 'non-custodial', }, devConfig: { mockMode: false, apiBaseUrl: 'https://playground.xramps.moneygram.com/api', }, }) break case 'RAMPS_SIGN_TRANSACTION': { const to = payload?.to as string const amount = payload?.amount as string const memo = payload?.memo as string | undefined if (!to || !amount) { post('RAMPS_SIGN_ERROR', { error: 'Missing transaction parameters' }) break } setSigning(true) try { const txHash = await sendStellarUsdc( userSecret, to, amount, memo, (payload?.tokenAddress as string | undefined) ?? 'USDC', payload?.requiredNetwork as 'mainnet' | 'testnet' | undefined, payload?.issuer as string | undefined, ) post('RAMPS_SIGN_SUCCESS', { txHash, walletAddress: walletGAddress }) } catch (err) { post('RAMPS_SIGN_ERROR', { error: err instanceof Error ? err.message : 'Signing failed', }) } finally { setSigning(false) } break}
For a full working component with view mode, customer pre-fill, and error UI, mirror the structure in the Solana React Native guide in the main developer portal and substitute Stellar signing from Step 4.
Cash-in does not require onSignTransaction in the standard happy path. The session widgetUrl already includes mode=off-ramp. Replace that value. Do not append a second mode. The widget reads the first mode parameter and would stay on cash-out.
const widgetUrl = new URL(session.widgetUrl)widgetUrl.searchParams.set('mode', 'on-ramp')
In RAMPS_CONFIG, set transaction.type to 'on-ramp'. The user deposits cash at a MoneyGram location and receives USDC in their Stellar wallet.
If you hold customer USDC in an omnibus wallet, use the React Native · Custodial cash-out guide instead. Handle RAMPS_DEPOSIT_ADDRESS, not RAMPS_SIGN_TRANSACTION.