≈ 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
| Step | Endpoint / Event | What it does |
|---|---|---|
| 1 | POST /users | Create an isolated sub-account (wallet + order book) per end user. Your uid is the idempotency key. |
| 2 | POST /users/{id}/kyc | Assert KYC from your own verification flow. Trading is blocked until VERIFIED (403 KYC_REQUIRED). |
| 3 | POST /users/{id}/deposit | Move USD from your master wallet into the sub-account. Always pass Idempotency-Key. |
| 4 | GET /quote/{symbol} | Required step 1 of every trade: firm fee breakdown + a single-use quoteId (60 s TTL). |
| 5 | POST /users/{id}/trade | Place the order with the quoteId. 202 — status PENDING, cost + fees escrowed. |
| 6 | Webhook: order.filled / order.rejected | Settlement pushed to you. Prefer this over polling GET /orders/{id}. |
| 7 | GET /users/{id}/portfolio | Show updated holdings and unrealized P&L after settlement. |
| 8 | GET /users/{id}/buying-power | Show tradable vs withdrawable cash (unsettled SELL proceeds are excluded from withdrawable). |
Implementation
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
Prove it in sandbox first