Skip to content
← All recipes

Build a Trading App

≈ 1–2 days to first settled trade

The end-to-end product loop behind a Robinhood-style app: isolated sub-accounts per user, KYC gating, funded wallets, the two-step quote → trade flow, and settlement pushed to you over webhooks. The whole sequence works in sandbox before you touch production.

The flow

StepEndpoint / EventWhat it does
1POST /usersCreate an isolated sub-account (wallet + order book) per end user. Your uid is the idempotency key.
2POST /users/{id}/kycAssert KYC from your own verification flow. Trading is blocked until VERIFIED (403 KYC_REQUIRED).
3POST /users/{id}/depositMove USD from your master wallet into the sub-account. Always pass Idempotency-Key.
4GET /quote/{symbol}Required step 1 of every trade: firm fee breakdown + a single-use quoteId (60 s TTL).
5POST /users/{id}/tradePlace the order with the quoteId. 202 — status PENDING, cost + fees escrowed.
6Webhook: order.filled / order.rejectedSettlement pushed to you. Prefer this over polling GET /orders/{id}.
7GET /users/{id}/portfolioShow updated holdings and unrealized P&L after settlement.
8GET /users/{id}/buying-powerShow tradable vs withdrawable cash (unsettled SELL proceeds are excluded from withdrawable).

Implementation

javascript
const BASE = 'https://mystocks.africa/api/v1/partner';
const KEY  = process.env.MYSTOCKS_API_KEY;
const h    = { 'x-api-key': KEY, 'Content-Type': 'application/json' };

// 1. Create sub-account (uid doubles as the idempotency key)
const { subAccountId } = await fetch(`${BASE}/users`, {
  method: 'POST', headers: h,
  body: JSON.stringify({ externalId: 'user_42', displayName: 'Amara Diallo', email: 'amara@example.com' }),
}).then(r => r.json());

// 2. Assert KYC after your own verification flow
await fetch(`${BASE}/users/${subAccountId}/kyc`, {
  method: 'POST', headers: { ...h, 'Idempotency-Key': `kyc_user42_${Date.now()}` },
  body: JSON.stringify({ status: 'VERIFIED', level: 'FULL', provider: 'Sumsub', reference: 'sum_ref_123' }),
});

// 3. Deposit $100 (idempotent — safe to retry with the SAME key)
await fetch(`${BASE}/users/${subAccountId}/deposit`, {
  method: 'POST', headers: { ...h, 'Idempotency-Key': 'dep_user42_2026-07-10' },
  body: JSON.stringify({ amount: 100, localAmount: 13100, localCurrency: 'KES', fxRate: 131 }),
});

// 4. Get a firm quote — REQUIRED before every trade (single-use, 60 s TTL)
const quote = await fetch(
  `${BASE}/quote/SCOM.KE?type=BUY&cashValue=50&subAccountId=${subAccountId}`, { headers: h }
).then(r => r.json());
// quote = { quoteId, grossValue, baseFee, partnerMarkupFee, totalCost, quoteExpiresAt, ... }

// 5. Place the BUY with the quoteId
const { orderId } = await fetch(`${BASE}/users/${subAccountId}/trade`, {
  method: 'POST', headers: { ...h, 'Idempotency-Key': 'trade_user42_dca_2026-07-10' },
  body: JSON.stringify({ symbol: 'SCOM.KE', type: 'BUY', cashValue: 50, quoteId: quote.quoteId }),
}).then(r => r.json());
// 202 Accepted — order is PENDING, $50 + fees escrowed.

// 6. Settlement arrives on your webhook (order.filled / order.rejected) —
//    see the "Handle Webhooks Reliably" recipe for the handler.

// 7. After order.filled: show the portfolio
const portfolio = await fetch(`${BASE}/users/${subAccountId}/portfolio`, { headers: h }).then(r => r.json());

// 8. Show tradable vs withdrawable cash
const bp = await fetch(`${BASE}/users/${subAccountId}/buying-power`, { headers: h }).then(r => r.json());
// bp = { cashBalanceUsd, buyingPowerUsd, unsettledUsd, withdrawableUsd, settlements: [...] }

Common mistakes

  • Trading without a quoteId. Production AND sandbox require the two-step quote → trade flow — a missing, expired (60 s), or reused quoteId is rejected. Fetch a fresh quote and retry.
  • Generating a new Idempotency-Key when retrying a failed call. The key must be stable per logical operation — a fresh key on retry is how double-buys happen.
  • Polling order status instead of registering a webhook. Settlement can take up to 4 business hours in production; order.filled tells you the moment it lands.
  • Skipping KYC assertion. Any trade or money movement on a non-VERIFIED sub-account returns 403 KYC_REQUIRED — assert it right after your own onboarding succeeds.
  • Showing cashBalanceUsd as withdrawable. SELL proceeds stay unsettled for T+N days; use withdrawableUsd from /buying-power for the withdraw screen.

Prove it in sandbox first

  • 1.Register at POST /api/sandbox/v1/register to get an sk_sandbox_ key — sandbox accounts start with $100,000 virtual USD.
  • 2.Run steps 1–5 against https://mystocks.africa/api/sandbox/v1/partner — same shapes, instant fills (no dealing desk).
  • 3.Register a webhook in sandbox and confirm you receive order.filled for the instant fill.
  • 4.When you request a live key, replay the exact same code against /api/v1/partner — only the base URL and key change.