Guide
Web integration · Stellar / USDC
Core non-custodial Web flow: sessions, iframe SDK, Stellar USDC signing and memos, testing.
MoneyGram Ramps: Web Integration Guide (Stellar / USDC)
Widget + API keys · Stellar / USDC
This guide is for Stellar wallets integrating through the same widget + API keys model as Solana. You do not implement SEP-10 or SEP-24 directly. MoneyGram handles anchor interaction inside the widget and REST API.
Shared credentials and environments: Getting started. Legacy SEP anchor partners: SEP guide.
Also see: Stellar hub · Quickstart · Web · React Native guide · API reference
MoneyGram Ramps lets Stellar wallet partners embed cash-out (off-ramp) and cash-in (on-ramp) flows for USDC.
Cash-out: The user sends USDC on Stellar to MoneyGram's deposit address (with a memo when required). The widget handles KYC and quoting. Your app signs the transfer when the widget requests it, then the user receives a MoneyGram reference number for cash pickup.
Cash-in: The user selects a MoneyGram location, deposits cash at the counter, and receives USDC in their Stellar wallet. No on-chain signing is required in the standard happy path.
Host: createRamps, a raw iframe, and React Native share one message contract. Use Embed the widget. This guide covers the Stellar non-custodial flow, including Memo.id and the USDC issuer.
Integration surface:
| Layer | Your responsibility |
|---|---|
| Backend | POST /v1/sessions with secret key (ramps_sk_*) |
| Frontend | Embed the MoneyGram widget with public key + session token |
| Wallet | Sign Stellar USDC transfers when the widget emits RAMPS_SIGN_TRANSACTION |
Integration time: 30–60 minutes for a basic sandbox flow
Chain: Stellar
Asset: USDC
| Sandbox | Production | |
|---|---|---|
Public key (pk) | ramps_pk_sbox_... | ramps_pk_prod_... |
Secret key (sk) | ramps_sk_sbox_... | ramps_sk_prod_... |
The secret key must only ever be used server-side.
Environments
| Sandbox (playground) | Production | |
|---|---|---|
| API base | https://playground.xramps.moneygram.com/api | https://xramps.moneygram.com/api |
| Session | POST {base}/v1/sessions | Same pattern |
| Real money | No | Yes |
| Session JWT TTL | 1 hour | 1 hour |
Sandbox and production keys are separate clusters. A playground key returns 401 on production hosts.
Stellar USDC (reference)
Use tokenAddress and requiredNetwork from the sign payload. Use issuer when the payload includes it. Otherwise choose the Circle issuer from requiredNetwork. Do not hardcode the mainnet issuer when testing with sandbox keys.
| Testnet (sandbox keys) | Mainnet (production keys) | |
|---|---|---|
| USDC asset code | USDC | USDC |
| Typical testnet issuer | GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 | |
| Typical mainnet issuer | GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN |
| Cash-out (off-ramp) | Cash-in (on-ramp) | |
|---|---|---|
| User action | Send USDC on Stellar | Deposit cash at a MoneyGram location |
| Widget mode | mode=off-ramp | mode=on-ramp |
| Partner callback | Sign USDC transfer (+ memo if provided) | Not used in the standard flow |
| Completion artifact | referenceNumber for pickup | Counter confirmation / reference |
Create a backend endpoint that calls MoneyGram's session API. Pass the user's Stellar G-address and chain: "stellar".
async function createMoneyGramSession(req, res) { 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 of the end user 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, })}
Call this endpoint fresh each time the user opens the widget. Do not cache session tokens beyond their 1-hour TTL.
Load the widget SDK and initialize with your public key and the session token from Step 1.
<script src="https://playground.xramps.moneygram.com/sdk/index.global.js"></script>const { createRamps } = window.RampsSDK const session = await fetch('/api/moneygram-session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ walletAddress: userGAddress }),}).then((r) => r.json()) const ramps = createRamps({ container: document.getElementById('ramps-widget-container'), sessionToken: session.sessionToken, widgetUrl: session.widgetUrl, wallet: { address: userGAddress, chain: 'stellar', asset: 'USDC', walletType: 'non-custodial', }, onSignTransaction: async (tx) => { // Step 3: sign Stellar USDC transfer return await signStellarUsdcTransfer(tx) }, onComplete: (transaction) => { console.log('Done:', transaction.referenceNumber, transaction.type) }, onError: (err) => console.error(err),}) ramps.open()
For cash-in, 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') const ramps = createRamps({ // ...sessionToken, wallet, transaction: { type: 'on-ramp' } widgetUrl: widgetUrl.toString(), mode: 'on-ramp',})
mode: 'on-ramp' on createRamps is sent in RAMPS_CONFIG and sets the flow. Set transaction.type to 'on-ramp' as well.
When the user commits a cash-out, the widget calls onSignTransaction with a payload like:
| Field | Description |
|---|---|
chain | 'stellar' |
asset | 'USDC' (required) |
to | MoneyGram deposit G-address |
amount | USDC amount as a string |
memo | Required when present. Numeric settlement memo. Attach with Memo.id |
tokenAddress | Asset code (USDC) |
issuer | Circle issuer when present. If absent, choose it from requiredNetwork |
requiredNetwork | 'testnet' or 'mainnet'. Match your wallet network |
Example with @stellar/stellar-sdk. Reuse usdcIssuer and settlementMemo anywhere you build a Stellar cash-out payment (Web, React Native, custodial).
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 signStellarUsdcTransfer(tx: { to: string amount: string memo?: string tokenAddress?: string issuer?: string requiredNetwork?: string}): Promise<string> { if (tx.requiredNetwork !== 'mainnet' && tx.requiredNetwork !== 'testnet') { throw new Error('Sign payload is missing requiredNetwork') } const requiredNetwork = tx.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(process.env.USER_SECRET!) // your wallet signing // tokenAddress is the asset code (USDC). Prefer issuer on the payload. const usdc = new Asset( tx.tokenAddress ?? 'USDC', tx.issuer ?? usdcIssuer(requiredNetwork), ) const account = await horizon.loadAccount(sourceKeypair.publicKey()) let builder = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase, }).addOperation( Operation.payment({ destination: tx.to, asset: usdc, amount: tx.amount, }) ) if (tx.memo?.trim()) { builder = builder.addMemo(settlementMemo(tx.memo.trim())) } const transaction = builder.setTimeout(180).build() transaction.sign(sourceKeypair) const result = await horizon.submitTransaction(transaction) return result.hash}
Memo: When tx.memo is set, attach it as a Stellar ID memo (Memo.id). The value is a numeric string (an unsigned 64-bit integer). Memo.text does not match MoneyGram's deposit record, and check-deposit rejects it. Omitting a memo that is present also fails deposit verification.
Issuer: tokenAddress is the asset code (USDC). Use issuer when the sign payload includes it. If it is absent, choose the Circle issuer from requiredNetwork (table above). Do not send the testnet issuer on a mainnet payload. Sandbox keys use testnet. Production keys use mainnet. Widgets deployed before issuer was added omit the field.
iframe / WebView postMessage alternative
If you embed widgetUrl in your own iframe, or in a React Native WebView, use Embed the widget. The payload on RAMPS_SIGN_TRANSACTION matches onSignTransaction. Reply with RAMPS_SIGN_SUCCESS and { txHash, walletAddress }, or RAMPS_SIGN_ERROR.
If you hold customer USDC in an omnibus wallet:
- Register as custodial at onboarding.
- Pass
customerIdentifier(stable internal user ID) inPOST /v1/sessions. - The widget may emit
RAMPS_DEPOSIT_ADDRESSinstead ofRAMPS_SIGN_TRANSACTIONso your backend moves funds from the hot wallet.
See the Web custodial cash-out guide for the same session and callback patterns on Stellar.
- Complete KYB in the partner portal.
- Submit verified Playground transaction IDs for integration tests.
- Configure product fees.
- Request production activation for live keys (
ramps_pk_prod_/ramps_sk_prod_).
Production keys require completed KYB and MoneyGram agent ID provisioning.
The widget uses the same REST API documented in the OpenAPI spec:
Key paths: /v1/sessions, /v1/transactions, /v1/transactions/{id}/status, /v1/transactions/{id}/check-deposit (cash-out, with Stellar txHash).
| This guide (widget + API keys) | Legacy SEP-24 path | |
|---|---|---|
| Credentials | ramps_pk_* / ramps_sk_* | Whitelist + anchor JWT |
| Integration | Widget + session + sign callback | SEP-10 + SEP-24 + polling |
| Onboarding | Self-serve sandbox keys (when enabled) | Manual whitelist review |
Existing SEP-24 partners may continue on the anchor path during transition. New Stellar pilot partners should use this widget path.
For pilot access issues, contact your MoneyGram Ramps integration contact. Do not paste secret keys or customer PII into support channels.