SEP-6: Withdraw (USDC → GHS)
Off-ramp USDC to Ghana Cedis via Mobile Money using the SEP-6 withdrawal API.
A withdrawal converts USDC on the Stellar network to GHS (Ghana Cedis) paid out to a Mobile Money wallet. The customer sends USDC to the anchor's Stellar address and receives GHS in their mobile wallet.
Flow overview
Client Anchor Stellar MoMo Gateway
│ │ │ │
│ GET /sep6/withdraw │ │ │
│────────────────────────►│ │ │
│ { account_id, memo } │ │ │
│◄────────────────────────│ │ │
│ │ │ │
│ Send USDC + memo │ │ │
│────────────────────────────────────────────────►►│ │
│ │ │ │
│ │ PaymentMonitor detects │ │
│ │◄────────────────────────│ │
│ │ │ │
│ │ Disburse GHS │ │
│ │────────────────────────────────────────►►│
│ │ │ │
│ on_change_callback │ │ │
│◄────────────────────────│ │ │Prerequisites
- Valid SEP-10 JWT token (Authentication)
- KYC status is
ACCEPTED(KYC) - USDC balance in your Stellar account
Step 1: Initiate a withdrawal
curl -X GET "https://<your-anchor-domain>/sep6/withdraw?\
asset_code=USDC&\
amount=6.5&\
account=GCEXAMPLE4KEYPAIR7HERE2REPLACE5WITH5YOUR5ACTUAL5STELLAR5KEY&\
on_change_callback=https://yourapp.com/webhooks/anchor" \
-H "Authorization: Bearer <sep10_jwt_token>"import { Wallet, Keypair } from '@stellar/typescript-wallet-sdk';
const wallet = Wallet.TestNet();
const ANCHOR_HOME_DOMAIN = '<your-anchor-domain>';
const anchor = wallet.anchor({ homeDomain: ANCHOR_HOME_DOMAIN });
const accountKp = Keypair.fromSecret('SCZANGBA5YHTNYVVV3C7CAZMCLXPJLNS2YFGXDNASLFTGBPFMRKCY6ML');
// Authenticate
const sep10 = await anchor.sep10();
const authToken = await sep10.authenticate({ accountKp });
// Initiate withdrawal
const sep6 = anchor.sep6();
const withdrawal = await sep6.withdraw({
authToken,
params: {
asset_code: 'USDC',
account: accountKp.publicKey,
amount: '6.5',
funding_method: 'mobile',
},
});
console.log("Withdrawal ID:", withdrawal.id);
console.log("Send USDC to:", withdrawal.account_id);
console.log("Memo:", withdrawal.memo);Parameters
| Parameter | Required | Description |
|---|---|---|
asset_code | Yes | Asset to withdraw — USDC |
amount | No | Amount of USDC to send |
account | Yes | Your Stellar public key |
on_change_callback | No | URL to receive transaction status updates |
Response:
{
"id": "txn_9a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
"account_id": "GANCHOR5RECEIVING5ADDRESS5HERE2REPLACE5WITH5ACTUAL5ANCHOR5KEY",
"memo_type": "text",
"memo": "SEEV-WDR-9A4B5C",
"eta": 300,
"min_amount": "1",
"max_amount": "10000"
}The response includes account_id (the anchor's Stellar address) and a memo. You must include this exact memo when sending USDC — it's how the anchor identifies your transaction.
Step 2: Send USDC to the anchor
Send the USDC payment to the anchor's Stellar address with the provided memo.
import { Wallet, Keypair } from '@stellar/typescript-wallet-sdk';
import {
Asset,
TransactionBuilder,
Networks,
Horizon,
Operation,
Memo,
} from '@stellar/stellar-sdk';
const USDC_ISSUER = 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5';
const server = new Horizon.Server('https://horizon-testnet.stellar.org');
async function sendUsdcToAnchor(
accountKp: Keypair,
anchorAddress: string,
memo: string,
amount: string
) {
const account = await server.loadAccount(accountKp.publicKey);
const usdc = new Asset('USDC', USDC_ISSUER);
const tx = new TransactionBuilder(account, {
fee: '100',
networkPassphrase: Networks.TESTNET,
})
.addOperation(
Operation.payment({
destination: anchorAddress,
asset: usdc,
amount, // e.g. "6.5"
})
)
.addMemo(Memo.text(memo))
.setTimeout(30)
.build();
tx.sign(accountKp);
const result = await server.submitTransaction(tx);
console.log("Payment submitted:", result.hash);
return result;
}Step 3: PaymentMonitor detects payment
Once the anchor's PaymentMonitor service detects the incoming USDC payment with the matching memo, it:
- Validates the payment amount and memo
- Looks up the associated withdrawal transaction
- Calculates the GHS equivalent at the current rate
- Initiates the Mobile Money disbursement
This happens automatically — no additional API calls are needed from your side.
Step 4: Auto payout to Mobile Money
The anchor disburses GHS to the mobile number registered in the customer's KYC data. The payout is automatic once the USDC payment is confirmed.
Payout timing:
- MTN Mobile Money: 1–3 minutes
- Vodafone Cash: 1–3 minutes
- AirtelTigo Money: 1–5 minutes
Step 5: Confirm withdrawal (optional)
In some cases, the anchor may require explicit confirmation before disbursing. Check the transaction status — if it's pending_user_transfer_complete, confirm the withdrawal:
curl -X POST "https://<your-anchor-domain>/sep6/confirm-withdrawal" \
-H "Authorization: Bearer <sep10_jwt_token>" \
-H "Content-Type: application/json" \
-d '{
"id": "txn_9a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d"
}'Response (200):
{
"transaction": {
"id": "txn_9a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
"status": "pending_anchor"
}
}Poll transaction status
curl -X GET "https://<your-anchor-domain>/sep6/transaction?id=txn_9a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d" \
-H "Authorization: Bearer <sep10_jwt_token>"import { Wallet, Keypair } from '@stellar/typescript-wallet-sdk';
const wallet = Wallet.TestNet();
const anchor = wallet.anchor({ homeDomain: '<your-anchor-domain>' });
const accountKp = Keypair.fromSecret('SCZANGBA5YHTNYVVV3C7CAZMCLXPJLNS2YFGXDNASLFTGBPFMRKCY6ML');
const sep10 = await anchor.sep10();
const authToken = await sep10.authenticate({ accountKp });
// Use the watcher for real-time withdrawal tracking
const sep6 = anchor.sep6();
const watcher = sep6.watcher();
const { stop, refresh } = watcher.watchOneTransaction({
authToken,
assetCode: 'USDC',
id: 'txn_9a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d',
onMessage: (txn) => console.log(`Status: ${txn.status}`),
onSuccess: (txn) => {
console.log(`Withdrawal complete! ${txn.amount_out} GHS sent to MoMo`);
stop();
},
onError: (err) => {
console.error('Withdrawal error:', err);
stop();
},
});
// Or get a single transaction directly
const { transaction } = await sep6.getTransactionBy({
authToken,
id: 'txn_9a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d',
});
console.log(`Status: ${transaction.status}`);Response (completed):
{
"transaction": {
"id": "txn_9a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
"kind": "withdrawal",
"status": "completed",
"amount_in": "6.50",
"amount_in_asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
"amount_out": "100.00",
"amount_out_asset": "iso4217:GHS",
"amount_fee": "0.10",
"amount_fee_asset": "stellar:USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
"started_at": "2026-08-05T01:15:00Z",
"completed_at": "2026-08-05T01:18:45Z",
"stellar_transaction_id": "def789abc012...",
"more_info_url": "https://<your-anchor-domain>/more-info/txn_9a4b5c6d"
}
}Withdrawal status flow
pending_user_transfer_start → Waiting for you to send USDC to anchor
pending_anchor → Anchor received USDC, processing GHS payout
pending_external → GHS payout submitted to MoMo network
completed → GHS delivered to customer's mobile wallet
error → Something went wrong (check message)Full TypeScript example
import { Wallet, Keypair } from '@stellar/typescript-wallet-sdk';
import {
Asset,
TransactionBuilder,
Networks,
Horizon,
Operation,
Memo,
} from '@stellar/stellar-sdk';
const wallet = Wallet.TestNet();
const ANCHOR_HOME_DOMAIN = '<your-anchor-domain>';
const anchor = wallet.anchor({ homeDomain: ANCHOR_HOME_DOMAIN });
const accountKp = Keypair.fromSecret('SCZANGBA5YHTNYVVV3C7CAZMCLXPJLNS2YFGXDNASLFTGBPFMRKCY6ML');
const USDC_ISSUER = 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5';
const server = new Horizon.Server('https://horizon-testnet.stellar.org');
// Full withdrawal flow
async function withdraw() {
// Step 1: Authenticate
const sep10 = await anchor.sep10();
const authToken = await sep10.authenticate({ accountKp });
// Step 2: Initiate withdrawal
const sep6 = anchor.sep6();
const withdrawal = await sep6.withdraw({
authToken,
params: {
asset_code: 'USDC',
account: accountKp.publicKey,
amount: '6.5',
funding_method: 'mobile',
},
});
console.log("Withdrawal initiated:", withdrawal.id);
console.log("Send USDC to:", withdrawal.account_id);
console.log("With memo:", withdrawal.memo);
// Step 3: Send USDC to anchor
const account = await server.loadAccount(accountKp.publicKey);
const usdc = new Asset('USDC', USDC_ISSUER);
const tx = new TransactionBuilder(account, {
fee: '100',
networkPassphrase: Networks.TESTNET,
})
.addOperation(
Operation.payment({
destination: withdrawal.account_id,
asset: usdc,
amount: '6.5',
})
)
.addMemo(Memo.text(withdrawal.memo))
.setTimeout(30)
.build();
tx.sign(accountKp);
await server.submitTransaction(tx);
// Step 4: Watch for completion (GHS payout)
const watcher = sep6.watcher();
const { stop } = watcher.watchOneTransaction({
authToken,
assetCode: 'USDC',
id: withdrawal.id,
onMessage: (txn) => console.log(`Status: ${txn.status}`),
onSuccess: (txn) => {
console.log(`Withdrawal complete! ${txn.amount_out} GHS sent to MoMo`);
stop();
},
onError: (err) => {
console.error('Withdrawal error:', err);
stop();
},
});
}
await withdraw();Always include the exact memo returned by the withdraw endpoint. Payments without the correct memo cannot be automatically matched and will require manual intervention.
Error responses
| Status | Error | Meaning |
|---|---|---|
| 400 | invalid_amount | Amount outside min/max range |
| 400 | invalid_asset_code | Asset not supported |
| 403 | kyc_required | KYC not yet approved |
| 404 | transaction_not_found | Transaction ID does not exist |
| 500 | internal_error | Anchor-side error — retry later |