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:
| # | Value | How to get it | Host |
|---|---|---|---|
| 1 | merchant_id | Register a merchant account → data.id | widget-backend.seevcash.com |
| 2 | api_key (sv_public_...) + sv_secret_... | Create a merchant API key | widget-backend.seevcash.com |
| 3 | user_id | Create an ACCEPTED SEP-12 customer | widget-backend.seevcash.com or anchor |
| 4 | session_id + session_token | Create a widget session for that customer | widget-backend.seevcash.com |
| 5 | SEP-10 JWT | Authenticate your Stellar keypair with the anchor | anchor.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-6 | SEP-24 | |
|---|---|---|
| UI | Wallet handles all UI | Anchor provides popup/webview |
| KYC | Wallet submits KYC via SEP-12 API | Anchor collects in hosted form |
| Best for | API integrations, automated systems | Simple wallets, MoneyGram flows |
| Complexity | More code, full control | Less 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.
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /info | No | Service info and supported assets |
POST | /transactions/deposit/interactive | JWT | Start interactive deposit |
POST | /transactions/withdraw/interactive | JWT | Start interactive withdrawal |
GET | /transaction | JWT | Get transaction status |
GET | /transactions | JWT | List transaction history |
GET | /sep24/transaction/more_info | No | Human-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.
| Field | Type | Required | Description |
|---|---|---|---|
asset_code | string | ✅ Required | Always "USDC" |
merchant_id | string | ✅ Required | Your merchant ID from registration |
session_id | string | ✅ Required | From POST /api/v1/widget/session |
session_token | string | ✅ Required | From POST /api/v1/widget/session |
api_key | string | ✅ Required | Your public API key (sv_public_...) |
user_id | string | ✅ Required | SEP-12 customer ID (must be ACCEPTED) |
account | string | Optional | Stellar public key to credit/debit. Defaults to the SEP-10 JWT's account |
amount | string | Optional | Amount (fiat for deposit, USDC for withdraw). If omitted, the user enters it in the interactive UI |
collection_mode | string | Optional | "auto" (default) or "manual" — see below |
kyc_override | boolean | Optional | Who hosts the KYC step — see below |
quote_id | string | Optional | From a SEP-38 quote (for a guaranteed rate) |
lang | string | Optional | Language 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.
| Value | Behavior |
|---|---|
"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.
| Value | Returned url points to | Meaning |
|---|---|---|
false (default) | The anchor's hosted KYC page | The anchor collects KYC |
true | The interactive widget page | The 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:
- KYC information (name, phone, ID) — unless handled externally via
kyc_override - Mobile money details (phone number, provider)
- 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:
- Enters the amount of USDC to withdraw
- Provides mobile money payout details
- 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| Status | Meaning |
|---|---|
incomplete | Interactive UI not yet completed |
pending_user_transfer_start | Deposit: awaiting MoMo approval. Withdraw: awaiting USDC transfer |
pending_anchor | Fiat received, preparing crypto delivery |
pending_stellar | USDC sent, awaiting blockchain confirmation |
pending_external | USDC received, initiating fiat payout |
pending_payout_confirmation | Mobile Money disbursement in progress |
completed | Transaction finished successfully |
failed | Something went wrong (see message) |
expired | Customer 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
| Feature | SEP-6 (Programmatic) | SEP-24 (Interactive) |
|---|---|---|
| KYC collection | Wallet → SEP-12 API | Anchor's hosted form |
| Payment details | Wallet shows instructions | Anchor's hosted form |
| Mobile money trigger | Anchor auto-collects (auto mode) | Anchor handles in form |
| Integration effort | More code | Less code (just open URL) |
| User experience control | Wallet controls UX | Anchor controls UX |
| MoneyGram support | ❌ | ✅ |
Related
- Deposit via SEP-6 — Programmatic deposit without interactive UI
- Withdrawal via SEP-6 — Programmatic withdrawal
- Authentication — Required before starting any flow
- KYC (SEP-12) — Produce the
ACCEPTEDcustomer ID used asuser_id - Stellar SEP-24 Specification