Seev PlusDocs
Docs
Stellar Anchor

SEP-24: Interactive Deposit & Withdrawal

Use the anchor-hosted interactive UI for deposit and withdrawal flows via SEP-24.

SEP-24 provides an interactive, anchor-hosted UI for deposits and withdrawals. Unlike SEP-6 (where the wallet handles all UX), SEP-24 opens a popup or webview where the anchor collects KYC and payment details directly from the user.

SeevCash supports both SEP-6 (programmatic) and SEP-24 (interactive). Choose SEP-6 for API-driven integrations and SEP-24 for simple wallet integrations that want the anchor to handle the UI.

All examples on this page target testnet at https://anchor.seevcash.com. Use test keys and test values only. Never commit a real Stellar secret key — the S... secrets below are throwaway placeholders.

Prerequisites: get your credentials

Before you can call the interactive endpoints, you need five values. They do not all come from the anchor — the merchant, API key, and session values come from the widget backend at https://widget-backend.seevcash.com, while the customer ID comes from KYC. Collect them in this order:

#ValueHow to get itHost
1merchant_idRegister a merchant account → data.idwidget-backend.seevcash.com
2api_key (sv_public_...) + sv_secret_...Create a merchant API keywidget-backend.seevcash.com
3user_idCreate an ACCEPTED SEP-12 customerwidget-backend.seevcash.com or anchor
4session_id + session_tokenCreate a widget session for that customerwidget-backend.seevcash.com
5SEP-10 JWTAuthenticate your Stellar keypair with the anchoranchor.seevcash.com

The api_key here is a merchant/widget key (sv_public_... / sv_secret_...) created at widget-backend.seevcash.com/api/v1/merchant/apikeys. It is not the Checkout/SeevPlus key described in Developer API Keys — those are a different system on a different host and the anchor will reject them. Use the public key (sv_public_...) in the request body api_key field; keep the secret key (sv_secret_...) server-side for the X-API-Key header on widget-backend calls.

Step 1 — Register and get merchant_id

curl -X POST 'https://widget-backend.seevcash.com/api/v1/merchant/register' \
  --header 'Content-Type: application/json' \
  --data '{
    "name": "Acme Payments",
    "legal_name": "Acme Payments Ltd.",
    "email": "payments@acmepay.com",
    "password": "<STRONG_PASSWORD>"
  }'

# Response data.id is your merchant_id. Then log in to get a 24h dashboard access_token:
curl -X POST 'https://widget-backend.seevcash.com/api/v1/merchant/login' \
  --header 'Content-Type: application/json' \
  --data '{ "email": "payments@acmepay.com", "password": "<STRONG_PASSWORD>" }'

Step 2 — Create an API key (sv_public_...)

curl -X POST 'https://widget-backend.seevcash.com/api/v1/merchant/apikeys' \
  --header 'Authorization: Bearer <ACCESS_TOKEN>' \
  --header 'Content-Type: application/json' \
  --data '{ "key_type": "public", "domains": ["http://localhost:3000"] }'

# The returned key (shown once) is your sv_public_... api_key.
# Create a second key with "key_type": "secret" for the X-API-Key header below.

Step 3 — Create an ACCEPTED customer (user_id)

The user_id must be a verified SEP-12 customer, not an arbitrary internal ID. See KYC (SEP-12) for the full flow. The resulting customer id is your user_id.

Step 4 — Create a widget session (session_id, session_token)

curl -X POST 'https://widget-backend.seevcash.com/api/v1/widget/session' \
  --header 'X-API-Key: sv_secret_<your_secret_key>' \
  --header 'Content-Type: application/json' \
  --data '{ "user_id": "<ACCEPTED_SEP12_CUSTOMER_ID>" }'

# Response: { "data": { "session_id": "...", "session_token": "...", "merchant_id": "...", "expires_at": "..." } }

Sessions expire after 30 minutes — mint a fresh one per flow.

Step 5 — Authenticate with the anchor (SEP-10 JWT)

Sign a SEP-10 challenge with your Stellar keypair to get the JWT used as Authorization: Bearer on the anchor calls below. See Authentication (SEP-10).

Steps 1–4 produce the merchant/session fields that a production integration must send on every request. While experimenting against the testnet host you may be able to call the interactive endpoints with only the SEP-10 JWT (step 5), but build with all five fields from the start so your integration works unchanged in production.

When to use SEP-24 vs SEP-6

SEP-6SEP-24
UIWallet handles all UIAnchor provides popup/webview
KYCWallet submits KYC via SEP-12 APIAnchor collects in hosted form
Best forAPI integrations, automated systemsSimple wallets, MoneyGram flows
ComplexityMore code, full controlLess code, less control

Base URL and endpoints

The base URL is https://anchor.seevcash.com. SEP-24 endpoints are served at the root of this host — there is no /sep24 path prefix. Wallets that read the stellar.toml pick this up automatically from TRANSFER_SERVER_SEP0024.

MethodPathAuthDescription
GET/infoNoService info and supported assets
POST/transactions/deposit/interactiveJWTStart interactive deposit
POST/transactions/withdraw/interactiveJWTStart interactive withdrawal
GET/transactionJWTGet transaction status
GET/transactionsJWTList transaction history
GET/sep24/transaction/more_infoNoHuman-readable transaction summary (HTML)

Required request fields

In addition to the standard SEP-24 fields, the SeevCash anchor requires a set of merchant/widget session fields on the deposit and withdraw interactive endpoints. These identify the merchant and the KYC-verified customer behind the request. A production integration must send all five on every deposit and withdraw request.

FieldTypeRequiredDescription
asset_codestring✅ RequiredAlways "USDC"
merchant_idstring✅ RequiredYour merchant ID from registration
session_idstring✅ RequiredFrom POST /api/v1/widget/session
session_tokenstring✅ RequiredFrom POST /api/v1/widget/session
api_keystring✅ RequiredYour public API key (sv_public_...)
user_idstring✅ RequiredSEP-12 customer ID (must be ACCEPTED)
accountstringOptionalStellar public key to credit/debit. Defaults to the SEP-10 JWT's account
amountstringOptionalAmount (fiat for deposit, USDC for withdraw). If omitted, the user enters it in the interactive UI
collection_modestringOptional"auto" (default) or "manual" — see below
kyc_overridebooleanOptionalWho hosts the KYC step — see below
quote_idstringOptionalFrom a SEP-38 quote (for a guaranteed rate)
langstringOptionalLanguage code (default "en")

The five merchant/session fields (merchant_id, session_id, session_token, api_key, user_id) are required for a production integration. Omitting any one returns 400 Bad Request (e.g. api_key is required), and no transaction is created. Send them on every deposit and withdraw interactive request.

While experimenting against the testnet host, these fields may be accepted when absent — but do not rely on that. Build and test your integration with all five present from the start so it behaves identically in production.

Getting the session fields

The session fields come from the widget backend, not the anchor. Create a short-lived session (valid 30 minutes) with a KYC-verified customer ID:

curl -X POST 'https://widget-backend.seevcash.com/api/v1/widget/session' \
  --header 'X-API-Key: sv_secret_<your_key>' \
  --header 'Content-Type: application/json' \
  --data '{ "user_id": "<ACCEPTED_SEP12_CUSTOMER_ID>" }'

# Response:
# { "data": { "session_id": "...", "session_token": "...", "merchant_id": "...", "expires_at": "..." } }

The user_id must be an ACCEPTED SEP-12 customer ID (see KYC), not an arbitrary internal identifier. The anchor treats session_id/session_token as opaque, widget-issued credentials — mint a fresh session per flow rather than reusing an expired one.

collection_mode — who delivers the fiat payout

collection_mode controls who moves the off-chain fiat for a transaction.

ValueBehavior
"auto" (default)The anchor disburses the Mobile Money payout itself
"manual"The anchor stands down; your backend owns the payout and notifies the anchor via confirm-withdrawal

If you omit it, the anchor pays out automatically. The SeevCash widget always sends "manual" so payout ownership lives in the widget — mirror that only if your backend performs the disbursement.

kyc_override — who hosts the KYC step

kyc_override changes which page the returned url points to.

ValueReturned url points toMeaning
false (default)The anchor's hosted KYC pageThe anchor collects KYC
trueThe interactive widget pageThe merchant/widget has already handled KYC

When kyc_override is true, the anchor assumes KYC is satisfied externally (the customer is an ACCEPTED SEP-12 record) and returns the interactive widget URL directly.

Flow overview

Wallet                    Anchor                     User
  │                         │                         │
  │  POST /transactions/deposit/interactive           │
  │────────────────────────►│                         │
  │  { url, id }            │                         │
  │◄────────────────────────│                         │
  │                         │                         │
  │  Open URL in popup      │                         │
  │─────────────────────────────────────────────────►│
  │                         │                         │
  │                         │  User fills KYC form    │
  │                         │◄────────────────────────│
  │                         │  User enters MoMo info  │
  │                         │◄────────────────────────│
  │                         │                         │
  │  Poll GET /transaction  │                         │
  │────────────────────────►│                         │
  │  { status: completed }  │                         │
  │◄────────────────────────│                         │

Interactive deposit (GHS → USDC)

Step 1: Start the interactive flow

curl -X POST "https://anchor.seevcash.com/transactions/deposit/interactive" \
  -H "Authorization: Bearer <sep10_jwt>" \
  -H "Content-Type: application/json" \
  -d '{
    "asset_code": "USDC",
    "account": "GA2E5D24D2LCGVA3SVVKDP64OTN2M2NLUWRDYINB352HW35ABZ66MPVR",
    "merchant_id": "<MERCHANT_ID>",
    "session_id": "<SESSION_ID>",
    "session_token": "<SESSION_TOKEN>",
    "api_key": "sv_public_<YOUR_PUBLIC_KEY>",
    "user_id": "<ACCEPTED_SEP12_CUSTOMER_ID>"
  }'
import { Wallet, SigningKeypair, StellarConfiguration } from '@stellar/typescript-wallet-sdk';

const ANCHOR_HOME_DOMAIN = 'anchor.seevcash.com';
const wallet = new Wallet({ stellarConfiguration: StellarConfiguration.TestNet() });
const anchor = wallet.anchor({ homeDomain: ANCHOR_HOME_DOMAIN });

// Throwaway test secret — replace with your own, never commit a real secret.
const accountKp = SigningKeypair.fromSecret('SCZANGBA5YHTNYVVV3C7CAZMCLXPJLNS2YFGXDNASLFTGBPFMRKCY6ML');

// Authenticate (SEP-10)
const sep10 = await anchor.sep10();
const authToken = await sep10.authenticate({ accountKp });

// Start interactive deposit (SEP-24) with the merchant/session fields
const sep24 = anchor.sep24();
const { url, id } = await sep24.deposit({
  assetCode: 'USDC',
  authToken,
  destinationAccount: accountKp.publicKey,
  extraFields: {
    merchant_id: '<MERCHANT_ID>',
    session_id: '<SESSION_ID>',
    session_token: '<SESSION_TOKEN>',
    api_key: 'sv_public_<YOUR_PUBLIC_KEY>',
    user_id: '<ACCEPTED_SEP12_CUSTOMER_ID>',
  },
});

// Open url in popup/webview for the user to complete
console.log("Interactive URL:", url);
console.log("Transaction ID:", id);

Response:

{
  "type": "interactive_customer_info_needed",
  "url": "https://exchange.seevcash.com/interactive?token=<JWT>",
  "id": "txn-456"
}

The url host depends on kyc_override (see above): with kyc_override: true it points to the interactive widget page; with the default false it points to the anchor's hosted KYC page. The session context is carried inside the token JWT.

Step 2: Open the URL

The wallet opens the returned url in a popup, iframe, or webview. The user completes:

  1. KYC information (name, phone, ID) — unless handled externally via kyc_override
  2. Mobile money details (phone number, provider)
  3. Amount confirmation

Step 3: Poll for completion

curl -X GET "https://anchor.seevcash.com/transaction?id=txn-456" \
  -H "Authorization: Bearer <sep10_jwt>"
const sep24 = anchor.sep24();
const { transaction } = await sep24.getTransactionBy({
  authToken,
  id: 'txn-456',
});
console.log('Status:', transaction.status);

Response (in progress):

{
  "transaction": {
    "id": "txn-456",
    "kind": "deposit",
    "status": "pending_user_transfer_start",
    "more_info_url": "https://anchor.seevcash.com/sep24/transaction/more_info?id=txn-456"
  }
}

Response (completed):

{
  "transaction": {
    "id": "txn-456",
    "kind": "deposit",
    "status": "completed",
    "amount_in": "100.00",
    "amount_out": "6.50",
    "stellar_transaction_id": "abc123def...",
    "completed_at": "2026-08-05T01:00:00Z"
  }
}

Interactive withdrawal (USDC → GHS)

Step 1: Start the interactive flow

curl -X POST "https://anchor.seevcash.com/transactions/withdraw/interactive" \
  -H "Authorization: Bearer <sep10_jwt>" \
  -H "Content-Type: application/json" \
  -d '{
    "asset_code": "USDC",
    "account": "GA2E5D24D2LCGVA3SVVKDP64OTN2M2NLUWRDYINB352HW35ABZ66MPVR",
    "merchant_id": "<MERCHANT_ID>",
    "session_id": "<SESSION_ID>",
    "session_token": "<SESSION_TOKEN>",
    "api_key": "sv_public_<YOUR_PUBLIC_KEY>",
    "user_id": "<ACCEPTED_SEP12_CUSTOMER_ID>"
  }'
import { Wallet, SigningKeypair, StellarConfiguration } from '@stellar/typescript-wallet-sdk';

const ANCHOR_HOME_DOMAIN = 'anchor.seevcash.com';
const wallet = new Wallet({ stellarConfiguration: StellarConfiguration.TestNet() });
const anchor = wallet.anchor({ homeDomain: ANCHOR_HOME_DOMAIN });

// Throwaway test secret — replace with your own, never commit a real secret.
const accountKp = SigningKeypair.fromSecret('SCZANGBA5YHTNYVVV3C7CAZMCLXPJLNS2YFGXDNASLFTGBPFMRKCY6ML');

// Authenticate (SEP-10)
const sep10 = await anchor.sep10();
const authToken = await sep10.authenticate({ accountKp });

// Start interactive withdrawal (SEP-24) with the merchant/session fields
const sep24 = anchor.sep24();
const { url, id } = await sep24.withdraw({
  assetCode: 'USDC',
  authToken,
  extraFields: {
    merchant_id: '<MERCHANT_ID>',
    session_id: '<SESSION_ID>',
    session_token: '<SESSION_TOKEN>',
    api_key: 'sv_public_<YOUR_PUBLIC_KEY>',
    user_id: '<ACCEPTED_SEP12_CUSTOMER_ID>',
  },
});

console.log("Interactive URL:", url);
console.log("Transaction ID:", id);

Response:

{
  "type": "interactive_customer_info_needed",
  "url": "https://exchange.seevcash.com/interactive?token=<JWT>",
  "id": "txn-789"
}

Step 2: User completes the form

In the interactive UI, the user:

  1. Enters the amount of USDC to withdraw
  2. Provides mobile money payout details
  3. Confirms the exchange rate

Step 3: Send USDC

After the interactive form is complete, the transaction status changes to pending_user_transfer_start. The wallet must send USDC to the anchor at withdraw_anchor_account with the withdraw_memo from the transaction:

import { Wallet, SigningKeypair, StellarConfiguration } from '@stellar/typescript-wallet-sdk';
import {
  Asset,
  TransactionBuilder,
  Networks,
  Horizon,
  Operation,
  Memo,
} from '@stellar/stellar-sdk';

const ANCHOR_HOME_DOMAIN = 'anchor.seevcash.com';
const wallet = new Wallet({ stellarConfiguration: StellarConfiguration.TestNet() });
const anchor = wallet.anchor({ homeDomain: ANCHOR_HOME_DOMAIN });

// Throwaway test secret — replace with your own, never commit a real secret.
const accountKp = SigningKeypair.fromSecret('SCZANGBA5YHTNYVVV3C7CAZMCLXPJLNS2YFGXDNASLFTGBPFMRKCY6ML');

const sep10 = await anchor.sep10();
const authToken = await sep10.authenticate({ accountKp });

// Get transaction details for withdraw_anchor_account and memo
const sep24 = anchor.sep24();
const { transaction } = await sep24.getTransactionBy({ authToken, id: 'txn-789' });

// Testnet Horizon + testnet network passphrase — keep these in sync with the Wallet network above.
const server = new Horizon.Server('https://horizon-testnet.stellar.org');
const account = await server.loadAccount(accountKp.publicKey);
const tx = new TransactionBuilder(account, {
  fee: '100',
  networkPassphrase: Networks.TESTNET,
})
  .addOperation(Operation.payment({
    destination: transaction.withdraw_anchor_account,
    asset: new Asset('USDC', 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN'),
    amount: transaction.amount_in,
  }))
  .addMemo(Memo.text(transaction.withdraw_memo))
  .setTimeout(300)
  .build();

tx.sign(accountKp);
await server.submitTransaction(tx);

Keep the network consistent end-to-end: a testnet Wallet must use testnet Horizon (https://horizon-testnet.stellar.org) and Networks.TESTNET. The USDC asset issuer also differs between testnet and mainnet — read it from the transaction / stellar.toml rather than hardcoding.

Step 4: Poll for completion

The anchor detects the incoming USDC payment, converts to GHS, and pays out via Mobile Money (when collection_mode is auto).

curl -X GET "https://anchor.seevcash.com/transaction?id=txn-789" \
  -H "Authorization: Bearer <sep10_jwt>"
const sep24 = anchor.sep24();
const { transaction } = await sep24.getTransactionBy({
  authToken,
  id: 'txn-789',
});
console.log('Status:', transaction.status);
{
  "transaction": {
    "id": "txn-789",
    "kind": "withdrawal",
    "status": "completed",
    "amount_in": "50.00",
    "amount_out": "665.00",
    "stellar_transaction_id": "xyz789...",
    "external_transaction_id": "PROV-456",
    "completed_at": "2026-08-05T01:05:00Z"
  }
}

Transaction status flow

Deposit:  incomplete → pending_user_transfer_start → pending_anchor → pending_stellar → completed
Withdraw: incomplete → pending_user_transfer_start → pending_external → pending_payout_confirmation → completed
StatusMeaning
incompleteInteractive UI not yet completed
pending_user_transfer_startDeposit: awaiting MoMo approval. Withdraw: awaiting USDC transfer
pending_anchorFiat received, preparing crypto delivery
pending_stellarUSDC sent, awaiting blockchain confirmation
pending_externalUSDC received, initiating fiat payout
pending_payout_confirmationMobile Money disbursement in progress
completedTransaction finished successfully
failedSomething went wrong (see message)
expiredCustomer didn't complete within the window

TypeScript integration (full example)

import { Wallet, SigningKeypair, StellarConfiguration } from '@stellar/typescript-wallet-sdk';

const ANCHOR_HOME_DOMAIN = 'anchor.seevcash.com';
const wallet = new Wallet({ stellarConfiguration: StellarConfiguration.TestNet() });
const anchor = wallet.anchor({ homeDomain: ANCHOR_HOME_DOMAIN });

// Throwaway test secret — replace with your own, never commit a real secret.
const accountKp = SigningKeypair.fromSecret('SCZANGBA5YHTNYVVV3C7CAZMCLXPJLNS2YFGXDNASLFTGBPFMRKCY6ML');

// 1. Authenticate (SEP-10)
const sep10 = await anchor.sep10();
const authToken = await sep10.authenticate({ accountKp });

// 2. Start interactive deposit (SEP-24)
const sep24 = anchor.sep24();
const { url, id } = await sep24.deposit({
  assetCode: 'USDC',
  authToken,
  destinationAccount: accountKp.publicKey,
  extraFields: {
    merchant_id: '<MERCHANT_ID>',
    session_id: '<SESSION_ID>',
    session_token: '<SESSION_TOKEN>',
    api_key: 'sv_public_<YOUR_PUBLIC_KEY>',
    user_id: '<ACCEPTED_SEP12_CUSTOMER_ID>',
  },
});

// 3. Open the URL in a popup/webview
window.open(url, '_blank', 'width=450,height=700');

// 4. Poll for status using the watcher
const watcher = sep24.watcher();
const { stop } = watcher.watchOneTransaction({
  authToken,
  assetCode: 'USDC',
  id,
  onMessage: (txn) => console.log('Status:', txn.status),
  onSuccess: (txn) => {
    console.log('Deposit complete! Stellar TX:', txn.stellar_transaction_id);
    stop();
  },
  onError: (err) => {
    console.error('Error:', err);
    stop();
  },
});

SEP-6 vs SEP-24 comparison

FeatureSEP-6 (Programmatic)SEP-24 (Interactive)
KYC collectionWallet → SEP-12 APIAnchor's hosted form
Payment detailsWallet shows instructionsAnchor's hosted form
Mobile money triggerAnchor auto-collects (auto mode)Anchor handles in form
Integration effortMore codeLess code (just open URL)
User experience controlWallet controls UXAnchor controls UX
MoneyGram support❌✅

On this page