Guide
Custodial integration overview
Shared foundation for custodial cash-out and cash-in: onboarding, omnibus wallets, sessions, profiles, notifications, and security.
Custodial integration overview (Solana and Stellar · USDC)
Shared foundation for custodial partners · Cash-out and cash-in · August 2026
This is the common ground for every custodial integration: onboarding, omnibus wallets, sessions, profiles, and the security rules that apply to both flows. Read it once, then follow the guide for the flow you are building.
The flow guides do not repeat this material. They link back here.
You are a custodial partner: you hold customer USDC in a shared omnibus or hot wallet, and your end users never sign a blockchain transaction. Customers are identified by a customerIdentifier you supply rather than by a wallet address.
If your end users sign with their own keys, you are non-custodial. Use the guide for your chain: Web · Solana, React Native · Solana, Web · Stellar, or React Native · Stellar.
Your custody model (walletCustody) is set at partner onboarding and stored on your partner record. You cannot switch per session: the API derives walletType from your registered configuration.
Everything on this page applies to both. Everything in this table differs, which is why the step-by-step guides stay separate.
| Dimension | Custodial cash-out (off-ramp) | Custodial cash-in (on-ramp) |
|---|---|---|
| Widget mode | off-ramp | on-ramp |
| Omnibus wallet role | Source (custodianCashOutWallet) | Destination (custodianCashInWallet) |
| Partner on-chain action | Required. Transfer USDC after MoneyGram allocates a deposit address | None |
| Key widget callback | onDepositAddress (web) / RAMPS_DEPOSIT_ADDRESS (React Native) | None. Do not implement onDepositAddress |
| What the customer does | Collects cash at an agent location | Pays cash at a store using a confirmation code |
| Your ledger action | Debit the internal balance, then send from the hot wallet | Credit the internal balance once the transaction is completed |
| Terminal signal on the done screen | Reference number | Confirmation code, which is not your signal to credit |
Cash-out is transfer orchestration after MoneyGram allocates a deposit address. Cash-in is persist, wait, then credit on confirmed settlement.
Confirm all of the following with MoneyGram before you integrate.
Custody mode
Your partner account must be registered with walletCustody: 'custodial'. This is selected during partner registration or set by MoneyGram ops.
Omnibus wallets
| Field | Role | Needed for |
|---|---|---|
custodianCashOutWallet | Source wallet that sends USDC to MoneyGram | Cash-out |
custodianCashInWallet | Destination wallet that receives USDC from MoneyGram | Cash-in |
Set these in the partner portal (Settings → Profile), or ask MoneyGram ops to configure them on your partner record. Sandbox may use one address for both. On cash-in, the API overrides any destinationWalletAddress you send with your configured custodianCashInWallet.
API keys
| Sandbox | Production | |
|---|---|---|
Public key (pk) | ramps_pk_sbox_... | ramps_pk_prod_... |
Secret key (sk) | ramps_sk_sbox_... | ramps_sk_prod_... |
The secret key is server-side only, for session creation. Never ship it in client-side JavaScript or a mobile bundle.
Allowed origins
Allowlist your web app origins in the partner portal before the keys work in the widget.
KYB and funding wallets
Complete KYB (Live Hub) before production. Custodial partners usually provide funding wallet addresses during KYB so MoneyGram can verify the omnibus setup. An approved KYB is also required before you can set a production webhook URL.
| Sandbox | Production | |
|---|---|---|
| API base | https://playground.xramps.moneygram.com/api | https://xramps.moneygram.com/api |
| Session endpoint | POST /v1/sessions | Same path |
| Widget URL | Returned by the session API (widget.html) | Returned by the session API |
| Solana USDC mint | 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU (devnet, SPL, not Token-2022) | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v (mainnet SPL) |
| Stellar USDC issuer | GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 (testnet) | GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN (mainnet) |
| Stellar settlement memo | Memo.id (numeric string) when depositMemo is present | Same |
Always use the widgetUrl from the session response. It carries the correct mode for the flow, so do not hardcode it. Do not use stub-widget.html.
Solana omnibus addresses are base58 public keys. Stellar omnibus addresses are G-addresses. A Stellar cash-in destination needs a USDC trustline and enough XLM to hold the trustline. Cash-out payments on Stellar must include depositMemo as Memo.id when it is present.
Custodial sessions require a customerIdentifier: a stable opaque string (UUID, hashed email, internal user ID) that identifies the end customer across transactions. Maximum 255 characters.
The identifier is embedded in the session JWT server-side. The widget cannot forge or change it.
const chain = 'solana' // or 'stellar' 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({ customerIdentifier, // from your authenticated session, never from the client body alone walletAddress: omnibusWallet, // cash-out: custodianCashOutWallet. cash-in: custodianCashInWallet chain, // 'solana' | 'stellar' // walletTransactionId: '550e8400-e29b-41d4-a716-446655440000', // optional, your own correlation id }),})
Request fields
| Field | Required | Description |
|---|---|---|
customerIdentifier | Yes | Stable internal user ID, max 255 characters |
walletAddress | Recommended | The omnibus wallet for the flow you are starting |
chain | Yes for widget sessions | 'solana' or 'stellar' |
walletTransactionId | Optional | UUID, your reference for MoneyGram correlation (custodial only) |
Response
{ "sessionId": "b4641a16-2b96-4afe-acb6-b71682305d08", "sessionToken": "eyJhbGci...", "walletType": "custodial", "language": "en", "widgetUrl": "https://playground.xramps.moneygram.com/widget.html?mode=off-ramp"}
Common errors
| Status | Cause |
|---|---|
| 400 | Missing customerIdentifier on a custodial partner |
| 403 | Public key used where the secret key is required |
| 401 | Invalid or revoked API key |
Session TTL: tokens expire after 1 hour. Create a fresh session each time the user opens the widget, and do not cache tokens.
Fetch your registered configuration at runtime rather than hardcoding wallet addresses in your frontend.
GET /v1/partners/configAuthorization: Bearer <sessionToken>
{ "walletType": "custodial", "custodianCashOutWallet": "YourSolanaHotWallet...", "custodianCashInWallet": "YourSolanaCashInWallet...", "depositWallets": { }}
Use the field that matches your flow as the wallet.address you pass to the widget.
Because custodial customers share an omnibus wallet, profile lookup uses customerIdentifier instead of a blockchain address. Both endpoints require a Bearer session token, not a raw API key.
| Endpoint | Purpose |
|---|---|
POST /v1/profiles | Pre-create or find a profile server-side. Uses the customerIdentifier from the session JWT. Returns { profileId, isNewProfile, ... } |
GET /v1/profiles/me | Called by the widget on load when walletType === 'custodial'. 200 prefills the KYC form, 204 means a first-time customer who completes KYC in the widget |
Three notification paths cover both flows, and they are documented once in Transaction status and webhooks:
| Path | Use it for |
|---|---|
| Widget events | User-facing UX in the session that is open right now |
GET /v1/transactions/{id}/status polling | Authoritative state, on demand |
| Partner webhooks | Server-side push after the user has closed the widget |
Two rules apply to every custodial integration:
- Persist
mgiTransactionIdwhen the transaction is created. It is your correlation key for webhooks. See RAMPS_TRANSACTION_CREATED. - Confirm with
GET /status?sync=truebefore moving money. Webhookstatusvalues use a different vocabulary from the Ramps APIstatusvalues, and a widget event is a UX signal rather than settlement.
These apply to both flows and are not optional in production.
- Never trust browser-supplied transfer parameters. Take the deposit address and the amount from
GET /statuson your server, not from the widget callback payload. - Make funds movement idempotent. Key a durable record on the Ramps transaction id and return the stored result on retry. Widget callbacks and webhooks can both repeat.
- Debit atomically and reverse on failure. Reserve funds only if the balance is sufficient, in one operation, and compensate if the on-chain send fails afterwards.
- Verify webhook signatures before acting on a notification, using the scheme in Transaction status and webhooks.
- Keep the secret key server-side. Sessions are created from your backend, and the widget only ever sees a session token.
- Derive
customerIdentifierfrom your authenticated session. A client-supplied user ID lets one customer read another customer's KYC prefill.
Partners often keep a non-custodial production integration while piloting custodial on a second partner record. That works, with three things to know.
- Each partner record has its own API keys. A custodial key and a non-custodial key are not interchangeable, and
walletTypefollows the record rather than the request. - Each record has its own webhook URL setting in the partner portal. Both records may point at the same backend endpoint. Events carry
mgiTransactionId, so route on the stored transaction rather than on which record delivered the event. - Both records on one environment verify webhooks against the same public key. Sandbox and production keys differ from each other, and both are published in Transaction status and webhooks.
MoneyGram provisions each record with its own agent and main office IDs. If you believe your two records share one, raise it with your integration contact before configuring webhooks on both.
| You are building | Start here |
|---|---|
| Custodial cash-out on the web (Solana) | Quickstart, then the Web guide |
| Custodial cash-out in React Native (Solana) | Quickstart, then the React Native guide |
| Custodial cash-out on the web (Stellar) | Web guide |
| Custodial cash-out in React Native (Stellar) | React Native guide |
| Custodial cash-in (both chains) | Quickstart, then the cash-in guide |
| Embed (Web SDK, iframe, React Native) | Embed the widget |
| Status, polling, and webhooks | Transaction status and webhooks |
| Widget configuration and callbacks | API reference |