Guide

Embed the widget

One host contract for Web createRamps, a raw iframe, and React Native WebView. Chain signing stays in the flow guides.

createRampsiframeReact Native

Embed the widget

One host page for every chain, custody model, and flow. Solana and Stellar signing stay in the flow guides.

Sessions, KYC, quotes, cash-in rules, cash-out rules, and webhooks do not change between Web and React Native. Only the host changes.

HostWhen to use it
Web createRampsBrowser app using the iframe SDK
Web iframeBrowser app that loads widgetUrl in its own iframe
React Native WebViewMobile app using react-native-webview

The message contract below is the same for the iframe and the WebView. createRamps wraps that contract. Do not keep a second copy in a platform guide.

Chain signing (Solana mint and ATA, Stellar issuer and Memo.id) stays in the flow guide:

ChainNon-custodialCustodial cash-out
SolanaWeb · React NativeWeb · React Native
StellarWeb · React NativeWeb · React Native

Use the widgetUrl from POST /v1/sessions. It is widget.html with a mode query. Do not use stub-widget.html.


Message contract

Widget and host exchange { type, payload }. For cash-out and cash-in the order is:

TEXT
RAMPS_READY  → RAMPS_CONFIG  → RAMPS_CHECK_BALANCE → RAMPS_BALANCE_RESULT  → RAMPS_TRANSACTION_CREATED          after validate, before commit  → sign or deposit (cash-out only)  → RAMPS_TRANSACTION_COMPLETE  → RAMPS_CLOSE
MessageDirectionWhenPayload
RAMPS_READYWidget → hostWidget loadednone
RAMPS_CONFIGHost → widgetReply to RAMPS_READYSession, wallet, optional pre-fill. See below
RAMPS_CHECK_BALANCEWidget → hostBefore quote{ amount, asset: 'USDC' }
RAMPS_BALANCE_RESULTHost → widgetReply{ balance, sufficient, asset: 'USDC' }
RAMPS_TRANSACTION_CREATEDWidget → hostAfter validate succeeds, before commit{ id, type, status: 'created', ... }
RAMPS_SIGN_TRANSACTIONWidget → hostNon-custodial cash-outSame shape as onSignTransaction
RAMPS_DEPOSIT_ADDRESSWidget → hostCustodial cash-out{ address, amount, chain, asset, memo? }
RAMPS_SIGN_SUCCESSHost → widgetTransfer submitted{ txHash, walletAddress }
RAMPS_SIGN_ERROREither directionSigning or transfer failed{ error }
RAMPS_TRANSACTION_COMPLETEWidget → hostDone screenReference number (cash-out) or confirmation code (cash-in)
RAMPS_TRANSACTION_FAILEDWidget → hostFlow failed{ transactionId, reason }
RAMPS_CLOSEWidget → hostUser closed the widgetnone

RAMPS_TRANSACTION_CREATED.type is 'cash-out' or 'cash-in'. Widget config transaction.type is 'off-ramp' or 'on-ramp' for the same flows. Persist id, and mgiTransactionId when it is present. Full field list: onTransactionCreated.

Cash-in does not send RAMPS_SIGN_TRANSACTION or RAMPS_DEPOSIT_ADDRESS. The confirmation code on RAMPS_TRANSACTION_COMPLETE is not settlement. Credit custodial cash-in only after GET /status?sync=true returns completed.

Custodial cash-out sends RAMPS_DEPOSIT_ADDRESS instead of RAMPS_SIGN_TRANSACTION. Reply with RAMPS_SIGN_SUCCESS and the on-chain txHash after your server sends the funds. Read the deposit address and amount from GET /v1/transactions/:id/status, not from the browser payload alone.

RAMPS_CONFIG

TypeScript
{  sessionToken: string  wallet: {    address: string          // Solana public key or Stellar G-address    chain: 'solana' | 'stellar'    asset: 'USDC'    walletType: 'custodial' | 'non-custodial'  }  transaction?: {    type: 'off-ramp' | 'on-ramp'    asset: 'USDC'    amount?: number    destinationCountry?: string  }  customer?: { /* KYC pre-fill. See the flow guide. */ }  devConfig?: { mockMode: false, apiBaseUrl: string }}

For cash-in, set transaction.type to 'on-ramp'. 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.

TypeScript
const widgetUrl = new URL(session.widgetUrl)widgetUrl.searchParams.set('mode', 'on-ramp')

With createRamps, also pass mode: 'on-ramp'. That value is sent in RAMPS_CONFIG and sets the flow.

To reopen a transaction, send RAMPS_CONFIG with mode: 'view' and transactionId set to the id from RAMPS_TRANSACTION_COMPLETE. With createRamps, pass viewTransaction: { id } instead.


Web createRamps

Load the SDK from the widget origin. It is not an npm package.

HTML
<script src="https://playground.xramps.moneygram.com/sdk/index.global.js"></script>
TypeScript
const { createRamps } = window.RampsSDK const ramps = createRamps({  container: document.getElementById('ramps-widget'),  sessionToken: session.sessionToken,  widgetUrl: session.widgetUrl,  wallet: {    address: walletAddress,    chain, // 'solana' | 'stellar'    asset: 'USDC',    walletType, // 'custodial' | 'non-custodial'  },  transaction: { type: 'off-ramp' }, // 'on-ramp' for cash-in  onTransactionCreated: (tx) => {    // Persist tx.id. Persist tx.mgiTransactionId when present.  },  onSignTransaction: async (tx) => {    // Non-custodial cash-out only. Sign with the chain guide sample. Return txHash.    return txHash  },  onDepositAddress: async () => {    // Custodial cash-out only. Do not implement this for cash-in.    return txHash  },  onComplete: (tx) => {    // Cash-out: reference number. Cash-in: confirmation code, not settlement.  },}) ramps.open()

Callbacks map onto the messages above. onTransactionCreated is RAMPS_TRANSACTION_CREATED. onSignTransaction is RAMPS_SIGN_TRANSACTION. onComplete is RAMPS_TRANSACTION_COMPLETE.


Web iframe

Load session.widgetUrl in an iframe. Listen for message events from the widget origin, and post replies to iframe.contentWindow.

TypeScript
const widgetOrigin = new URL(session.widgetUrl).origin window.addEventListener('message', (event) => {  if (event.origin !== widgetOrigin) return  const { type, payload } = event.data ?? {}   if (type === 'RAMPS_READY') {    iframe.contentWindow?.postMessage({      type: 'RAMPS_CONFIG',      payload: {        sessionToken: session.sessionToken,        wallet: { address: walletAddress, chain, asset: 'USDC', walletType },        transaction: { type: 'off-ramp' },      },    }, widgetOrigin)  }   if (type === 'RAMPS_TRANSACTION_CREATED') {    // Persist payload.id and payload.mgiTransactionId when present.  }   if (type === 'RAMPS_SIGN_TRANSACTION') {    // Non-custodial cash-out. Payload matches onSignTransaction.    // Reply with RAMPS_SIGN_SUCCESS { txHash, walletAddress } or RAMPS_SIGN_ERROR.  }   if (type === 'RAMPS_DEPOSIT_ADDRESS') {    // Custodial cash-out only.  }})

Check event.origin before acting. The payload fields for RAMPS_SIGN_TRANSACTION match onSignTransaction in the API reference.


The contract is the iframe contract. The transport is react-native-webview.

  • Receive widget messages on onMessage.
  • Send host messages with injectJavaScript. The injected script must end with true;. Otherwise it is ignored.
  • Do not put the secret API key in the app binary. Create the session on your server.
  • Production WebViews load HTTPS. Set Android usesCleartextTraffic to false in production. iOS needs no ATS exception for the MoneyGram widget host.
  • Reject messages whose page URL is not the widget origin.
TSX
function post(type: string, payload?: Record<string, unknown>) {  const json = JSON.stringify(payload ? { type, payload } : { type })  const encoded = btoa(unescape(encodeURIComponent(json)))  webViewRef.current?.injectJavaScript(`    (function () {      var data = JSON.parse(decodeURIComponent(escape(atob('${encoded}'))));      window.dispatchEvent(new MessageEvent('message', { data: data }));    })();    true;  `)}
TSX
function onMessage(event: WebViewMessageEvent) {  if (!event.nativeEvent.url.startsWith(widgetOrigin)) return  const { type, payload } = JSON.parse(event.nativeEvent.data)  if (type === 'RAMPS_READY') post('RAMPS_CONFIG', config)  if (type === 'RAMPS_TRANSACTION_CREATED') persist(payload)  if (type === 'RAMPS_SIGN_TRANSACTION') signAndReply(payload)}

Set javaScriptEnabled and domStorageEnabled. Set cacheEnabled={false} so a cached widget does not run against a new session.

Worked component examples stay in the React Native flow guides. If those examples and this table disagree, this table is the contract.