> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getpara.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Cash In and Cash Out USDC with MoneyGram

> Use a Para wallet with MoneyGram Ramps so users can turn cash into USDC, or USDC into cash, at MoneyGram locations on Stellar and Solana.

MoneyGram Ramps lets users exchange cash and USDC at more than 480,000 MoneyGram locations. Users can cash out in 170+ countries and territories and cash in at locations in 25+ countries.

MoneyGram runs identity checks, quotes, compliance, and settlement inside a widget you embed in your app. Para gives the user a wallet and signs the USDC payment when they cash out. This guide covers the flow on Stellar, and the last section shows what to change for Solana.

## What you'll learn

* Prepare the Stellar wallet to hold USDC
* Create a MoneyGram session on your server for the user's Para wallet
* Sign the cash-out payment with the Para Stellar signer
* Open the MoneyGram widget for cash-out or cash-in
* Track each transaction until it settles

## How it works

The widget runs in an iframe on your page. Your server creates a short-lived session for the wallet address, and the browser opens the widget with it.

On cash-out, the widget asks your app for a USDC payment to MoneyGram, and the Para wallet signs it. On cash-in, the user pays cash at a MoneyGram location and MoneyGram sends USDC to the wallet, so your app signs nothing.

| Part of the flow | Who handles it |
| - | - |
| Login and the user's wallet | Para |
| Identity checks, quotes, compliance, and settlement | MoneyGram |
| Cash pickup and drop-off | MoneyGram agent locations |
| Session creation with the MoneyGram secret key | Your server |
| Cash-out payment signing | Your app, with a Para signer |
| Allowlisted domain and go-live steps | You, with MoneyGram |

> [!NOTE]
>
> MoneyGram's crypto services are not available in Alaska, Louisiana, or Hawaii.

## Before you start

You need a React app with `ParaProvider` and a login flow, plus a server that can keep a secret key. Your Para API key must have Stellar enabled in the [Developer Portal](https://developer.getpara.com). These guides cover the Para setup:

<picture>![React SDK Quickstart](/images/v3/cards/framework-react.png)</picture>

## [React SDK Quickstart](/v3/react/quickstart)

Add ParaProvider and authentication to your app.

<picture>![Use Para with Stellar](/images/v3/cards/chain-stellar.png)</picture>

## [Use Para with Stellar](/v3/react/guides/web3-operations/stellar/setup-libraries)

Sign Stellar transactions with the signed-in Para wallet.

You also need MoneyGram Ramps partner access. Register through MoneyGram's [getting started guide](https://xramps.moneygram.com/ops/developer/getting-started), then get Playground keys and an allowlisted domain from the partner portal. On a domain that MoneyGram has not allowlisted, the widget does not load.

A Stellar account does not exist on the network until it receives XLM. Fund the Para wallet's address before its first transaction, then keep enough XLM for fees and the trustline reserve. Stellar's [create an account guide](https://developers.stellar.org/docs/build/guides/basics/create-account) shows how to fund a testnet account.

## Install the packages

Install the Para React SDK and the Stellar SDK. The versions below are the ones used for this guide.

```bash theme={null}
npm install @getpara/react-sdk@3.23.0 @stellar/stellar-sdk@14.6.1
```

MoneyGram's widget SDK is not an npm package. You load it with a script tag when you open the widget.

## Connect to Stellar

MoneyGram Playground keys run on Stellar testnet, and production keys run on mainnet. The snippets in this guide use these testnet values for the network passphrase, the Horizon server, and the USDC asset:

```typescript theme={null}
import { Asset, Horizon, Networks } from "@stellar/stellar-sdk";

const networkPassphrase = Networks.TESTNET;
const horizon = new Horizon.Server("https://horizon-testnet.stellar.org");
const usdc = new Asset("USDC", "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5");
```

Get the signed-in user's Stellar signer from [useParaStellarSigner()](/v3/react/guides/hooks/use-para-stellar-signer) in a component inside `ParaProvider`:

```typescript theme={null}
import { useParaStellarSigner } from "@getpara/react-sdk";

const { stellarSigner } = useParaStellarSigner({ networkPassphrase });
```

`stellarSigner.address` is the wallet address you send to MoneyGram, and `stellarSigner.signTransaction` signs Stellar transactions with the Para wallet. Run the steps below after the user logs in and `stellarSigner` is set.

## Add a USDC trustline

A Stellar account can hold USDC only after it adds a trustline to the USDC issuer. Cash-in needs the trustline so MoneyGram can deliver USDC to the wallet. Cash-out needs USDC in the wallet, so the trustline already exists by then.

Load the account from Horizon and look for a USDC balance. If there is none, sign and submit a `changeTrust` operation:

```typescript theme={null}
import { BASE_FEE, Operation, TransactionBuilder } from "@stellar/stellar-sdk";

const account = await horizon.loadAccount(stellarSigner.address);

const hasUsdcTrustline = account.balances.some(
  (balance) =>
    "asset_code" in balance &&
    balance.asset_code === usdc.getCode() &&
    balance.asset_issuer === usdc.getIssuer(),
);

if (!hasUsdcTrustline) {
  const transaction = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase })
    .addOperation(Operation.changeTrust({ asset: usdc }))
    .setTimeout(180)
    .build();

  const { signedTxXdr } = await stellarSigner.signTransaction(transaction.toXDR());
  await horizon.submitTransaction(TransactionBuilder.fromXDR(signedTxXdr, networkPassphrase));
}
```

> [!WARNING]
>
> Add the trustline before you open the widget for cash-in. Without it, MoneyGram cannot deliver USDC to the wallet.

## Create a session on your server

Each widget opens with a MoneyGram session. Send the wallet address from the browser to your server. Your server then creates the session with the MoneyGram secret key:

```typescript theme={null}
const response = await fetch(`${process.env.MONEYGRAM_API_URL}/v1/sessions`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": process.env.MONEYGRAM_SECRET_KEY!,
  },
  body: JSON.stringify({ walletAddress, chain: "stellar" }),
});

const { sessionToken, widgetUrl } = await response.json();
```

Keep the secret key and the API URL in server environment variables. The Playground API URL is `https://playground.xramps.moneygram.com/api`.

Check the response status, then return `sessionToken` and `widgetUrl` to the browser, which needs both to open the widget. An error response has neither field. A `401` means the secret key does not match the API URL, such as a Playground key sent to the production API.

A session token expires after one hour, so create a new session each time the widget opens. Send the same wallet address for the same user every time. MoneyGram binds each session to that address, which lets the user reopen earlier transactions and lets the session token read their status.

> [!WARNING]
>
> Keep the secret key (`ramps_sk_`) on your server. Never put it in client code or a public repository.

> [!NOTE]
>
> Before production, confirm the user's Para session on your server with a [verification token](/v3/server/guides/sessions#using-verification-tokens), check that the wallet address belongs to that user, and rate-limit session creation.

## Sign the cash-out payment

When the user commits a cash-out, the widget calls `onSignTransaction` with the payment MoneyGram expects. The payload carries the deposit address, the amount, and the network. It also carries the USDC asset and a memo when MoneyGram sets them.

The callback checks the network, then builds the USDC payment, signs it with the Para wallet, and submits it. It returns the transaction hash to the widget:

```typescript theme={null}
import { Asset, BASE_FEE, Memo, Operation, TransactionBuilder } from "@stellar/stellar-sdk";

const onSignTransaction = async (payment: {
  to: string;
  amount: string;
  memo?: string;
  tokenAddress?: string;
  issuer?: string;
  requiredNetwork?: string;
}) => {
  if (payment.requiredNetwork !== "testnet") {
    throw new Error("MoneyGram payment is not for Stellar testnet");
  }

  const account = await horizon.loadAccount(stellarSigner.address);
  const asset = new Asset(payment.tokenAddress ?? "USDC", payment.issuer ?? usdc.getIssuer());

  const builder = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase })
    .addOperation(Operation.payment({ destination: payment.to, asset, amount: payment.amount }))
    .setTimeout(180);
  if (payment.memo) builder.addMemo(Memo.id(payment.memo.trim()));

  const { signedTxXdr } = await stellarSigner.signTransaction(builder.build().toXDR());
  const result = await horizon.submitTransaction(TransactionBuilder.fromXDR(signedTxXdr, networkPassphrase));
  return result.hash;
};
```

On Stellar, `tokenAddress` holds the asset code (`USDC`), not a contract address. The issuer comes from `issuer`. Widgets deployed before MoneyGram added `issuer` leave it out, so the callback falls back to the testnet USDC issuer.

> [!WARNING]
>
> Attach the memo with `Memo.id`. A text memo fails MoneyGram's deposit check, and so does a payment that leaves out a memo the payload includes.

> [!WARNING]
>
> A submit request that times out can still land on the network, and the widget lets the user retry after a network error. Keep `signedTxXdr` until the submit succeeds. When the widget calls `onSignTransaction` again, resubmit that transaction or look up its hash on Horizon before you build a new payment. Stellar applies a signed transaction only once, but a new transaction sends a second payment.

## Open the widget

Load MoneyGram's SDK on the page that shows the widget:

```html theme={null}
<script src="https://playground.xramps.moneygram.com/sdk/index.global.js"></script>
```

The script adds `window.RampsSDK` and has no TypeScript types, so declare the fields you use in a TypeScript project.

To open the widget, pass the session, the Para wallet, and your callbacks to `createRamps`. `container` is the page element that holds the widget's iframe:

```typescript theme={null}
const mode = "off-ramp";

const url = new URL(widgetUrl);
url.searchParams.set("mode", mode);

const widget = window.RampsSDK.createRamps({
  container,
  sessionToken,
  widgetUrl: url.toString(),
  mode,
  wallet: {
    address: stellarSigner.address,
    chain: "stellar",
    asset: "USDC",
    walletType: "non-custodial",
  },
  transaction: { type: mode },
  onSignTransaction,
  onTransactionCreated,
  onComplete,
});

widget.open();
```

Set `mode` to `off-ramp` for cash-out or `on-ramp` for cash-in. The session's `widgetUrl` always includes `mode=off-ramp`, so replace that query parameter instead of adding a second one. Pass the same mode in `mode` and `transaction.type`.

The widget calls `onSignTransaction` for cash-out. Standard cash-in flows do not call it. The next section covers `onTransactionCreated` and `onComplete`. Call `widget.destroy()` to remove the iframe when the user leaves the page.

> [!NOTE]
>
> The script URL above is MoneyGram's Playground build. Confirm the production URL with MoneyGram before you go live. Your Content Security Policy must also allow iframes from MoneyGram's domains.

## Track settlement

`onTransactionCreated` fires at most once, after MoneyGram validates the transaction and before it commits. MoneyGram does not retry it, so store the record as soon as it arrives. Keep `id` to look up the status later and `mgiTransactionId` to match webhook events.

`onComplete` fires when the widget shows its final screen. For a cash-out, the record includes the `referenceNumber` the user gives at pickup.

The transaction can still be in progress after `onComplete`. A cash-in, for example, waits for the user to pay at the counter. Ask MoneyGram for the current status from your server:

```bash theme={null}
curl "$MONEYGRAM_API_URL/v1/transactions/$TRANSACTION_ID/status?sync=true" \
  -H "x-api-key: $MONEYGRAM_SECRET_KEY"
```

Poll until the transaction reaches a final status for its flow:

| Flow | Stop polling at |
| - | - |
| Cash-in | `completed`, `failed`, or `quote_expired` |
| Cash-out | `completed` when the pickup reference is ready, or `paid_out` when the user has collected the cash. Also stop at `failed`, `quote_expired`, `refunded`, or `refund_failed`. |

> [!WARNING]
>
> Do not stop polling a cash-out at `funds_received`. That status means MoneyGram has received the USDC. The cash is not ready to collect yet.

For server-to-server updates, add a webhook URL in the MoneyGram partner portal under **Settings > Integration**. You can save a production webhook URL only after MoneyGram approves your business verification (KYB).

Webhook events use their own status names and can arrive out of order. Find the stored record by `mgiTransactionId`, then confirm the status with the request above before you act on it. MoneyGram's [transaction status and webhooks reference](https://xramps.moneygram.com/ops/developer/reference/transaction-status-webhooks) covers the event format and signature verification.

## Use Solana instead

The Solana flow keeps the session, widget, and settlement steps from Stellar. You replace the signer and the cash-out callback, and you set two `chain` values to `solana`. Your Para API key must have Solana enabled, and the wallet needs SOL to pay the cash-out transaction fee. Solana has no trustlines, so skip that step.

Install the Solana packages. The versions below are the ones used for this guide.

```bash theme={null}
npm install @solana/kit@2.3.0 @solana-program/token@0.5.1
```

Connect to Solana devnet, which MoneyGram Playground keys use. Create `rpc` once, outside your components, because the signer hook rebuilds the signer each time it gets a new `rpc` object:

```typescript theme={null}
import { createSolanaRpc } from "@solana/kit";

const rpc = createSolanaRpc("https://api.devnet.solana.com");
```

Get the Solana signer from [useParaSolanaSigner()](/v3/react/guides/hooks/use-para-solana-signer) in a component inside `ParaProvider`:

```typescript theme={null}
import { useParaSolanaSigner } from "@getpara/react-sdk";

const { solanaSigner } = useParaSolanaSigner({ rpc });
```

The Solana payload carries the USDC mint in `tokenAddress` and its decimals in `tokenDecimals`. MoneyGram reports devnet as `testnet` in `requiredNetwork`.

The callback converts the decimal amount to base units. It then sends a USDC `transferChecked` from the user's token account to MoneyGram's token account and returns the transaction signature:

```typescript theme={null}
import {
  address,
  appendTransactionMessageInstruction,
  createTransactionMessage,
  getBase64EncodedWireTransaction,
  pipe,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  signTransactionMessageWithSigners,
} from "@solana/kit";
import { TOKEN_PROGRAM_ADDRESS, findAssociatedTokenPda, getTransferCheckedInstruction } from "@solana-program/token";

const onSignTransaction = async (payment: {
  to: string;
  amount: string;
  tokenAddress?: string;
  tokenDecimals?: number;
  requiredNetwork?: string;
}) => {
  if (payment.requiredNetwork !== "testnet") {
    throw new Error("MoneyGram payment is not for Solana devnet");
  }
  if (!payment.tokenAddress || payment.tokenDecimals === undefined) {
    throw new Error("MoneyGram payment is missing the USDC mint or decimals");
  }

  const mint = address(payment.tokenAddress);
  const decimals = payment.tokenDecimals;
  const [whole, fraction = ""] = payment.amount.split(".");
  if (fraction.length > decimals) throw new Error(`Amount has more than ${decimals} decimal places`);
  const amount = BigInt(whole + fraction.padEnd(decimals, "0"));

  const [source] = await findAssociatedTokenPda({ owner: solanaSigner.address, mint, tokenProgram: TOKEN_PROGRAM_ADDRESS });
  const [destination] = await findAssociatedTokenPda({ owner: address(payment.to), mint, tokenProgram: TOKEN_PROGRAM_ADDRESS });
  const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();

  const message = pipe(
    createTransactionMessage({ version: 0 }),
    (tx) => setTransactionMessageFeePayerSigner(solanaSigner, tx),
    (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
    (tx) =>
      appendTransactionMessageInstruction(
        getTransferCheckedInstruction({ source, mint, destination, authority: solanaSigner, amount, decimals }),
        tx,
      ),
  );

  const signedTransaction = await signTransactionMessageWithSigners(message);
  return rpc.sendTransaction(getBase64EncodedWireTransaction(signedTransaction), { encoding: "base64" }).send();
};
```

> [!WARNING]
>
> The retry risk from the Stellar step applies here too. Keep the signed transaction until it confirms. On a retry, resend it or check its signature with `getSignatureStatuses` before you build a new transfer. The old transaction can no longer land once its blockhash expires.

Then change these values from the Stellar steps:

1. Create the session with `chain: "solana"`.
2. In `createRamps`, set `wallet.address` to `solanaSigner.address` and `wallet.chain` to `"solana"`.
3. Pass the Solana `onSignTransaction` to `createRamps`.

MoneyGram's [Solana web guide](https://xramps.moneygram.com/ops/developer/guides/web-solana) covers the rest of the Solana payload.

## Go live

Switch every network value to mainnet at the same time. MoneyGram picks the network from your key, and a key sent to the other environment's API returns `401`.

| Setting | Testnet | Mainnet |
| - | - | - |
| MoneyGram API URL | `https://playground.xramps.moneygram.com/api` | `https://xramps.moneygram.com/api` |
| MoneyGram secret key | `ramps_sk_sbox_...` | `ramps_sk_prod_...` |
| `requiredNetwork` check in `onSignTransaction` | `testnet` | `mainnet` |
| Stellar passphrase | `Networks.TESTNET` | `Networks.PUBLIC` |
| Horizon URL | `https://horizon-testnet.stellar.org` | `https://horizon.stellar.org` |
| Stellar USDC issuer | `GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5` | `GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN` |
| Solana RPC URL | `https://api.devnet.solana.com` | Your mainnet RPC URL |

To get production keys, complete these steps with MoneyGram:

1. Complete KYB in the partner portal.
2. Submit verified Playground transaction IDs for MoneyGram's integration tests.
3. Configure your product fees.
4. Request production activation.

MoneyGram's [production checklist](https://xramps.moneygram.com/ops/developer/reference/troubleshooting) covers the rest, such as CORS limited to your production domain and session tokens kept out of logs.

## Keep building with Para

Use the same signed-in Para wallets for other actions on each chain:

<picture>![Send Stellar tokens](/images/v3/cards/sdk-stellar-send-tokens.png)</picture>

## [Send Stellar tokens](/v3/react/guides/web3-operations/stellar/send-tokens)

Send XLM and Stellar assets with the Para Stellar signer.

<picture>![Send Solana tokens](/images/v3/cards/sdk-solana-send-tokens.png)</picture>

## [Send Solana tokens](/v3/react/guides/web3-operations/solana/send-tokens)

Move SPL tokens with a Para-signed Solana transaction.

<picture>![Verify sessions on your server](/images/v3/cards/sdk-transfer-session.png)</picture>

## [Verify sessions on your server](/v3/server/guides/sessions)

Confirm a user's Para session before your server acts for them.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.