Guide

Legacy Stellar integration · SEP-10 / SEP-24

Whitelist anchor path: SEP-10 authentication, SEP-24 interactive cash-in and cash-out, on-chain USDC with memo, and MoneyGram reference numbers. New partners: use the Stellar widget hub instead.

1–3 daysStellarUSDC

MoneyGram Ramps — Stellar Integration Guide (USDC)

Version 1.2 · Stellar + USDC · July 2026

Legacy integration path. This guide is for partners on the Stellar whitelist + SEP-10 / SEP-24 anchor model. New Stellar wallet integrations should use the widget + API keys path: Stellar hub, Credentials & environments, and the Web or React Native Stellar quickstarts.


Overview

What is MoneyGram Ramps?

MoneyGram Ramps on Stellar lets wallets and exchanges offer USDC cash-in (on-ramp) and cash-out (off-ramp) at MoneyGram locations worldwide. MoneyGram handles KYC, compliance, and settlement. Your wallet integrates via the Stellar anchor protocols SEP-10 (authentication) and SEP-24 (interactive deposit and withdrawal).

Supported asset: USDC on Stellar.

Integration time: 1–3 days for a Stellar-native wallet

Coverage: 480,000+ MoneyGram locations · 170+ countries for off-ramps

On and Off Ramps

Cash-out (off-ramp): the user sends USDC to MoneyGram and picks up cash at an MoneyGram agent location.
Cash-in (on-ramp): the user pays cash at a MoneyGram agent and receives USDC in their Stellar account.

Cash-out (withdrawal)

  • Authenticate the user via SEP-10 (Stellar Web Auth).
  • Initiate a withdrawal using the SEP-24 interactive endpoint.
  • Launch MoneyGram's UI in a webview.
  • User completes the UI flow by providing transaction details and KYC.
  • Poll transaction status until pending_user_transfer_start.
  • Send USDC on Stellar with the memo provided by the anchor.
  • Poll again until pending_user_transfer_complete, then display MoneyGram's reference number (external_transaction_id) for agent pickup.

Cash-in (deposit)

  • Authenticate the user via SEP-10 (Stellar Web Auth).
  • Initiate a deposit using the SEP-24 interactive endpoint.
  • Launch MoneyGram's UI in a webview.
  • User completes the UI flow by selecting a deposit location and providing KYC details.
  • Poll transaction status until pending_user_transfer_start — the user pays cash at the selected agent.
  • Receive USDC in the user's Stellar account after the agent confirms payment.

Step 1 — Get allowlisted (Sandbox/Stellar testnet)

Note: The information below is for sandbox/testnet access only. Only provide your production domain/addresses during Certification and go-live in Production.

MoneyGram must allowlist your wallet before you can connect to the Stellar anchor. To provide the necessary info for allowlisting, log into the MoneyGram Ramps partner portal and go to the Developers section (you will need to create an account if you do not already have one.)

You will need to provide the following under "Whitelist in TestNet":

Non-custodial wallets

  • Domain where your stellar.toml is hosted (must include a signing key for Stellar testnet)
  • Wallet name and intended use case

Custodial wallets

  • Wallet domain used in auth (e.g. wallet-provider.com)
  • Authentication wallet address (testnet)
  • Withdraw wallet address (testnet)
  • Deposit wallet address (testnet)

Tip: Verify your TOML file with the Stellar TOML Checker before requesting access.

Step 2 — Install the Stellar SDK

  1. Install the Stellar Wallet SDK: yarn add @stellar/typescript-wallet-sdk

Step 3 — Implement SEP-10, SEP-24, and Status page

Step 4 — Test and Certify the integration in sandbox

Provide the Ramps team with transaction IDs and/or reference numbers for your test transactions — see Certification and go-live in Production.

See KYB and legal agreements.

Step 6 — Provide your production domain/keys and go-live

See Production domain and keys.


SEP-1 (stellar.toml)

MoneyGram publishes its current Signing Key, SEP-10, and SEP-24 endpoints in its stellar.toml. Treat this as the source of truth rather than hardcoding values.

Environmentstellar.toml
Sandboxhttps://extmgxanchor.moneygram.com/.well-known/stellar.toml
Productionhttps://mgxanchor.moneygram.com/.well-known/stellar.toml

SEP-10 authentication

Authenticate users with Stellar Web Auth before any SEP-24 call.

Anchor endpoints

EnvironmentHome domainAuth endpoint
Sandboxextmgxanchor.moneygram.comhttps://extmgxanchor.moneygram.com/stellarsepservice/auth
Productionmgxanchor.moneygram.comhttps://mgxanchor.moneygram.com/stellarsepservice/auth

MoneyGram signing keys (verify challenge signatures)

NetworkPublic key
SandboxGCUZ6YLL5RQBTYLTTQLPCM73C5XAIUGK2TIMWQH7HPSGWVS2KJ2F3CHS
ProductionGD5NUMEX7LYHXGXCAD4PGW7JDMOUY2DKRGY5XZHJS5IONVHDKCJYGVCL

Authentication flow

  1. Request an authentication challenge for the user's Stellar account.
  2. MoneyGram returns a challenge transaction.
  3. Verify MoneyGram's signature on the challenge.
  4. Sign the challenge with your wallet's signing key.
  5. Submit the signed challenge and receive a JWT (authToken).
TypeScript
import { Wallet, SigningKeypair } from '@stellar/typescript-wallet-sdk' const MGI_HOME = 'extmgxanchor.moneygram.com' // sandbox// const CLIENT_DOMAIN = 'mywalletdomain.com' // required for non-custodial wallets async function authenticate(authSecretKey: string) {  const wallet = Wallet.TestNet()  const anchor = wallet.anchor({ homeDomain: MGI_HOME })  const sep10 = await anchor.sep10()  const authKey = SigningKeypair.fromSecret(authSecretKey)  const authToken = await sep10.authenticate({ accountKp: authKey })  // const authToken = await sep10.authenticate({ accountKp: authKey, clientDomain: CLIENT_DOMAIN }) // required for non-custodial wallets  return authToken}

Custodial vs non-custodial

CustodialNon-custodial
AccountsCommon Stellar accounts for auth and fundsIndividual user accounts
Home domain in SEP-10Not requiredRequired — pass your wallet domain
Client domain in SEP-10Not requiredRequired — provide the client domain you allowlisted with MoneyGram
User identificationPositive integer user ID as memo (≤ 64 bits)User's public key + your TOML SIGNING_KEY
SEP-9 "amount" fieldRequired on SEP-24Optional

Learn more: SEP-10 specification

SEP-24 interactive flow

After SEP-10, call SEP-24 to start a cash-in or cash-out. The response includes an interactive url and transaction id. Open url in a webview or in-app browser. Use the Stellar Wallet SDK (shown below) rather than calling the endpoint directly.

SEP-24 endpoint

For reference, the underlying SEP-24 endpoint the SDK calls:

EnvironmentEndpoint
Sandboxhttps://extmgxanchor.moneygram.com/stellarsepservice/sep24
Productionhttps://mgxanchor.moneygram.com/stellarsepservice/sep24

Cash-out (withdrawal)

User sends USDC · picks up cash at MoneyGram.

TypeScript
const { url, id } = await anchor.sep24().withdraw({  authToken,  withdrawalAccount: FUNDS_STELLAR_KEYPAIR.public_key,  assetCode: 'USDC',  lang: 'en',  extraFields: {    // Required for custodial wallets; optional for non-custodial    amount: '50.00',  },})

Cash-in (deposit)

User pays cash at MoneyGram · receives USDC in their Stellar account.

TypeScript
const { url, id } = await anchor.sep24().deposit({  authToken,  destinationAccount: FUNDS_STELLAR_KEYPAIR.public_key,  assetCode: 'USDC',  lang: 'en',  extraFields: {    amount: '100.00', // required for custodial; optional otherwise  },})

Response includes url (open in webview) and id (save for polling).

Learn more: SEP-24 specification

Passing customer info (SEP-9)

MoneyGram supports selected SEP-9 fields. Include them in your SEP-24 interactive request body "extraFields" attribute to pre-fill KYC and reduce friction in MoneyGram's UI.

Custodial wallets must pass amount. Non-custodial wallets may omit it.

JSON
{  ...  "extraFields": {    "asset_code": "USDC",    "amount": "15",    "account": "GDFKIZYZ2HHOXWLSHMVJRQFKVUJTQJAPSHR2HWUB7BLNNZ4BQPQBM36T",    "first_name": "Jhonny",    "last_name": "Adrian",    "birth_date": "1999-12-31",    "mobile_number": "+15076451253",    "address": "2119 Hidden Valley Rd",    "city": "Northfield",    "postal_code": "55057",    "address_country_code": "USA",    "state_or_province": "US-MN"  }}
FieldFormat
first_nameGiven / first name
last_nameFamily name. Second last name allowed, comma-separated: "Garcia, Lopez"
mobile_numberE.164 with country code, e.g. +15076451253
birth_dateYYYY-MM-DD
addressStreet line 1; line 2 comma-separated if needed
cityCity or town
postal_codePostal / ZIP code
address_country_code3-letter ISO, e.g. USA, CAN
state_or_provinceISO-3166-2, e.g. US-MN. Used for USA, CAN, MEX only

Listen for close notification (postMessage)

MoneyGram's interactive UI notifies your host app via postMessage so you can close the webview and resume polling in your wallet.

Current Ramps UI — COMMIT_RESULT

When the user successfully commits in MoneyGram's UI, the host receives:

FieldValue
type"COMMIT_RESULT"
payload.transactionSEP-24-style transaction object (id, status, etc.)
timestampMilliseconds since epoch

Web iframe

JavaScript
window.addEventListener('message', (event) => {  const message = event.data  if (message?.type === 'COMMIT_RESULT') {    const transaction = message.payload.transaction    // Close webview, start/refresh polling with transaction.id  }})

React Native WebView

JavaScript
<WebView  onMessage={(event) => {    const message = JSON.parse(event.nativeEvent.data)    if (message.type === 'COMMIT_RESULT') {      const transaction = message.payload.transaction      // Close modal, poll GET /sep24/transaction?id=transaction.id    }  }}/>

iOS WKWebView: register a script message handler named rampsUI; parse the JSON string body and check type === 'COMMIT_RESULT'.

Ignore other message types (e.g. COMMIT_REQUEST) unless you handle the wallet send step separately.

Legacy Stellar UI

Older integrations on stellar.moneygram.com may post a transaction object directly. Close the webview when transaction.status === 'pending_user_transfer_start':

JavaScript
window.addEventListener('message', (event) => {  const txJson = event.data?.transaction  if (txJson?.status === 'pending_user_transfer_start') {    webview.close()  }})

Poll transaction status

Poll GET /sep24/transaction?id=... (or use the Wallet SDK watcher) until status reaches pending_user_transfer_start, then perform the on-chain step for cash-out or wait for inbound USDC on cash-in.

Cash-out: funds must be sent within 30 minutes of reaching pending_user_transfer_start, or the user will be unable to complete their transaction.

TypeScript
const watcher = anchor.sep24().watcher() const { stop } = watcher.watchOneTransaction({  authToken,  assetCode: 'USDC',  id: transactionId,  onMessage: (transaction) => {    if (transaction.status === 'pending_user_transfer_start') {      // Cash-out: send USDC (see Send or Receive USDC below). Cash-in: monitor for inbound payment.    }    if (transaction.status === 'pending_user_transfer_complete') {      // Cash-out: show reference number (see Send or Receive USDC below)    }  },  onSuccess: (transaction) => {    // completed / refunded / expired  },  onError: (transaction) => {    // error / no_market / too_small / too_large  },})

Raw endpoint

Bash
curl "{MONEYGRAM_DOMAIN}/sep24/transaction?id=$TRANSACTION_ID" \  -H "Authorization: Bearer $SEP10_JWT"
Environment{MONEYGRAM_DOMAIN}
Sandboxhttps://extmgxanchor.moneygram.com
Productionhttps://mgxanchor.moneygram.com

Status lifecycle

StatusMeaningYour action
incompleteUser still in MoneyGram UIKeep polling
pending_user_transfer_startReady for on-chain step (cash-out: send USDC; cash-in: wait for inbound payment after agent takes cash)Send USDC (withdrawal) or monitor account (deposit)
pending_user_transfer_completeOn-chain transfer confirmed · reference number available (withdrawals)Display external_transaction_id for cash pickup
pending_anchorAnchor processingKeep polling
completedSuccessShow confirmation
refunded / expiredTerminal — no further user actionShow appropriate message
error / no_market / too_small / too_largeFailed or rejectedInspect message fields; do not retry blindly

Lookup by other IDs

After on-chain activity you can also fetch by Stellar hash or reference number:

TypeScript
// By anchor transaction id (always available)await anchor.sep24().getTransactionBy({ authToken, id: transactionId }) // By Stellar payment hash (after on-chain transfer)await anchor.sep24().getTransactionBy({ authToken, stellarTransactionId }) // By MoneyGram reference number (external_transaction_id)await anchor.sep24().getTransactionBy({ authToken, externalTransactionId })

Response fields to watch

FieldWhen presentUse
withdraw_anchor_accountCash-out at pending_user_transfer_startDestination for USDC payment
withdraw_memo / withdraw_memo_typeCash-out at pending_user_transfer_startMemo on outbound payment (id memo type is common)
amount_inCash-outUSDC amount to send
deposit_memo / deposit_memo_typeCash-inMatch inbound USDC from MoneyGram
stellar_transaction_idAfter on-chain transferLink to Horizon
external_transaction_idAt pending_user_transfer_complete (withdrawals)Reference number for agent pickup
more_info_urlAt pending_user_transfer_completeHosted page with pickup details

Send or Receive USDC

Act when status is pending_user_transfer_start.

Cash-out — send USDC to MoneyGram

Use fields from the transaction object:

  • Destination: withdraw_anchor_account
  • Amount: amount_in
  • Memo: withdraw_memo with withdraw_memo_type (typically id)
TypeScript
import { Horizon } from '@stellar/stellar-sdk' onMessage: async (transaction) => {  if (transaction.status !== 'pending_user_transfer_start') return   const txBuilder = await stellar.transaction({    sourceAddress: FUNDS_STELLAR_KEYPAIR,    baseFee: 10000,    timebounds: 180,  })   const transferTransaction = txBuilder    .transferWithdrawalTransaction(transaction, asset)    .build()   transferTransaction.sign(FUNDS_STELLAR_KEYPAIR)   try {    const response = await stellar.submitTransaction(transferTransaction)    console.log('Stellar transaction ID:', response.id)  } catch (error) {    // Handle 504 retries, tx_bad_seq (reload account sequence), etc.    // https://developers.stellar.org/docs/learn/encyclopedia/errors-and-debugging/error-handling  }}

After submission, continue polling until status reaches pending_user_transfer_complete — the reference number is now available in:

  • external_transaction_id — show this in your wallet UI for agent pickup
  • more_info_url — hosted page with pickup instructions
TypeScript
onMessage: (transaction) => {  if (transaction.status === 'pending_user_transfer_complete') {    console.log(      `Reference number: ${transaction.external_transaction_id}`,      `Details: ${transaction.more_info_url}`,    )  }}

Share the reference number with pickup instructions. Continue polling until completed.

Cash-in — receive USDC from MoneyGram

For deposits, you do not send USDC. The user pays cash at the selected MoneyGram agent. After the agent receives payment, MoneyGram sends USDC to the user's Stellar account.

Monitor the user's account for an inbound USDC payment matching deposit_memo / deposit_memo_type:

TypeScript
import { Horizon } from '@stellar/stellar-sdk' const server = new Horizon.Server('https://horizon-testnet.stellar.org') server  .payments()  .forAccount(userAccount)  .join('transactions')  .cursor(cursor)  .stream({    onmessage: (payment) => {      if (        payment.type !== 'payment' ||        payment.from === userAccount ||        payment.asset_code !== 'USDC'      ) return       const tx = getTransactionByMemo(        payment.transaction_attr.memo,        payment.transaction_attr.memo_type,      ) // your DB lookup       if (tx) {        console.log('Deposit matched:', payment.transaction_attr.id)      }    },  })

Trustline: The user's Stellar account must have a USDC trustline before receiving USDC. Learn about trustlines.

No reference number is shown in MoneyGram's UI when the flow starts. The user:

  1. Selects a deposit location in MoneyGram's UI.
  2. Commits the transaction and receives a confirmation code to present at the counter (before paying cash).
  3. Pays cash at the agent.
  4. After the agent receives payment, MoneyGram may populate a reference linked to the transaction — poll for updated status.

Do not expect external_transaction_id at commit time for cash-in. Continue polling after the user visits the agent.

more_info_url / Refunds

The transaction status lookup returns a more_info_url field — a link to a standalone, MoneyGram-hosted transaction page. Render this page in a webview within your app, the same way you handle the SEP-24 interactive flow URL.

The more_info_url page shows the user their transaction details, and — for cash-out (withdrawal) — includes a Cancel transfer button that lets the user refund their withdrawal before picking up funds.


Before you launch in Production you will need to complete KYB and legal, as well as complete test transactions in the sandbox environment.

Certify in the sandbox environment

Test the following flows in the sandbox environment:

  1. Cash-out (withdraw) flow
  2. Cash-out refund (withdraw) — use the Cancel transfer button on the more_info_url page
  3. Cash-in (deposit) — stage a transaction for deposit

After completing your test transactions, provide the transaction ID in the Developer section of the partner portal under Playground test transactions.

Complete KYB and Legal Agreements in the partner portal. Reach out to the MoneyGram Ramps team with any questions.

Production domain and keys

Once you have certified in the sandbox environment and have completed the KYB/legal agreements, you can provide the Ramps team with your production domain and keys (info is based on your wallet type, see Get allowlisted section above) in the Developers section of the partner portal.


Reference

Sandbox Cash-in Location Selection

Cash-in (deposit) requires the user to pre-select the deposit location where they will provide the funds. In the sandbox environment, not all locations that appear in the UI locator are configured for MoneyGram Ramps. The widget highlights approved test locations at the top of the picker in playground and other non-production environments. When testing Cash-in in the sandbox environment, use a location from the following table:

CountryAddressLocation Name
USA2600 RICE CREEK RD, NEW BRIGHTON, MN 55112-5344CUB FOODS NEW BRIGHTON
USA1655 NEW YORK AVE, ARLINGTON, TX 76010-4766ELRODS COST PLUS - #7 - ARLINGTON
Canada407 LAURIER AVE W, OTTAWA, ON K1R 7Y7SPOT PLUS - CANADA POST
United Kingdom48 Elm Row, EDINBURGH, EH7 4AHBM GLOBALX LIMITED - #UK161
Ireland15 AUNGIER STREET, RATHMINES WEST, DUBLIN, CO DUBLIN D02 DD73WIRED - CASH AND DEBIT
HungaryDOBO KORUT 8, KECSKEMET, BACS-KISKUN 6000CORNER TRADE KFT.
PolandAl. Jerozlimskie 89 lok. 152 B C D, WARSZAWA, MAZOWIECKIE 02-001LITTLE INDIA SHOP - SKLEP LITTLE INDIA
ArgentinaAVENIDA CORDOBA 947, RETIRO, BUENOS AIRES, CAPITAL FEDERAL 1054LATIN EXPRESS FINANCIAL SERVICES ARGENTI
ArgentinaGENERAL MARIANO ACHA 4092, BUENOS AIRES, CAPITAL FEDERAL 1430SAAVEDRA COBRO EXPRESS - #LEAR1-1476

Note: The Cash-out flow also displays a locator, but the user is not required to pre-select a location to withdraw funds. After completing the UI flow the user can visit any participating MoneyGram location to pick up their funds.

Transaction limits

on/off ramps (production)MinMax
On-ramp (cash-in)15 USDC950 USDC
Off-ramp (cash-out)15 USDC2,500 USDC

Off-ramps available in 170+ countries — see the location locator.

Key concepts

Memo: Identifier tying on-chain payments to MoneyGram records. Always use the memo returned in the SEP-24 transaction object (withdraw_memo for cash-out, deposit_memo for cash-in). Learn more

Trustline: Required before a Stellar account can hold USDC. Learn more

Resources

Setting up a Stellar testnet wallet

If you would like to try out the Moneygram Ramps UI right now:

  1. Generate two testnet Stellar keypairs (authentication and funds) via Stellar Lab.
  2. Fund accounts with XLM and USDC (create a USDC trustline in Stellar Lab).
  3. Test with the Stellar Demo Wallet on testnet.
    • "Provide a secret key (testnet only)" => use your secret key you just created
    • Change "centre.io" to "extmgxanchor.moneygram.com"
    • "Select action" => "Sep-24 withdraw" => "Start"
    • UI will launch in a pop-up window