MyStocks Partner API
REST API for integrating African and global stock trading into your application. Use the sandbox environment for testing — all sandbox data is isolated with no real money. When ready, switch to production endpoints with your pk_live_ key.
Sandbox: https://mystocks.africa/api/sandbox/v1/partnerProduction: https://mystocks.africa/api/v1/partnerTry in API TesterQuick Start
5 minGo from zero to your first executed trade in six steps — all against the sandbox, no approval required.
Step 1 — Register and get your API key
curl -X POST https://mystocks.africa/api/sandbox/v1/register \
-H "Content-Type: application/json" \
-d '{ "businessName": "Acme", "email": "dev@acme.com" }'{
"apiKey": "sk_sandbox_xxxxxxxxxxxxxxxx",
"walletBalance": 100000,
"currency": "USD"
}Step 2 — Create a sub-account for your end-user
Each of your end-users gets an isolated wallet. Alternatively use POST /api/sandbox/v1/partner/auto-register (idempotent — safe to call on every login).
curl -X POST https://mystocks.africa/api/sandbox/v1/partner/users \
-H "Authorization: Bearer sk_sandbox_<your_key>" \
-H "Content-Type: application/json" \
-d '{
"externalId": "user_42",
"displayName": "Jane Doe",
"email": "jane@example.com"
}'{
"subAccountId": "usr_xxxxxxxxxxxx",
"externalId": "user_42",
"displayName": "Jane Doe",
"kycStatus": "NONE",
"status": "active",
"wallet": {
"currency": "USD",
"balance": 0
}
}Step 3 — Deposit funds
You collect your user's local-currency payment. Report the USD equivalent and the FX rate you applied.
curl -X POST https://mystocks.africa/api/sandbox/v1/partner/users/usr_xxxxxxxxxxxx/deposit \
-H "Authorization: Bearer sk_sandbox_<your_key>" \
-H "Idempotency-Key: dep_user42_001" \
-H "Content-Type: application/json" \
-d '{
"amount": 500,
"localAmount": 655000,
"localCurrency": "KES",
"fxRate": 1310,
"note": "Mpesa ref mpesa_QHJ29SK"
}'{
"message": "Deposit successful.",
"subAccountId": "usr_xxxxxxxxxxxx",
"amount": 500,
"currency": "USD",
"newSubBalance": 500,
"newMasterBalance": 99500
}Step 4 — Check the stock price
curl "https://mystocks.africa/api/sandbox/v1/partner/stocks/SCOM.KE" \ -H "Authorization: Bearer sk_sandbox_<your_key>"
{
"symbol": "SCOM.KE",
"name": "Safaricom PLC",
"exchange": "NSE",
"currency": "KES",
"price": 16.5,
"usdPrice": 0.0126,
"change": 0.25,
"changePct": 1.54
}Step 5 — Place a trade on behalf of the sub-account
You can use the full exchange-qualified symbol (SCOM.KE) or just the bare ticker (SCOM) — the API resolves it automatically.
curl -X POST https://mystocks.africa/api/sandbox/v1/partner/users/usr_xxxxxxxxxxxx/trade \
-H "Authorization: Bearer sk_sandbox_<your_key>" \
-H "Idempotency-Key: trade_user42_001" \
-H "Content-Type: application/json" \
-d '{
"symbol": "SCOM.KE",
"type": "BUY",
"quantity": 1000
}'{
"status": "FILLED",
"orderId": "ord_xxxxxxxxxxxx",
"subAccountId": "usr_xxxxxxxxxxxx",
"externalId": "user_42",
"type": "BUY",
"symbol": "SCOM.KE",
"quantity": 1000,
"usdPrice": 0.0126,
"localPrice": 16.5,
"currency": "KES",
"gross": 12.6,
"fee": 0.09,
"totalCost": 12.69,
"newSubBalance": 487.31,
"note": "Order filled instantly in sandbox. Production trades are PENDING until admin approval."
}Step 6 — Check order history (or use webhooks)
curl "https://mystocks.africa/api/sandbox/v1/partner/users/usr_xxxxxxxxxxxx/orders?limit=1" \ -H "Authorization: Bearer sk_sandbox_<your_key>"
{
"orders": [
{
"orderId": "ord_xxxxxxxxxxxx",
"status": "FILLED",
"symbol": "SCOM.KE",
"type": "BUY",
"quantity": 1000,
"usdPrice": 0.0126,
"localPrice": 16.5,
"currency": "KES",
"gross": 12.6,
"fee": 0.09,
"totalCost": 12.69,
"filledAt": "2026-05-19T10:15:00Z"
}
],
"count": 1
}In sandbox, sub-account trades settle instantly (FILLED) with no admin queue. In production, status progresses PENDING → COMPLETED (or REJECTED) after admin review during exchange hours.
Settlement timeline — production
| Phase | Timeline | Notes |
|---|---|---|
| MyStocks order review | Within 4 business hours | Orders submitted during market hours. Outside hours → queued for next session. |
| Exchange settlement (most exchanges) | T+3 business days | NSE, NGX, JSE, GSE, BRVM, ZSE, BSE, LUSE — title transfers 3 business days after execution. |
| Exchange settlement (EGX) | T+2 business days | Egyptian Exchange — title transfers 2 business days after execution. |
The MyStocks review window is when your PENDING order becomes COMPLETED — this is what triggers the order.filled webhook and credits shares/proceeds to the wallet. The exchange settlement cycle (T+2/T+3) governs when the underlying title and custody transfer through the exchange depository; it does not affect wallet balances in the API. Full per-exchange settlement data is available at GET /api/v1/partner/market/settlement.
Order lifecycle
Sandbox only: orders skip PENDING/PROCESSING and resolve immediately as FILLED. Cancel is only possible from PENDING — once admin locks the order for review (PROCESSING) the DELETE returns HTTP 409.
Cancelling a PENDING order
While an order is still PENDING, you can cancel it with a DELETE request. For BUY orders, the escrowed funds are refunded atomically. SELL orders carry no wallet impact so cancellation is immediate. Once an order moves to PROCESSING, COMPLETED, or REJECTED it can no longer be cancelled (HTTP 409).
# Cancel a sub-account order curl -X DELETE "https://mystocks.africa/api/v1/partner/users/usr_xxxxxxxxxxxx/orders/ord_xxxxxxxxxxxx" \ -H "Authorization: Bearer pk_live_<your_key>" # Cancel a master-account order curl -X DELETE "https://mystocks.africa/api/v1/partner/orders/ord_xxxxxxxxxxxx" \ -H "Authorization: Bearer pk_live_<your_key>"
A successful cancellation fires an order.cancelled webhook so downstream systems can react without polling.
Skip polling — use webhooks
Register a webhook URL to receive order.filled, order.rejected, and order.cancelled events in real time. See the Webhooks section, and the Going Live checklist when you're ready to switch to production.
Overview
The MyStocks Sandbox API lets you build and test stock trading integrations using realistic data without any financial risk. Every sandbox account starts with $100,000 USD in virtual funds. Trades settle instantly — there's no approval queue.
Instant Settlement
Trades fill immediately with no admin queue.
Full API Parity
Mirrors production endpoints and response shapes.
Isolated Data
Separate Firebase project — zero production risk.
Available Stocks
All stocks listed on MyStocks are available for sandbox trading — NSE, NGX, JSE, GSE, BRVM, LUSE, EGX, BSE and more. Prices are live read-through from production. Use GET /stocks to browse the full catalogue and get exact symbol strings.
| Symbol | Name | Exchange | Currency | Example (live price) |
|---|---|---|---|---|
| SCOM.KE | Safaricom PLC | NSE | KES | live KES price |
| EQTY.KE | Equity Group Holdings | NSE | KES | live KES price |
| DANGCEM.NG | Dangote Cement | NGX | NGN | live NGN price |
| ZENITH.NG | Zenith Bank | NGX | NGN | live NGN price |
| MTN.ZA | MTN Group | JSE | ZAR | live ZAR price |
| GCBBANK.GH | GCB Bank | GSE | GHS | live GHS price |
| SNTS.BF | Sonatel | BRVM | XOF | live XOF price |
| ZANACO.ZM | Zambia National Commercial Bank | LUSE | ZMW | live ZMW price |
This is a sample — hundreds more stocks are available. Call GET /stocks for the full live list.
Use Cases
Three common products you can build with this API. Each guide shows the exact endpoints to chain together — from authentication to a working feature.
Build a Trading App
The core product loop: create user accounts, fund them, let users buy and sell stocks, and receive real-time settlement notifications. This sequence works end-to-end in sandbox before you ever touch production.
| Step | Endpoint | What it does |
|---|---|---|
| 1 | POST /users | Create an isolated sub-account (wallet + order book) for each user. |
| 2 | POST /users/:id/kyc | Assert KYC status from your own verification flow. Required before trading. |
| 3 | POST /users/:id/deposit | Move USD from your master wallet to the sub-account. Pass Idempotency-Key. |
| 4 | GET /market/quotes?symbol=SCOM.KE | Fetch live price + fee quote before showing the trade screen. |
| 5 | POST /users/:id/trade | Place a BUY or SELL order. Responds 202 — order is PENDING, funds escrowed. |
| 6 | GET /users/:id/orders/:orderId | Poll for status changes (PENDING → PROCESSING → COMPLETED). |
| 7 | Webhook: order.filled / order.rejected | Receive settlement notification instead of polling. Recommended over step 6. |
| 8 | GET /users/:id/portfolio | Show updated holdings and unrealised P&L after settlement. |
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
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,
body: JSON.stringify({ status: 'VERIFIED', level: 'FULL', provider: 'Sumsub', reference: 'sum_ref_123' }),
});
// 3. Deposit $100 (idempotent — safe to retry)
await fetch(`${BASE}/users/${subAccountId}/deposit`, {
method: 'POST',
headers: { ...h, 'Idempotency-Key': 'dep_user42_${Date.now()}' },
body: JSON.stringify({ amount: 100, localAmount: 13100, localCurrency: 'KES', fxRate: 131 }),
});
// 4. Get live quote
const { data: quotes } = await fetch(
`${BASE}/market/quotes?symbol=SCOM.KE`, { headers: h }
).then(r => r.json());
const { usdPrice } = quotes[0];
// 5. Place BUY order
const { orderId } = await fetch(`${BASE}/users/${subAccountId}/trade`, {
method: 'POST',
headers: { ...h, 'Idempotency-Key': 'trade_user42_buy_scom_${Date.now()}' },
body: JSON.stringify({ symbol: 'SCOM.KE', type: 'BUY', cashValue: 50 }),
}).then(r => r.json());
// → 202 Accepted. orderId is PENDING, $50 + fee escrowed.
// 6. (Optional) Poll until settled — or use a webhook instead (step 7)
let order;
do {
await new Promise(r => setTimeout(r, 10_000)); // wait 10 s
order = await fetch(`${BASE}/users/${subAccountId}/orders/${orderId}`, { headers: h }).then(r => r.json());
} while (order.status === 'PENDING' || order.status === 'PROCESSING');
// 8. Show portfolio
const portfolio = await fetch(`${BASE}/users/${subAccountId}/portfolio`, { headers: h }).then(r => r.json());
console.log(portfolio.holdings); // [{ symbol: 'SCOM.KE', units: ..., currentValue: ... }]Key things to know
- —Orders are 202 Accepted immediately. Settlement is async (within 4 business hours). In sandbox, orders resolve instantly.
- —KYC must be VERIFIED before a sub-account can trade. Attempting a trade on a KYC_NONE account returns 403 KYC_REQUIRED.
- —Always pass Idempotency-Key on deposit and trade calls — mobile networks drop connections silently.
- —Use order.filled / order.rejected webhooks rather than polling. Register one URL and handle all events there.
Build a Portfolio Tracker
Show your users a live view of their holdings, P&L, and dividend history. Useful for wealth management apps, neobanks showing investment tabs, and B2B reporting dashboards.
| Step | Endpoint | What it does |
|---|---|---|
| 1 | GET /users/:id/portfolio | Live holdings: units, avg cost, current USD price, unrealised P&L. |
| 2 | GET /market/quotes?symbols=A,B,C | Refresh prices for all held symbols in one batch request (max 50). |
| 3 | GET /stocks/:symbol/chart | Load price history chart data for each holding (1W, 1M, 3M, 1Y). |
| 4 | GET /users/:id/dividends | Show dividend income history with per-share amounts and pay dates. |
| 5 | GET /dividends/calendar | Show upcoming dividend declarations for stocks the user holds. |
| 6 | GET /report/aum | Partner-level: aggregate AUM across all sub-accounts (CSV export available). |
| 7 | GET /report/positions | Open positions summary by symbol across all sub-accounts. |
| 8 | Webhook: dividend.paid | Notify users the moment a dividend is credited to their wallet. |
const BASE = 'https://mystocks.africa/api/v1/partner';
const h = { 'x-api-key': process.env.MYSTOCKS_API_KEY };
async function loadPortfolioScreen(subAccountId) {
// 1. Fetch holdings + summary
const { holdings, summary } = await fetch(
`${BASE}/users/${subAccountId}/portfolio`, { headers: h }
).then(r => r.json());
// 2. Refresh prices for all held symbols in one call
const symbols = holdings.map(h => h.symbol).join(',');
const { data: quotes } = await fetch(
`${BASE}/market/quotes?symbols=${symbols}`, { headers: h }
).then(r => r.json());
const priceMap = Object.fromEntries(quotes.map(q => [q.symbol, q.usdPrice]));
// 3. Enrich holdings with live prices
const enriched = holdings.map(holding => ({
...holding,
livePrice: priceMap[holding.symbol] ?? holding.currentUsdPrice,
liveValue: (priceMap[holding.symbol] ?? holding.currentUsdPrice) * holding.units,
}));
// 4. Dividend history
const { dividends } = await fetch(
`${BASE}/users/${subAccountId}/dividends?limit=10`, { headers: h }
).then(r => r.json());
// 5. Upcoming dividends
const { dividends: upcoming } = await fetch(
`${BASE}/dividends/calendar`, { headers: h }
).then(r => r.json());
const eligible = upcoming.filter(d => d.partnerEligible);
return { summary, holdings: enriched, recentDividends: dividends, upcomingDividends: eligible };
}
// Export AUM report to CSV
const csv = await fetch(`${BASE}/report/aum?format=csv`, { headers: h }).then(r => r.text());Key things to know
- —portfolio returns priceIsLive: true when the price was fetched live from the stock doc at query time. Refresh with /market/quotes for the latest tick.
- —Batch /market/quotes accepts up to 50 symbols per request. For large portfolios, chunk your symbol list.
- —dividends/calendar returns partnerEligible: true when at least one of your sub-accounts holds that stock — use this to filter noisy entries.
- —/report/aum and /report/positions support ?format=csv for spreadsheet exports. Use for compliance and reconciliation.
Build a Market Data Widget
Embed a live stock ticker, price charts, or a top-movers board directly in your app — no sub-accounts required. Use a data key (pk_data_) so the credential is safe to ship in browser JavaScript or a React Native bundle.
| Step | Endpoint | What it does |
|---|---|---|
| 1 | POST /api-keys/data-key | Create a read-only pk_data_ key safe to embed in client code. |
| 2 | GET /market/quotes?symbols=A,B,C | Fetch live prices for up to 50 symbols in one call. Poll every 15–30 s. |
| 3 | GET /stocks/:symbol/chart?period=1M | Load pre-shaped chart data (open, close, change %) for a given period. |
| 4 | GET /stocks/:symbol/history?period=1M | Load raw OHLCV candles for custom chart rendering. |
| 5 | GET /market/movers?direction=gainers | Top gainers or losers by exchange. Refresh once per minute. |
| 6 | GET /market/status | Show whether an exchange is open or closed before displaying prices. |
| 7 | GET /companies/:symbol | Company profile, fundamentals, logo URL, and last 20 corporate actions. |
| 8 | GET /market-intel | Editorial market intelligence articles to add context alongside prices. |
// Safe to ship in browser JS — pk_data_ keys are read-only
const DATA_KEY = 'pk_data_xxxxxxxxxxxxxxxx'; // from POST /api-keys/data-key
const BASE = 'https://mystocks.africa/api/v1/partner';
const h = { 'x-api-key': DATA_KEY };
// Ticker widget — poll every 15 s during market hours
async function refreshTicker(symbols) {
const { data } = await fetch(
`${BASE}/market/quotes?symbols=${symbols.join(',')}`, { headers: h }
).then(r => r.json());
return data; // [{ symbol, usdPrice, change, volume, ... }]
}
// Price chart for a single stock
async function loadChart(symbol, period = '1M') {
const { data } = await fetch(
`${BASE}/stocks/${symbol}/chart?period=${period}`, { headers: h }
).then(r => r.json());
return data; // [{ date, close, change, changePercent }]
}
// Top movers board
async function loadMovers(exchange = 'NSE', direction = 'gainers') {
const { data } = await fetch(
`${BASE}/market/movers?exchange=${exchange}&direction=${direction}&limit=5`, { headers: h }
).then(r => r.json());
return data;
}
// Market open/closed status
async function isMarketOpen(exchange = 'NSE') {
const { exchanges } = await fetch(`${BASE}/market/status`, { headers: h }).then(r => r.json());
return exchanges.find(e => e.code === exchange)?.isOpen ?? false;
}
// Company card (logo, description, fundamentals)
async function loadCompanyCard(symbol) {
return fetch(`${BASE}/companies/${symbol}`, { headers: h }).then(r => r.json());
}Key things to know
- —Data keys share the rate-limit bucket of the parent full key. An active widget polling every 15 s uses ~4 req/min — well within starter limits.
- —Prices refresh every 15 minutes during market hours. Polling faster than once per 30 s is unlikely to see new data.
- —market/status returns nextOpen timestamp when an exchange is closed — use it to show a countdown to market open.
- —company logo URLs are returned in GET /companies/:symbol as logoUrl. Cache them client-side — they change rarely.
Authentication
Every request (except POST /register) requires your API key. Send it using either method:
Option A — Authorization header (recommended)
# Sandbox Authorization: Bearer sk_sandbox_<your_key> # Production Authorization: Bearer pk_live_<your_key>
Option B — Custom header
x-api-key: sk_sandbox_<your_key> # or pk_live_<your_key>
Security note
Never expose a full key (pk_live_) in client-side code or public repositories. Use a data key (pk_data_) for any browser or mobile context — see below. Sandbox keys carry no financial risk; production keys control real funds — guard them accordingly.
Data keys — safe for client-side embedding
A pk_data_ key is a scoped, read-only credential derived from your full key. It is safe to ship in browser JavaScript, React Native apps, or any public context because it cannot trade, move funds, or access sub-account PII.
Allowed endpoints (GET only)
Generate a data key via POST /api-keys/data-key. Data keys share the rate-limit bucket of their parent full key. Any call outside the allowed list returns 403 FORBIDDEN with error.code: "FORBIDDEN".
Two endpoints use Firebase ID token auth — not a partner key
GET /api/v1/partner/me and POST /api/v1/partner/api-keys/revoke are authenticated with a Firebase ID token (Authorization: Bearer <firebase-id-token>) obtained from the partner dashboard login session. This is intentional: /me powers the dashboard UI, and /revoke must remain accessible even after a key is compromised. All other routes use your partner API key as normal.
Idempotency — safe retries for money-movement calls
On unstable mobile networks a POST can succeed on the server but time out on the client — causing a double-charge if the app retries. For deposit, withdraw, and trade endpoints, pass a unique Idempotency-Key header. We deduplicate by key for 24 hours and return the cached response on retry.
POST /api/v1/partner/users/{userId}/deposit
Authorization: Bearer pk_live_<your_key>
Idempotency-Key: dep_riven_user_42_1743152580 # unique per attemptUse any unique string — a UUID or your own transaction ID works well. If a concurrent duplicate is detected you receive HTTP 409 until the first request completes.
Key Management
Rotate, revoke, and manage scoped read-only data keys. The revoke endpoint uses a Firebase ID token — not a partner key — so it remains accessible even after a key is compromised.
/api-keys/rotateGenerate a new pk_live_ key. The old key is suspended immediately. All registered webhooks and sub-accounts are automatically migrated to the new key. A key.rotated audit event is logged.
curl -X POST "https://mystocks.africa/api/v1/partner/api-keys/rotate" \ -H "Authorization: Bearer pk_live_<current_key>"
{
"apiKey": "pk_live_new_xxxxxxxxxxxxxxxx",
"createdAt": "2026-06-09T10:00:00Z",
"migrated": {
"webhooks": 3,
"subAccounts": 142,
"deliveries": 0
}
}Important
Update all services with the new key before discarding the response — the old key is suspended immediately and the new key is shown only once. Data keys (pk_data_) are unaffected by rotation.
/api-keys/revokePermanently revoke a partner API key. This action cannot be undone. A new key must be issued by MyStocks operations. This endpoint uses a Firebase ID token (Authorization: Bearer <firebase-id-token>), not a partner key, so it remains callable even after a key compromise. A key.revoked audit event is logged.
curl -X POST "https://mystocks.africa/api/v1/partner/api-keys/revoke" \ -H "Authorization: Bearer <firebase-id-token>"
{
"message": "Key revoked. Contact support@mystocks.africa to issue a replacement.",
"revokedAt": "2026-06-09T10:05:00Z"
}/api-keys/data-key/api-keys/data-key/api-keys/data-keyCreate, fetch, or delete a read-only pk_data_ key safe to embed in browser JavaScript or React Native bundles. Only one data key exists per partner at a time — creating a new one replaces the previous. GET returns a masked version of the key (maskedKey); the full key is only shown on POST. See Authentication for the allowed endpoint list.
curl -X POST "https://mystocks.africa/api/v1/partner/api-keys/data-key" \ -H "Authorization: Bearer pk_live_<key>"
{
"dataKey": "pk_data_xxxxxxxxxxxxxxxx",
"maskedKey": "pk_data_xxxxxxxx...",
"createdAt": "2026-06-09T10:00:00Z"
}Partner Settings
Configure branding (logo URL) and SMTP transactional email. SMTP enables partner-branded emails to your sub-account users. A settings.updated audit event is logged on every PATCH with the list of changed fields.
/settings/settingsFetch or update partner settings. SMTP password is never returned — only a hasPassword: true boolean confirms it is set.
| Field | Type | Required | Description |
|---|---|---|---|
| logoUrl | string | No | HTTPS URL to your company logo (PNG or SVG). Shown in the dashboard and partner-branded emails. |
| smtp.host | string | No | SMTP server hostname. |
| smtp.port | integer | No | SMTP port (e.g. 587 for STARTTLS, 465 for implicit TLS). |
| smtp.user | string | No | SMTP username. |
| smtp.password | string | No | SMTP password. Write-only — never returned in GET responses. |
| smtp.from | string | No | From address for outbound emails, e.g. noreply@acme.com. |
curl "https://mystocks.africa/api/v1/partner/settings" \ -H "Authorization: Bearer pk_live_<key>"
{
"logoUrl": "https://acme.com/logo.svg",
"smtp": {
"host": "smtp.acme.com",
"port": 587,
"user": "noreply@acme.com",
"hasPassword": true,
"from": "noreply@acme.com"
},
"updatedAt": "2026-06-09T10:00:00Z"
}/settings(SMTP test)Send a test email via your configured SMTP server to your registered partner address. Use to validate SMTP credentials before enabling partner-branded emails.
curl -X POST "https://mystocks.africa/api/v1/partner/settings" \ -H "Authorization: Bearer pk_live_<key>"
{
"message": "Test email sent to dev@acme.com via smtp.acme.com:587."
}FX & Currency Model
MyStocks operates a split-responsibility FX model. You own the foreign-exchange relationship with your end-users; MyStocks handles FX only at the point of equity execution.
| Flow | Who handles FX? | What you provide | What MyStocks does |
|---|---|---|---|
| Funding (deposit) | Partner | amount + localAmount + fxRate | Records for audit trail; credits USD wallet |
| Equity trading | MyStocks | — | Applies live spot rate at execution; settles in local market currency |
| Dividends | MyStocks | — | Converts declared dividend to USD at spot; credits sub-account wallet |
| Withdrawal | Partner | amount + fxRate applied | Debits USD wallet; partner pays user in local currency |
Deposit — partner-handled FX example
Your user pays KES 655,000 via M-Pesa. You apply your exchange rate (1 USD = 1,310 KES) and report the USD equivalent to MyStocks. MyStocks credits the sub-account wallet with $500 USD and stores the fxRate for audit purposes.
// Deposit request body
{
"amount": 500,
"localAmount": 655000,
"localCurrency": "KES",
"fxRate": 1310, // KES per USD — your applied rate
"note": "mpesa_QHJ29SK"
}Trading — MyStocks-handled FX
When a BUY order executes on the NSE, MyStocks converts the USD amount to KES at the live interbank spot rate at the moment of execution. The fillPrice in the order response is always expressed in the stock's native currency (e.g. KES for NSE). MyStocks debits the sub-account USD wallet using the spot rate applied.
FX risk
Sub-account wallets are denominated in USD. Exchange rate movements between deposit and trade execution are borne by the end-user. MyStocks does not offer hedging. Ensure your product disclosures reflect this.
ETFs & Price Charts
NewExpand your product offering with dedicated exchange-traded funds (ETFs) listed across African markets (e.g. Satrix on the JSE), and render rich price charts for both Stocks and ETFs.
1. Retrieving ETFs Catalog
Query listed ETFs using the /etfs endpoints. Responses carry comprehensive fund-specific metadata such as expense ratios, asset classes, geographical focus, risk levels, and top holdings.
curl "https://mystocks.africa/api/sandbox/v1/partner/etfs?exchange=JSE" \ -H "Authorization: Bearer sk_sandbox_<your_key>"
{
"etfs": [
{
"id": "STXEME.ZA",
"symbol": "STXEME.ZA",
"name": "Satrix MSCI Emerging Markets ETF",
"exchange": "JSE",
"currency": "ZAR",
"price": 75,
"usdPrice": 4.55,
"assetType": "ETF",
"logoUrl": "https://mystocks.africa/logos/stxeme-za.svg",
"fundMetadata": {
"brand": "Satrix",
"expenseRatio": 0.38,
"geographicalFocus": "Global Emerging Markets",
"indexTracked": "MSCI Emerging Markets Index",
"riskLevel": "HIGH",
"managementStyle": "PASSIVE"
}
}
],
"count": 1
}2. Mapped Price Charts API
To build price history charts without manually calculating date grids, retrieve pre-computed, currency-correct charts via the /chart endpoints. Price values are returned in their native local trading currency to guarantee precise UI alignments, alongside converted auxiliary USD histories.
Stocks Charts
GET /stocks/SCOM.KE/chart?period=1yETFs Charts
GET /etfs/STXEME.ZA/chart?period=3mcurl "https://mystocks.africa/api/sandbox/v1/partner/etfs/STXEME.ZA/chart?period=3m" \ -H "Authorization: Bearer sk_sandbox_<your_key>"
{
"symbol": "STXEME.ZA",
"currency": "ZAR",
"period": "3m",
"labels": [
"2026-03-01",
"2026-04-01",
"2026-05-23"
],
"opens": [
72.8,
73.9,
74.6
],
"highs": [
73.9,
74.5,
75.2
],
"lows": [
72.5,
73.4,
74.3
],
"prices": [
73.5,
74.2,
75
],
"volumes": [
12000,
18500,
24000
],
"priceHistory": [
{
"date": "2026-03-01T00:00:00.000Z",
"open": 72.8,
"high": 73.9,
"low": 72.5,
"price": 73.5,
"close": 73.5,
"usdPrice": 4.45,
"volume": 12000
},
{
"date": "2026-04-01T00:00:00.000Z",
"open": 73.9,
"high": 74.5,
"low": 73.4,
"price": 74.2,
"close": 74.2,
"usdPrice": 4.5,
"volume": 18500
},
{
"date": "2026-05-23T19:59:41.000Z",
"open": 74.6,
"high": 75.2,
"low": 74.3,
"price": 75,
"close": 75,
"usdPrice": 4.55,
"volume": 24000
}
]
}3. High-Performance Multi-Symbol Batch Quotes
When loading multi-asset dashboard watchlists or custom UI cards, avoid running consecutive parallel HTTP connections. Query up to 50 comma-separated stock or ETF symbols in a single roundtrip using ?symbols=A,B,C. Use ?symbol=X&exchange=Y for a single symbol — the response unwraps to a plain object in data and returns 404 if not found. In batch mode, symbols that cannot be resolved are listed in not_found rather than failing the entire request — check not_found.length to detect partial or fully-unresolved batches. Sending more than 50 symbols returns 400 BATCH_LIMIT_EXCEEDED with max and received fields in the error body.volume is always an integer — 0 for instruments with no recorded trades today, never null.
curl "https://mystocks.africa/api/sandbox/v1/partner/market/quotes?symbols=SCOM.KE,GLD.ZA,INVALID_XYZ" \ -H "Authorization: Bearer sk_sandbox_<your_key>"
{
"data": [
{
"symbol": "SCOM.KE",
"name": "Safaricom PLC",
"exchange": "NSE",
"currency": "KES",
"price": 16.5,
"usd_price": 0.0126,
"change": 0.25,
"change_pct": 0.015385,
"volume": 4210000,
"bid": null,
"ask": null,
"market_status": "OPEN"
},
{
"symbol": "GLD.ZA",
"name": "NewGold Issuer ETF",
"exchange": "JSE",
"currency": "ZAR",
"price": 410,
"usd_price": 24.85,
"change": -1.5,
"change_pct": -0.003643,
"volume": 0,
"bid": null,
"ask": null,
"market_status": "OPEN"
}
],
"not_found": [
"INVALID_XYZ"
],
"meta": {
"request_id": "4a3f9c1e-7b2d-4e8a-9f06-1c5d3e2b7a04",
"timestamp": "2026-06-04T09:42:00.000Z"
}
}4. Exchange Market Status & Holiday Calendar
Use GET /market/status to determine whether a specific exchange is currently open, and when it next opens. Status is computed from live exchange schedules and the Firestore holiday calendar managed in /admin/marketholidays. When an exchange is closed due to a public holiday the response includes a holiday field with the reason string. If holiday data is temporarily unavailable the endpoint degrades gracefully to schedule-only status.
curl "https://mystocks.africa/api/sandbox/v1/partner/market/status?exchange=NSE" \ -H "Authorization: Bearer sk_sandbox_<your_key>"
Normal trading day — open
{
"exchange": "NSE",
"name": "Nairobi Securities Exchange",
"country": "Kenya",
"currency": "KES",
"isOpen": true,
"status": "OPEN",
"localOpen": "09:00",
"localClose": "15:00",
"timezone": "Africa/Nairobi",
"nextOpen": null,
"checkedAt": "2026-06-03T09:45:00.000Z"
}Public holiday — forced closed
{
"exchange": "NSE",
"name": "Nairobi Securities Exchange",
"country": "Kenya",
"currency": "KES",
"isOpen": false,
"status": "CLOSED",
"localOpen": "09:00",
"localClose": "15:00",
"timezone": "Africa/Nairobi",
"nextOpen": "2026-06-02T06:00:00.000Z",
"holiday": "Madaraka Day",
"checkedAt": "2026-06-01T10:00:00.000Z"
}Omit ?exchange= to receive status for all nine supported exchanges (NSE, NGX, JSE, GSE, BRVM, ZSE, BSE, LUSE, EGX) plus a top-level anyOpen boolean.
5. Top Movers
Retrieve the top price-change movers for any supported exchange — sorted by change_pct descending (gainers) or ascending (losers). Results are paginated; use page and limit to walk through them. Only instruments with a recorded changePct value appear — instruments with no intraday price movement are excluded.
Parameters
Meta envelope
curl "https://mystocks.africa/api/sandbox/v1/partner/market/movers?exchange=NSE&direction=gainers&limit=5&page=1" \ -H "Authorization: Bearer sk_sandbox_<your_key>"
{
"data": [
{
"symbol": "EABL.KE",
"name": "East African Breweries",
"exchange": "NSE",
"currency": "KES",
"price": 185,
"usd_price": 1.423077,
"change": 10,
"change_pct": 0.057143,
"volume": 312000,
"market_status": "OPEN"
},
{
"symbol": "SCOM.KE",
"name": "Safaricom PLC",
"exchange": "NSE",
"currency": "KES",
"price": 16.5,
"usd_price": 0.126923,
"change": 0.5,
"change_pct": 0.03125,
"volume": 4210000,
"market_status": "OPEN"
}
],
"meta": {
"exchange": "NSE",
"direction": "gainers",
"total_count": 48,
"page": 1,
"per_page": 5,
"has_next": true
}
}6. Holiday Calendar
Fetch upcoming exchange closure dates for any supported exchange and date range. Use this to power "next trading day" schedulers, show users when their exchange is closed, or warn before placing an order near a holiday. Defaults to today through 12 months out. Maximum range is 2 years.
Parameters
Supported exchanges
# NSE holidays for the rest of 2026 curl "https://mystocks.africa/api/sandbox/v1/partner/market/holidays?exchange=NSE&from=2026-06-06&to=2026-12-31" \ -H "Authorization: Bearer sk_sandbox_<your_key>" # All exchanges, default range (today + 12 months) curl "https://mystocks.africa/api/sandbox/v1/partner/market/holidays" \ -H "Authorization: Bearer sk_sandbox_<your_key>"
{
"data": [
{
"exchange": "NSE",
"date": "2026-06-01",
"reason": "Madaraka Day"
},
{
"exchange": "NSE",
"date": "2026-10-10",
"reason": "Utamaduni Day"
},
{
"exchange": "NSE",
"date": "2026-10-20",
"reason": "Mashujaa Day"
},
{
"exchange": "NSE",
"date": "2026-12-12",
"reason": "Jamhuri Day"
},
{
"exchange": "NSE",
"date": "2026-12-25",
"reason": "Christmas Day"
},
{
"exchange": "NSE",
"date": "2026-12-26",
"reason": "Boxing Day"
}
],
"count": 6,
"from": "2026-06-06",
"to": "2026-12-31",
"meta": {
"request_id": "req_abc123",
"timestamp": "2026-06-06T10:00:00Z",
"exchange": "NSE"
}
}Combine with GET /market/status for the full picture: status tells you if the market is open right now, while holidays gives you the forward-looking calendar to build scheduling logic. Data keys (pk_data_) can call this endpoint.
7. Exchange Directory
Returns all supported exchanges with their trading hours, currencies, settlement cycles, and live OPEN/CLOSED status in a single call. Also accessible as GET /market-data/exchanges.
curl "https://mystocks.africa/api/sandbox/v1/partner/market-data/exchanges" \ -H "Authorization: Bearer sk_sandbox_<key>"
{
"data": [
{
"code": "NSE",
"name": "Nairobi Securities Exchange",
"country": "Kenya",
"currency": "KES",
"timezone": "Africa/Nairobi",
"open": "09:00",
"close": "15:00",
"settlement": "T+3",
"isOpen": true,
"status": "OPEN"
},
{
"code": "NGX",
"name": "Nigerian Exchange Group",
"country": "Nigeria",
"currency": "NGN",
"timezone": "Africa/Lagos",
"open": "10:00",
"close": "14:30",
"settlement": "T+3",
"isOpen": false,
"status": "CLOSED"
}
],
"meta": {
"request_id": "req_abc123",
"timestamp": "2026-06-09T10:00:00Z"
}
}8. EOD OHLCV Candles
End-of-day OHLCV (open, high, low, close, volume) candles for any symbol over a custom date range. Also accessible as GET /market-data/ohlcv. Note: open, high, and low are null until intraday data becomes available for that session — check ohlc_available in the response before rendering a candlestick chart.
| Field | Type | Required | Description |
|---|---|---|---|
| symbol | string | Yes | Exchange-qualified symbol, e.g. SCOM.KE. |
| exchange | string | Yes | Exchange code, e.g. NSE. |
| from | string | No | Start date (YYYY-MM-DD, UTC). |
| to | string | No | End date (YYYY-MM-DD, UTC). |
| interval | string | No | Candle interval — currently only 1d supported. |
curl "https://mystocks.africa/api/sandbox/v1/partner/market-data/ohlcv?symbol=SCOM.KE&exchange=NSE&from=2026-06-01&to=2026-06-06" \ -H "Authorization: Bearer sk_sandbox_<key>"
{
"data": [
{
"date": "2026-06-02",
"open": null,
"high": null,
"low": null,
"close": 16.25,
"volume": 3820000,
"ohlc_available": false
},
{
"date": "2026-06-03",
"open": 16.3,
"high": 16.75,
"low": 16.1,
"close": 16.5,
"volume": 4210000,
"ohlc_available": true
}
],
"meta": {
"symbol": "SCOM.KE",
"exchange": "NSE",
"from": "2026-06-01",
"to": "2026-06-06",
"interval": "1d",
"request_id": "req_def456",
"timestamp": "2026-06-09T10:00:00Z"
}
}Aliases: GET /market/quotes and GET /market/movers are also accessible as GET /market-data/quotes and GET /market-data/movers — identical handlers, interchangeable.
Sandbox Scenario Simulations
To reliably test edge cases and error handlings, pass standard simulation headers in Sandbox:
X-Sandbox-Force-Status: Force specific HTTP status codes (e.g.,429Rate Limited,500Internal Error).X-Sandbox-Force-Error: Force machine-readable application sub-error codes (e.g.,INSUFFICIENT_FUNDS,KYC_REQUIRED).
Error Handling
Every error response carries a top-level error object with a machine-readable code string and a human-readablemessage. This applies to all endpoints: auth failures, rate limits, validation, not-found, and server errors. Check error.code for programmatic handling; use error.message for logging or UI display.
Auth error (401)
{
"error": {
"code": "AUTH_MISSING",
"message": "Provide a valid partner API key via Authorization: Bearer pk_live_... or X-Api-Key header."
}
}Not found error (404)
{
"error": {
"code": "NOT_FOUND",
"message": "Stock SCOM.KE not found."
}
}Validation error (400)
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Ambiguous symbol \"MTN\" matches multiple listings: MTN.GH, MTN.NG. Use an exchange-qualified symbol."
}
}Server error (500)
{
"error": {
"code": "INTERNAL_ERROR",
"message": "Upstream data source unavailable. Retry with exponential backoff."
}
}| Field | Type | Required | Description |
|---|---|---|---|
| 200 | OK | No | Request succeeded. |
| 201 | Created | No | Resource created (register, create sub-account). |
| 202 | Accepted | No | Order accepted and queued for admin review. Returned by trade, subscribe. Funds are escrowed immediately; settlement is asynchronous. |
| 400 | Bad Request | No | Missing or invalid parameters — check the error message. |
| 401 | Unauthorized | No | API key missing, invalid, revoked, or expired. |
| 403 | Forbidden | No | Action not permitted. Causes: partner account not yet approved, sub-account frozen, or attempting an operation outside your tier. |
| 404 | Not Found | No | Stock symbol, sub-account, holding, or instrument not found. |
| 409 | Conflict | No | Idempotency collision — a concurrent request with the same Idempotency-Key is still in progress. Retry after the first request resolves. |
| 422 | Unprocessable | No | Business rule violation: insufficient wallet balance, KYC required, sub-account frozen, fund not open for subscription, or units exceed available holdings. |
| 429 | Too Many Requests | No | Rate limit exceeded. Check X-RateLimit-Remaining and X-RateLimit-Reset headers. Default: 100 req/min (starter), 500 (growth), 2000 (enterprise). |
| 500 | Internal Server Error | No | Server-side failure — retry with exponential backoff or contact support. |
Machine-readable error codes
When the error field is an object, the error.code string is one of the values below. Check error.code first for programmatic handling, then use error.message for logging or display.
| Field | Type | Required | Description |
|---|---|---|---|
| AUTH_MISSING | 401 | No | No API key supplied. Send Authorization: Bearer pk_live_... or X-Api-Key header. |
| AUTH_INVALID | 401 | No | Key format is wrong or the key does not exist in the registry. |
| AUTH_SUSPENDED | 403 | No | Partner account is temporarily suspended. Contact support@mystocks.africa. |
| AUTH_REVOKED | 403 | No | Key permanently revoked. A new key must be issued — revocation cannot be undone. |
| RATE_LIMITED | 429 | No | Too many requests. Back off and retry after the X-RateLimit-Reset timestamp. |
| MISSING_PARAM | 400 | No | A required query or body parameter is absent. Check error.param (or error.params[]) for the field name. |
| INVALID_SYMBOL | 400 | No | Symbol fails format validation: must be 2–20 alphanumeric characters, optionally dot-separated (e.g. KEGN.KE). Check error.invalid[] for the offending symbols in a batch request. |
| UNKNOWN_EXCHANGE | 400 | No | Exchange code not recognised. Supported codes: NSE, NGX, JSE, GSE, BRVM, ZSE, BSE, LUSE, EGX. |
| INVALID_TYPE | 400 | No | A parameter has the wrong type — e.g. a numeric value was passed where a string exchange code is required. |
| VALIDATION_ERROR | 400 | No | Request body or query param failed validation. Check error.message for the specific field. |
| BATCH_LIMIT_EXCEEDED | 400 | No | Batch request exceeds the maximum symbol count. Check error.max for the limit and error.received for how many were sent. |
| NOT_FOUND | 404 | No | Requested resource (stock, sub-account, order, instrument) does not exist. |
| CONFLICT | 409 | No | Concurrent request conflict: idempotency-key collision or order state changed before the operation completed. |
| INSUFFICIENT_FUNDS | 400 | No | Wallet balance too low. Top up the master wallet or deposit funds into the sub-account first. |
| KYC_REQUIRED | 403 | No | Sub-account KYC is not verified. Assert KYC via POST /users/{userId}/kyc then retry. |
| FORBIDDEN | 403 | No | Authenticated but not authorised. Common causes: read-only data key used on a write endpoint, or operation outside your tier. |
| INTERNAL_ERROR | 500 | No | Unexpected server-side failure. Safe to retry with exponential backoff. File a support ticket if it persists. |
Order rejection codes
When an order moves to REJECTED status, both the order.rejected webhook and the GET /orders/{orderId} response include a structured rejectionCode field alongside the free-text rejectionReason. Use rejectionCode to drive programmatic handling (e.g. showing a different UI message or routing to a support flow); use rejectionReason for display or logging.
| Field | Type | Required | Description |
|---|---|---|---|
| INSUFFICIENT_FUNDS | string | No | Sub-account or master wallet had insufficient balance at time of review. |
| KYC_REQUIRED | string | No | Sub-account KYC is not verified. Assert via POST /users/{userId}/kyc before retrying. |
| MARKET_CLOSED | string | No | The exchange was closed or on a public holiday when the order was reviewed. |
| COMPLIANCE_HOLD | string | No | Order flagged for compliance review. Contact support. |
| TECHNICAL_ISSUE | string | No | A technical issue prevented execution. Safe to retry. |
| OTHER | string | No | Reason does not fit a standard code — see rejectionReason for detail. |
Rate Limits
Limits are applied per API key, per minute. Every response includes rate-limit headers so you can track consumption and back off gracefully.
| Tier | Requests / min | Notes |
|---|---|---|
| Sandbox / Starter | 100 | Default for all new sandbox and starter accounts. |
| Growth | 500 | Available on the Growth plan. Contact us to upgrade. |
| Enterprise | 2,000 | Custom limits available above 2,000 req/min — contact partnerships@mystocks.africa. |
Rate-limit headers (every response)
X-RateLimit-Limit: 100 # your tier's request ceiling per minute X-RateLimit-Remaining: 43 # requests remaining in the current window X-RateLimit-Reset: 1751400060 # Unix timestamp (seconds) when the window resets Retry-After: 17 # seconds until the window resets (only set on 429 responses)
The window is a 1-minute sliding window. Exceeding the limit returns 429 immediately — requests are not queued or delayed. Use Retry-After (seconds) or X-RateLimit-Reset (epoch) to know when to retry. The window resets fully at the top of each minute, not on a rolling per-request basis.
429 response body
{
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded. Tier: starter (100 req/min). Resets in 17s."
}
}Backoff example (Node.js)
async function fetchWithBackoff(url, options, retries = 3) {
const res = await fetch(url, options);
if (res.status === 429 && retries > 0) {
const reset = Number(res.headers.get('X-RateLimit-Reset'));
const delay = Math.max(reset * 1000 - Date.now(), 1000);
await new Promise(r => setTimeout(r, delay));
return fetchWithBackoff(url, options, retries - 1);
}
return res;
}- Read endpoints (GET /stocks, GET /quote) are cheaper to cache locally — avoid polling them per user request.
- Batch sub-account operations where possible: list all orders once per minute rather than one request per user.
- Webhook callbacks do not count toward your rate limit.
- Rate limits apply to both sandbox and production keys independently.
Pagination
List endpoints use cursor-based pagination. Pass ?limit= to control page size and ?cursor= to advance to the next page. Every paginated response includes hasMore and nextCursor so you never need to calculate offsets.
Request parameters
| Field | Type | Required | Description |
|---|---|---|---|
| limit | integer | No | Items per page. Default and max vary by endpoint (see table below). Clamped to the endpoint maximum if exceeded. |
| cursor | string | No | Opaque cursor returned as nextCursor in the previous response. Omit on the first request. Do not construct or parse cursor values — treat them as opaque strings. |
Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| count | integer | No | Number of items in this page (≤ limit). |
| hasMore | boolean | No | true if at least one more page exists. false means this is the last page. |
| nextCursor | string | null | No | Pass as ?cursor= on the next request. null when hasMore is false. |
Limit defaults and maximums by endpoint
| Endpoint | Default | Max |
|---|---|---|
| GET /users | 100 | 500 |
| GET /orders | 50 | 200 |
| GET /users/{userId}/orders | 50 | 200 |
| GET /users/{userId}/transactions | 50 | 200 |
| GET /audit | 50 | 200 |
| GET /webhooks/{id}/deliveries | 20 | 100 |
Fetching all pages — JavaScript example
async function fetchAllOrders(apiKey) {
const orders = [];
let cursor = null;
do {
const url = new URL('https://mystocks.africa/api/v1/partner/orders');
url.searchParams.set('limit', '200');
if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, { headers: { 'x-api-key': apiKey } });
const page = await res.json();
orders.push(...page.orders);
cursor = page.nextCursor; // null on the last page
} while (cursor);
return orders;
}Cursor stability
Cursors are Firestore document IDs and remain valid indefinitely. New items inserted after your first request will appear in subsequent pages if they sort after the cursor position — consistent forward-only iteration is guaranteed. Do not cache cursors across API key rotations; the underlying document scope is the same but results may differ.
Stocks & Market Data
Real-time and historical price data for equities and ETFs across all supported African exchanges. Endpoints prefixed GET /stocks/ return equity data; GET /etfs/ mirrors the same shape for exchange-traded funds. All price endpoints accept both exchange-qualified symbols (SCOM.KE) and bare tickers (SCOM) when unambiguous.
/stocksReturns the full catalogue of tradeable equities. Supports filtering by exchange, sector, and listing status. All prices are live read-through from production regardless of environment.
| Field | Type | Required | Description |
|---|---|---|---|
| exchange | string | No | Filter by exchange code: NSE, NGX, JSE, GSE, BRVM, LUSE, USE, DSE, EGX, BSE, ZSE. |
| sector | string | No | Filter by sector name (e.g. Banking, Telecommunications). |
| listingStatus | string | No | ACTIVE (default), SUSPENDED, or DELISTED. |
| limit | integer | No | Items per page. Default 100, max 500. |
| cursor | string | No | Pagination cursor from the previous response. |
curl "https://mystocks.africa/api/sandbox/v1/partner/stocks?exchange=NSE&limit=5" \ -H "Authorization: Bearer sk_sandbox_<key>"
{
"stocks": [
{
"id": "SCOM.KE",
"symbol": "SCOM.KE",
"name": "Safaricom PLC",
"exchange": "NSE",
"currency": "KES",
"sector": "Telecommunications",
"assetType": "STOCK",
"listingStatus": "ACTIVE",
"price": 16.5,
"usdPrice": 0.0126,
"change": 0.25,
"changePct": 1.54,
"volume": 4210000,
"logoUrl": "https://mystocks.africa/logos/scom-ke.svg",
"lastPriceUpdate": "2026-06-04T09:45:00Z"
}
],
"count": 1,
"hasMore": true,
"nextCursor": "cursor_abc"
}/stocks/{symbol}Real-time quote for a single stock. Returns the full StockSummary shape plus open, dayHigh, dayLow, previousClose, volume, and a self-hosted logoUrl. The symbol path accepts exchange-qualified (SCOM.KE), slug (safaricom), or bare ticker (SCOM).
curl "https://mystocks.africa/api/sandbox/v1/partner/stocks/SCOM.KE" \ -H "Authorization: Bearer sk_sandbox_<key>"
{
"id": "SCOM.KE",
"symbol": "SCOM.KE",
"name": "Safaricom PLC",
"exchange": "NSE",
"currency": "KES",
"sector": "Telecommunications",
"assetType": "STOCK",
"listingStatus": "ACTIVE",
"price": 16.5,
"usdPrice": 0.0126,
"change": 0.25,
"changePct": 1.54,
"open": 16.2,
"dayHigh": 16.75,
"dayLow": 16.1,
"previousClose": 16.25,
"volume": 4210000,
"logoUrl": "https://mystocks.africa/logos/scom-ke.svg",
"lastPriceUpdate": "2026-06-04T09:45:00Z"
}/stocks/{symbol}/chartalso: /etfs/{symbol}/chartPre-computed price history for rendering charts. Prices are in the stock's native local currency. See the ETFs & Price Charts section for full parameter details and response shape.
| Field | Type | Required | Description |
|---|---|---|---|
| period | string | Yes | 1d, 1w, 1m, 3m, 6m, 1y, or 5y. |
/stocks/{symbol}/pulseRecent news headlines, corporate announcements, and analyst sentiment for a stock. Use to populate a “News” tab or price-movement explanation in your UI.
curl "https://mystocks.africa/api/sandbox/v1/partner/stocks/SCOM.KE/pulse" \ -H "Authorization: Bearer sk_sandbox_<key>"
{
"symbol": "SCOM.KE",
"pulse": [
{
"id": "pulse_abc",
"title": "Safaricom H1 profit up 14%",
"summary": "Safaricom posted KES 23.4bn net profit for H1 2026, driven by M-PESA revenue growth.",
"source": "MyStocks Africa",
"publishedAt": "2026-06-03T08:00:00Z",
"url": "https://mystocks.africa/market-intel/safaricom-h1-2026"
}
],
"count": 1
}Company Profiles
/companies/{symbol}Company fundamentals: description, sector, market cap, P/E ratio, EPS, 52-week high/low, and financial highlights. Also available as a list via GET /companies with ?exchange= and ?sector= filters.
curl "https://mystocks.africa/api/sandbox/v1/partner/companies/SCOM.KE" \ -H "Authorization: Bearer sk_sandbox_<key>"
{
"symbol": "SCOM.KE",
"name": "Safaricom PLC",
"exchange": "NSE",
"sector": "Telecommunications",
"description": "Safaricom PLC is the largest telecoms provider in East Africa and pioneer of M-PESA mobile money.",
"marketCap": 1360000000,
"pe": 12.4,
"eps": 1.33,
"high52w": 19.8,
"low52w": 12.5,
"dividendYield": 6.2,
"sharesOutstanding": 40065000000
}/companies/{symbol}/newsCurated news and regulatory announcements for the company. Same shape as /pulse but sourced from exchange filings and financial newswires. Supports ?limit= and ?cursor=.
/companies/tickersCanonical ticker list — every symbol, slug, display name, and exchange in one call. Use to build a local symbol-resolution map or a search index without paginating GET /stocks. Response is a flat array sorted alphabetically by symbol.
curl "https://mystocks.africa/api/sandbox/v1/partner/companies/tickers" \ -H "Authorization: Bearer sk_sandbox_<key>"
{
"tickers": [
{
"symbol": "ACCESS.NG",
"slug": "access-holdings",
"name": "Access Holdings",
"exchange": "NGX"
},
{
"symbol": "DANGCEM.NG",
"slug": "dangote-cement",
"name": "Dangote Cement",
"exchange": "NGX"
},
{
"symbol": "EABL.KE",
"slug": "eabl",
"name": "East African Breweries",
"exchange": "NSE"
}
],
"count": 3
}/market-intelEditorial market intelligence articles and exchange announcements curated by the MyStocks research team. Use to power a “Market News” feed in your product. Filter by ?exchange=NGX or ?symbol=SCOM.KE. Returns { articles, count } — newest first.
| Field | Type | Required | Description |
|---|---|---|---|
| symbol | string | No | Filter to articles tagged with a specific stock symbol, e.g. SCOM.KE. |
| exchange | string | No | Filter articles by exchange code, e.g. NGX, NSE. |
| limit | integer | No | Max results. Default 20, max 100. |
curl "https://mystocks.africa/api/sandbox/v1/partner/market-intel?exchange=NGX&limit=5" \ -H "Authorization: Bearer sk_sandbox_<key>"
{
"articles": [
{
"id": "intel_ngx_q2",
"title": "NGX Q2 2026 Market Wrap",
"summary": "NGX All-Share Index gained 4.2% in Q2 led by banking and cement sectors.",
"slug": "ngx-q2-2026-market-wrap",
"category": "macro",
"symbols": [],
"exchange": "NGX",
"imageUrl": null,
"source": "MyStocks Africa",
"sourceUrl": null,
"publishedAt": "2026-06-01T08:00:00Z",
"createdAt": "2026-06-01T07:30:00Z"
}
],
"count": 1
}/market-intel/{id}Returns full detail for a single article including the full body field (HTML or markdown). Resolves by Firestore document ID or URL slug.
curl "https://mystocks.africa/api/sandbox/v1/partner/market-intel/ngx-q2-2026-market-wrap" \ -H "Authorization: Bearer sk_sandbox_<key>"
{
"id": "intel_ngx_q2",
"title": "NGX Q2 2026 Market Wrap",
"summary": "NGX All-Share Index gained 4.2% in Q2.",
"body": "<p>Full article content here...</p>",
"slug": "ngx-q2-2026-market-wrap",
"category": "macro",
"symbols": [
"DANGCEM.NG",
"GTCO.NG"
],
"exchange": "NGX",
"imageUrl": null,
"source": "MyStocks Africa",
"sourceUrl": null,
"author": "MyStocks Research",
"tags": [
"NGX",
"Q2",
"equities"
],
"publishedAt": "2026-06-01T08:00:00Z",
"createdAt": "2026-06-01T07:30:00Z",
"updatedAt": null
}Sub-Accounts
Each of your end-users gets an isolated sub-account with its own USD wallet, portfolio, and order history. All write operations (deposit, withdraw, trade) are scoped to a single sub-account. The master wallet is the funding source for sub-account deposits.
/usersCreate a sub-account for one of your end-users. Returns HTTP 201 with the new account. Use externalId to map MyStocks sub-account IDs back to your own user table.
| Field | Type | Required | Description |
|---|---|---|---|
| externalId | string | Yes | Your internal user ID. Must be unique per partner. |
| displayName | string | No | Full name shown in the MyStocks admin console. |
| string | No | User email address. |
curl -X POST "https://mystocks.africa/api/sandbox/v1/partner/users" \
-H "Authorization: Bearer sk_sandbox_<key>" \
-H "Content-Type: application/json" \
-d '{"externalId":"user_42","displayName":"Jane Doe","email":"jane@example.com"}'{
"subAccountId": "usr_abc123",
"externalId": "user_42",
"displayName": "Jane Doe",
"email": "jane@example.com",
"kycStatus": "NONE",
"kycLevel": "NONE",
"status": "active",
"walletBalance": 0
}/auto-registerIdempotent sub-account creation — safe to call on every user login. If a sub-account already exists for the given uid it is returned unchanged (no duplicate created). Returns the same shape as POST /users.
| Field | Type | Required | Description |
|---|---|---|---|
| uid | string | Yes | Your internal user identifier. Acts as idempotency key. |
| string | No | Email address. | |
| name | string | No | Display name. |
| phone | string | No | Phone in E.164 format, e.g. +254712345678. |
| country | string | No | ISO 3166-1 alpha-2 country code. |
/users/users/{userId}List all sub-accounts (cursor-paginated, default 100/page, max 500) or fetch a single one by its subAccountId. Filter by ?externalId= to look up using your own ID.
/users/{userId}Update display name or email. Set status: "frozen" to suspend all trading, deposits, and withdrawals for the sub-account. Set back to status: "active" to restore access.
curl -X PATCH "https://mystocks.africa/api/v1/partner/users/usr_abc123" \
-H "Authorization: Bearer pk_live_<key>" \
-H "Content-Type: application/json" \
-d '{"status":"frozen"}'/users/{userId}/depositCredit a sub-account wallet. The USD amount is deducted from your master wallet and added to the sub-account. Pass localAmount, localCurrency, and fxRate for the audit trail — they do not affect how much USD is credited. Always set Idempotency-Key.
| Field | Type | Required | Description |
|---|---|---|---|
| amount | number | Yes | USD to credit. Deducted from your master wallet. |
| localAmount | number | No | Local-currency equivalent (for audit trail only). |
| localCurrency | string | No | ISO 4217 code, e.g. KES, NGN, GHS. |
| fxRate | number | No | Local units per USD that you applied. |
| note | string | No | Reference string shown in transaction history. |
curl -X POST "https://mystocks.africa/api/sandbox/v1/partner/users/usr_abc123/deposit" \
-H "Authorization: Bearer sk_sandbox_<key>" \
-H "Idempotency-Key: dep_user42_001" \
-H "Content-Type: application/json" \
-d '{"amount":500,"localAmount":655000,"localCurrency":"KES","fxRate":1310,"note":"mpesa_QHJ29SK"}'{
"message": "Deposit successful.",
"subAccountId": "usr_abc123",
"amount": 500,
"currency": "USD",
"newSubBalance": 500,
"newMasterBalance": 99500
}/users/{userId}/withdrawDebit a sub-account wallet and return the funds to your master wallet. You are responsible for paying the user in their local currency at the rate you applied. The sub-account must have sufficient uninvested balance. Always set Idempotency-Key.
| Field | Type | Required | Description |
|---|---|---|---|
| amount | number | Yes | USD to move to your master wallet. |
| localAmount | number | No | Local equivalent for audit trail. |
| localCurrency | string | No | ISO 4217 code. |
| fxRate | number | No | Rate you applied. |
| note | string | No | Reference for transaction history. |
{
"message": "Withdrawal successful.",
"subAccountId": "usr_abc123",
"amount": 200,
"currency": "USD",
"newSubBalance": 300,
"newMasterBalance": 99700
}/users/{userId}/kycAssert the KYC result you obtained using your own provider. Set status: "VERIFIED" and level: "BASIC" to unlock trading for the sub-account. FULL unlocks enhanced limits and higher-value subscriptions. Supply your internal reference for audit correlation.
| Field | Type | Required | Description |
|---|---|---|---|
| status | string | Yes | NONE | PENDING | VERIFIED |
| level | string | Yes | NONE | BASIC (ID verified) | FULL (enhanced due diligence) |
| reference | string | No | Your KYC session ID for audit correlation. |
curl -X POST "https://mystocks.africa/api/v1/partner/users/usr_abc123/kyc" \
-H "Authorization: Bearer pk_live_<key>" \
-H "Content-Type: application/json" \
-d '{"status":"VERIFIED","level":"BASIC","reference":"kyc_session_88721"}'{
"message": "KYC status updated.",
"subAccountId": "usr_abc123",
"kycStatus": "VERIFIED",
"kycLevel": "BASIC"
}Trading
Place BUY and SELL orders on behalf of your master account or any sub-account. In sandbox, orders fill instantly. In production, orders move PENDING → PROCESSING → COMPLETED / REJECTED after admin review within 4 business hours during exchange hours. Always use Idempotency-Key on trade calls.
/quote/{symbol}Preview the fee breakdown for a hypothetical trade without placing an order. Returns gross value, base fee (0.75%), optional partner markup, total cost (BUY) or estimated proceeds (SELL), and whether the wallet has sufficient funds.
| Field | Type | Required | Description |
|---|---|---|---|
| type | string | Yes | BUY or SELL. |
| quantity | integer | Yes | Number of shares. |
| userId | string | No | Sub-account ID. Omit to quote against the master wallet. |
curl "https://mystocks.africa/api/sandbox/v1/partner/quote/SCOM.KE?type=BUY&quantity=500" \ -H "Authorization: Bearer sk_sandbox_<key>"
{
"symbol": "SCOM.KE",
"name": "Safaricom PLC",
"exchange": "NSE",
"currency": "KES",
"type": "BUY",
"quantity": 500,
"localPrice": 16.5,
"usdPrice": 0.127306,
"gross": 63.65,
"baseFee": 0.48,
"partnerMarkupFee": 0,
"fee": 0.48,
"totalCost": 64.13,
"walletBalance": 1250,
"sufficientFunds": true,
"feeRate": 0.75,
"note": "This is a quote only. No order has been placed."
}/trademaster account/users/{userId}/tradesub-accountPlace a BUY or SELL order. BUY escrows the cost (gross + fee) from the wallet immediately. SELL checks that the sub-account holds the required units. The symbol accepts exchange-qualified format (SCOM.KE) or bare ticker (SCOM) — auto-resolved when unambiguous.
| Field | Type | Required | Description |
|---|---|---|---|
| symbol | string | Yes | Exchange-qualified ticker or unambiguous bare ticker. |
| type | string | Yes | BUY or SELL. |
| quantity | integer | Yes | Whole shares. Must be a positive integer. |
| stopLoss | number | No | Auto-cancel price floor (USD). Optional. |
| takeProfit | number | No | Auto-settle price ceiling (USD). Optional. |
curl -X POST "https://mystocks.africa/api/sandbox/v1/partner/users/usr_abc123/trade" \
-H "Authorization: Bearer sk_sandbox_<key>" \
-H "Idempotency-Key: trade_user42_001" \
-H "Content-Type: application/json" \
-d '{"symbol":"SCOM.KE","type":"BUY","quantity":1000}'{
"status": "PENDING",
"orderId": "ord_abc123",
"subAccountId": "usr_abc123",
"externalId": "user_42",
"type": "BUY",
"symbol": "SCOM.KE",
"quantity": 1000,
"priceAtOrder": 16.5,
"usdPriceAtOrder": 0.0126,
"gross": 12.6,
"baseFee": 0.09,
"partnerMarkupFee": 0,
"fee": 0.09,
"totalCost": 12.69,
"note": "Order is pending admin approval. Funds reserved from sub-account wallet."
}Production returns PENDING with the order's reserved price as priceAtOrder (local) and usdPriceAtOrder — the wallet balance is not echoed because settlement is asynchronous. In sandbox the same call resolves instantly to FILLED and the response instead carries usdPrice, localPrice, currency, and the post-trade newSubBalance (see the Quick Start).
Sandbox cheat codes
Quantity 100 — instant auto-fill (COMPLETED). Quantity 999 — instant auto-reject (REJECTED). Use to test your webhook handlers without waiting for the admin queue.
/orders/orders/{orderId}/users/{userId}/ordersList orders for the master account or a specific sub-account (cursor-paginated). Fetch a single order by ID to poll status. Order fields include status, rejectionCode, rejectionReason, fee, baseFee, partnerMarkupFee, and timestamps. Filter by ?status=PENDING, ?symbol=, or ?from=/to= date range.
{
"orders": [
{
"id": "ord_abc123",
"symbol": "SCOM.KE",
"name": "Safaricom PLC",
"exchange": "NSE",
"type": "BUY",
"status": "COMPLETED",
"quantity": 1000,
"priceAtOrder": 16.5,
"usdPriceAtOrder": 0.0126,
"totalAmount": 12.69,
"feeAmount": 0.09,
"currency": "USD",
"localCurrency": "KES",
"rejectionCode": null,
"rejectionReason": null,
"settledAt": "2026-06-04T11:30:00Z",
"createdAt": "2026-06-04T09:45:00Z"
}
],
"count": 1,
"hasMore": false,
"nextCursor": null
}/orders/{orderId}/users/{userId}/orders/{orderId}Cancel a PENDING order. BUY escrow is refunded atomically. Returns HTTP 409 if the order has moved to PROCESSING, COMPLETED, or REJECTED — it cannot be cancelled at that point. A successful cancellation fires an order.cancelled webhook.
/users/{userId}/transactionsFull wallet ledger for a sub-account — every credit and debit in chronological order. Includes deposits, withdrawals, trade escrows (INVEST), trade proceeds (SELL), dividend distributions (DISTRIBUTION), fund redemptions (REDEEM), and fees. Cursor-paginated — pass nextCursor from the previous response as ?cursor= to page forward. Filter by ?type=DEPOSIT or date range via ?from=YYYY-MM-DD&to=YYYY-MM-DD (UTC, inclusive).
| Param | Type | Description |
|---|---|---|
| type | string | Filter: DEPOSIT | WITHDRAWAL | INVEST | SELL | DISTRIBUTION | REDEEM | FEE | TRANSFER_IN | TRANSFER_OUT |
| from | date | Include transactions on or after this date (YYYY-MM-DD, UTC) |
| to | date | Include transactions on or before this date (YYYY-MM-DD, UTC) |
| cursor | string | Opaque cursor from nextCursor. Omit for first page |
| limit | integer | Page size, default 50, max 200 |
{
"transactions": [
{
"id": "txn_abc123",
"type": "DEPOSIT",
"status": "COMPLETED",
"direction": "CREDIT",
"amount": 500,
"currency": "USD",
"description": "Partner deposit",
"reference": "dep_riven_user_42_1743152580",
"createdAt": "2026-06-01T10:00:00Z",
"settledAt": "2026-06-01T10:00:01Z"
},
{
"id": "txn_xyz789",
"type": "INVEST",
"status": "PENDING",
"direction": "DEBIT",
"amount": 64.13,
"currency": "USD",
"description": "BUY SCOM.KE x500",
"reference": "ord_def456",
"createdAt": "2026-06-04T09:45:00Z",
"settledAt": null
}
],
"count": 2,
"hasMore": false,
"nextCursor": null
}/portfoliomaster/users/{userId}/portfoliosub-accountHoldings with current market value, cost basis, unrealized P&L, and currency. Also includes fund and bond positions if the account holds subscriptions.
{
"walletBalance": 487.31,
"totalValue": 512.31,
"holdings": [
{
"symbol": "SCOM.KE",
"name": "Safaricom PLC",
"quantity": 1000,
"avgCostUsd": 0.01269,
"currentPriceUsd": 0.0126,
"marketValue": 12.6,
"unrealizedPnl": -0.09,
"currency": "KES"
}
]
}Bonds, Funds & Opportunities
Beyond equities, the Partner API provides access to fixed-income instruments, open-ended funds, and curated private-market opportunities. All subscriptions and redemptions are handled via sub-account endpoints.
/bonds/bonds/{id}List all available fixed-income instruments — government bonds, T-bills, Eurobonds, infrastructure bonds, and commercial papers. Filter by ?instrumentType=TREASURY_BILL (or BOND | EUROBOND | COMMERCIAL_PAPER | INFRASTRUCTURE_BOND), ?currency=, or ?exchange=.
curl "https://mystocks.africa/api/sandbox/v1/partner/bonds?instrumentType=TREASURY_BILL" \ -H "Authorization: Bearer sk_sandbox_<key>"
{
"bonds": [
{
"id": "ke-treasury-91",
"symbol": "KE91T",
"name": "Kenya 91-Day T-Bill",
"instrumentType": "TREASURY_BILL",
"couponRate": 15.8,
"maturityDate": "2026-09-01",
"pricePerUnit": 100,
"currency": "KES",
"minInvestment": 50000,
"status": "ACTIVE",
"yield": 15.8
}
],
"count": 1
}/funds/funds/{id}Money market and yield funds with instant redemption. Response includes NAV per unit, yield, distribution frequency, minimum investment, and whether the fund is currently open for subscriptions.
{
"funds": [
{
"id": "fund_mmf_africa",
"name": "Africa Money Market Fund",
"fundType": "MONEY_MARKET",
"navPerUnit": 1,
"annualYield": 12.5,
"currency": "USD",
"minInvestment": 50,
"status": "OPEN",
"redemptionType": "instant",
"distributionFrequency": "Monthly"
}
],
"count": 1
}/opportunitiesCurated private credit deals and pre-IPO allocations. Results are merged from both collections and sorted newest first. Each deal exposes target return, minimum commitment, risk level, and funding progress. Subscriptions use POST /users/{userId}/subscribe with assetType: "OPPORTUNITY" or "PRE_IPO".
| Field | Type | Required | Description |
|---|---|---|---|
| type | string | No | OPPORTUNITY | PRE_IPO. Omit for all types. |
curl "https://mystocks.africa/api/sandbox/v1/partner/opportunities?type=PRE_IPO" \ -H "Authorization: Bearer sk_sandbox_<key>"
{
"opportunities": [
{
"id": "TREEPZ-PRE-IPO",
"assetType": "PRE_IPO",
"name": "Treepz — Pre-IPO Round",
"slug": "treepz-pre-ipo",
"symbol": null,
"issuer": null,
"description": "Ghana-based intercity bus platform targeting a secondary listing on the NSE in Q2 2027.",
"minInvestment": 500,
"targetAmount": 3000000,
"currentRaised": 900000,
"expectedReturn": 0.45,
"riskLevel": "HIGH",
"currency": "USD",
"status": "OPEN",
"maturityDate": null,
"expectedExitDate": "2027-06-30",
"createdAt": "2026-05-01T10:00:00Z"
}
],
"count": 1
}/opportunities/{id}Returns full detail for a single private market deal or pre-IPO offering. Resolves from the opportunities collection first, then preIPOOfferings. Accepts Firestore document ID or URL slug.
curl "https://mystocks.africa/api/sandbox/v1/partner/opportunities/TREEPZ-PRE-IPO" \ -H "Authorization: Bearer sk_sandbox_<key>"
{
"id": "TREEPZ-PRE-IPO",
"assetType": "PRE_IPO",
"name": "Treepz — Pre-IPO Round",
"slug": "treepz-pre-ipo",
"symbol": null,
"issuer": null,
"description": "Ghana-based intercity bus platform with 250+ routes targeting a secondary listing on the NSE in Q2 2027.",
"minInvestment": 500,
"targetAmount": 3000000,
"currentRaised": 900000,
"expectedReturn": 0.45,
"riskLevel": "HIGH",
"currency": "USD",
"status": "OPEN",
"maturityDate": null,
"expectedExitDate": "2027-06-30",
"createdAt": "2026-05-01T10:00:00Z"
}/users/{userId}/subscribeSubscribe a sub-account to a bond, fund, private deal, or pre-IPO. The sub-account must be KYC-verified. Funds are escrowed immediately; the position appears in the portfolio once confirmed.
| Field | Type | Required | Description |
|---|---|---|---|
| assetType | string | Yes | BOND | FUND | OPPORTUNITY | PRE_IPO |
| assetId | string | Yes | Firestore doc ID from the relevant list endpoint. |
| units | number | No | Whole units. Required for BOND and FUND. |
| amount | number | No | USD commitment. Required for OPPORTUNITY and PRE_IPO. |
curl -X POST "https://mystocks.africa/api/v1/partner/users/usr_abc123/subscribe" \
-H "Authorization: Bearer pk_live_<key>" \
-H "Content-Type: application/json" \
-d '{"assetType":"FUND","assetId":"fund_mmf_africa","units":500}'/users/{userId}/redeemRedeem fund units back to the sub-account wallet. For funds with redemptionType: "instant", proceeds credit immediately. Units in a lock-up period cannot be redeemed — check unlockedUnits in the portfolio response first.
| Field | Type | Required | Description |
|---|---|---|---|
| holdingId | string | Yes | Sub-account holding doc ID (same as the fund doc ID). |
| unitsToRedeem | number | Yes | Units to redeem. Must not exceed unlockedUnits. |
Fund Flow — Master Wallet
Request top-ups from MyStocks operations to fund your master wallet and submit payout requests to repatriate surplus funds. Both endpoints are idempotent — always pass Idempotency-Key to prevent duplicate submissions.
/accountMaster wallet balance plus an aggregate portfolio summary across all sub-accounts. Useful as a dashboard home-screen data source.
curl "https://mystocks.africa/api/v1/partner/account" \ -H "Authorization: Bearer pk_live_<key>"
{
"masterWalletBalance": 94500,
"totalAum": 45230.5,
"subAccountCount": 142,
"currency": "USD"
}/topup/topupSubmit a master wallet top-up request to MyStocks operations. When funds are credited a wallet.credited webhook fires. Use GET to list all previous top-up requests with their status.
| Field | Type | Required | Description |
|---|---|---|---|
| amount | number | Yes | USD amount requested. |
| reference | string | No | Your internal transfer reference for reconciliation. |
curl -X POST "https://mystocks.africa/api/v1/partner/topup" \
-H "Authorization: Bearer pk_live_<key>" \
-H "Idempotency-Key: topup_20260609_001" \
-H "Content-Type: application/json" \
-d '{"amount":10000,"reference":"wire_TXN987654"}'{
"id": "tup_abc123",
"amount": 10000,
"currency": "USD",
"status": "PENDING",
"reference": "wire_TXN987654",
"createdAt": "2026-06-09T10:00:00Z"
}/payout/payoutSubmit a payout request to repatriate funds from your master wallet to your settlement account. MyStocks operations processes and debits your master wallet on fulfilment. Use GET to list all previous payout requests.
| Field | Type | Required | Description |
|---|---|---|---|
| amount | number | Yes | USD amount to repatriate. |
| reference | string | No | Your internal reference for reconciliation. |
curl -X POST "https://mystocks.africa/api/v1/partner/payout" \
-H "Authorization: Bearer pk_live_<key>" \
-H "Idempotency-Key: payout_20260609_001" \
-H "Content-Type: application/json" \
-d '{"amount":5000,"reference":"payout_REF123"}'{
"id": "pay_abc123",
"amount": 5000,
"currency": "USD",
"status": "PENDING",
"reference": "payout_REF123",
"createdAt": "2026-06-09T10:00:00Z"
}Watchlists
Each sub-account has an isolated watchlist. Use it to let your users save stocks they want to track, then load it alongside live prices to power a personalized market overview screen. Watchlist entries include live price and day-change percent when available.
/users/{userId}/watchlistReturns all watchlisted stocks for a sub-account, ordered by most recently added. Live price and day-change percent are included where data is available.
curl https://mystocks.africa/api/v1/partner/users/usr_abc123/watchlist \ -H "x-api-key: pk_live_KEY"
{
"data": [
{
"assetId": "scom_ke_01",
"symbol": "SCOM.KE",
"name": "Safaricom PLC",
"exchange": "NSE",
"sourceType": "LISTED_STOCK",
"price": 16.25,
"usdPrice": 0.1258,
"change": 1.56,
"currency": "KES",
"addedAt": "2026-06-06T08:00:00Z"
}
],
"count": 1,
"meta": {
"request_id": "req_abc123",
"timestamp": "2026-06-06T08:01:00Z"
}
}/users/{userId}/watchlistAdds a stock to the sub-account watchlist. The symbol is auto-resolved to its canonical exchange-qualified form — pass SCOM and get back SCOM.KE. Returns 409 if the stock is already on the watchlist.
| Field | Type | Required | Description |
|---|---|---|---|
| symbol | string | Yes | Ticker symbol. Exchange-qualified (SCOM.KE) preferred. Bare tickers (SCOM) are auto-resolved. |
curl -X POST https://mystocks.africa/api/v1/partner/users/usr_abc123/watchlist \
-H "x-api-key: pk_live_KEY" \
-H "Content-Type: application/json" \
-d '{"symbol":"SCOM.KE"}'{
"assetId": "scom_ke_01",
"symbol": "SCOM.KE",
"name": "Safaricom PLC",
"exchange": "NSE",
"addedAt": "2026-06-06T08:00:00Z",
"meta": {
"request_id": "req_abc124",
"timestamp": "2026-06-06T08:00:00Z",
"symbol": "SCOM.KE"
}
}/users/{userId}/watchlist/{symbol}Removes a stock from the sub-account watchlist. Returns 204 No Content on success, 404 if the symbol is not on the watchlist.
curl -X DELETE https://mystocks.africa/api/v1/partner/users/usr_abc123/watchlist/SCOM.KE \ -H "x-api-key: pk_live_KEY"
Webhooks
Register HTTPS endpoints to receive real-time event notifications. MyStocks signs every delivery with an HMAC-SHA256 signature over the raw request body using your webhook secret. Your endpoint must respond HTTP 2xx within 8 seconds; heavy processing should be deferred to a background queue. Failed deliveries are retried with exponential backoff — check the delivery log via GET /webhooks/{id}/deliveries.
/webhooks| Field | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | HTTPS endpoint. Must respond 2xx within 8 s. |
| events | string[] | Yes | Array of event type strings to subscribe to. |
| secret | string | No | HMAC signing secret (min 16 chars). Generated automatically if omitted. |
curl -X POST "https://mystocks.africa/api/v1/partner/webhooks" \
-H "Authorization: Bearer pk_live_<key>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/mystocks",
"events": ["order.filled","order.rejected","deposit.confirmed","kyc.updated"],
"secret": "my-signing-secret-min-16-chars"
}'{
"id": "wh_abc123",
"url": "https://yourapp.com/webhooks/mystocks",
"events": [
"order.filled",
"order.rejected",
"deposit.confirmed",
"kyc.updated"
],
"status": "active"
}All webhook events
| Event | Trigger |
|---|---|
| order.pending | An order was submitted and is awaiting admin review. |
| order.filled | An order was approved and executed. Shares/proceeds credited. |
| order.rejected | An order was declined. Includes rejectionCode and rejectionReason. |
| order.cancelled | An order was cancelled via DELETE /orders/{id}. |
| trade.settled | Alias for order.filled — use order.filled in new integrations. |
| trade.rejected | Alias for order.rejected — use order.rejected in new integrations. |
| deposit.confirmed | A sub-account deposit was recorded. |
| withdraw.confirmed | A sub-account withdrawal was processed. |
| wallet.credited | Master wallet received a top-up from MyStocks ops. |
| kyc.updated | A sub-account KYC status changed. |
| account.frozen | A sub-account was frozen by partner or by MyStocks compliance. |
| dividend.paid | A dividend was received and credited to a sub-account. |
| incident.declared | A platform incident affecting your integration was opened. |
| incident.resolved | A previously declared incident was closed. |
Delivery envelope
Every webhook delivery wraps the event-specific payload in a standard three-field envelope. Sandbox deliveries add isSandbox: true to the top level.
{
"event": "order.filled",
"timestamp": "2026-06-06T10:32:00Z",
"data": {
"...": "event-specific fields — see reference below"
}
}Event payload reference
order.pending
Fired immediately when a trade is submitted. Funds are reserved but the order has not yet been executed.
{
"event": "order.pending",
"timestamp": "2026-06-06T10:00:00Z",
"data": {
"orderId": "ord_abc123",
"subAccountId": "usr_abc123",
"externalId": "user_42",
"type": "BUY",
"symbol": "SCOM.KE",
"quantity": 1000,
"priceAtOrder": 16.25,
"usdPriceAtOrder": 0.01258,
"gross": 12.58,
"baseFee": 0.09,
"partnerMarkupFee": 0,
"fee": 0.09,
"totalCost": 12.67,
"status": "PENDING",
"note": "Order is pending admin approval. Funds reserved from sub-account wallet."
}
}order.filled + alias trade.settled
Fired when an order is approved and executed by MyStocks ops. For BUY includes totalCost; for SELL includes proceeds. Both events fire with identical payloads — use order.filled in new integrations.
{
"event": "order.filled",
"timestamp": "2026-06-06T10:32:00Z",
"data": {
"orderId": "ord_abc123",
"subAccountId": "usr_abc123",
"externalId": "user_42",
"type": "BUY",
"symbol": "SCOM.KE",
"exchange": "NSE",
"quantity": 1000,
"priceAtOrder": 16.25,
"usdPriceAtOrder": 0.01258,
"feeAmount": 0.09,
"status": "FILLED",
"settledAt": "2026-06-06T10:32:00Z",
"settlementUsdPrice": 0.01261,
"totalCost": 12.7
}
}order.rejected + alias trade.rejected
Fired when an order is declined. For BUY orders, escrowed funds are returned to the sub-account wallet before this fires.
{
"event": "order.rejected",
"timestamp": "2026-06-06T10:05:00Z",
"data": {
"orderId": "ord_abc123",
"subAccountId": "usr_abc123",
"externalId": "user_42",
"type": "BUY",
"symbol": "SCOM.KE",
"exchange": "NSE",
"quantity": 1000,
"priceAtOrder": 16.25,
"usdPriceAtOrder": 0.01258,
"feeAmount": 0.09,
"status": "REJECTED",
"settledAt": "2026-06-06T10:05:00Z",
"rejectionCode": "LIQUIDITY_UNAVAILABLE",
"rejectionReason": "Insufficient market liquidity for this order size."
}
}order.cancelled
Fired when a partner cancels a PENDING order via DELETE /orders/{orderId}. BUY cancellations include refunded (amount returned to sub-account wallet); SELL cancellations have no wallet impact.
{
"event": "order.cancelled",
"timestamp": "2026-06-06T09:15:00Z",
"data": {
"orderId": "ord_abc123",
"subAccountId": "usr_abc123",
"externalId": "user_42",
"type": "BUY",
"symbol": "SCOM.KE",
"quantity": 1000,
"status": "CANCELLED",
"refunded": 12.67,
"currency": "USD"
}
}deposit.confirmed
Fired after funds are moved from the master wallet to the sub-account wallet. Optional local-currency fields appear when provided in the deposit request.
{
"event": "deposit.confirmed",
"timestamp": "2026-06-06T08:30:00Z",
"data": {
"subAccountId": "usr_abc123",
"externalId": "user_42",
"amount": 50,
"currency": "USD",
"newBalance": 150,
"localAmount": 6500,
"localCurrency": "KES",
"fxRate": 130
}
}withdraw.confirmed
Fired after a sub-account withdrawal moves funds back to the partner master wallet.
{
"event": "withdraw.confirmed",
"timestamp": "2026-06-06T08:45:00Z",
"data": {
"subAccountId": "usr_abc123",
"externalId": "user_42",
"amount": 25,
"currency": "USD",
"newBalance": 125
}
}wallet.credited
Fired when MyStocks ops credits the partner master wallet (e.g. after a top-up request is fulfilled).
{
"event": "wallet.credited",
"timestamp": "2026-06-06T09:00:00Z",
"data": {
"amount": 500,
"newBalance": 1500,
"currency": "USD"
}
}kyc.updated
Fired when a sub-account KYC status changes — either via the partner KYC endpoint or when overridden by MyStocks compliance. overriddenByAdmin: true appears only on admin overrides.
{
"event": "kyc.updated",
"timestamp": "2026-06-06T11:00:00Z",
"data": {
"subAccountId": "usr_abc123",
"externalId": "user_42",
"kycStatus": "VERIFIED",
"kycLevel": "FULL",
"reference": "sumsub_ref_abc123"
}
}account.frozen
Fired on both freeze and unfreeze of a sub-account. Use the frozen boolean to distinguish direction. When a partner API key is revoked by MyStocks, this fires without subAccountId and instead includes revokedBy and reason.
{
"event": "account.frozen",
"timestamp": "2026-06-06T12:00:00Z",
"data": {
"subAccountId": "usr_abc123",
"externalId": "user_42",
"frozen": true,
"frozenBy": "platform_admin"
}
}dividend.paid
Fired once per dividend distribution batch, grouped by symbol. The distributions array lists each sub-account that received a payout.
{
"event": "dividend.paid",
"timestamp": "2026-06-06T09:00:00Z",
"data": {
"symbol": "SCOM.KE",
"name": "Safaricom PLC",
"dividendPerShare": 0.00065,
"currency": "KES",
"totalUsdPaid": 0.65,
"distributionCount": 2,
"distributions": [
{
"subAccountId": "usr_abc123",
"externalId": "user_42",
"units": 1000,
"usdYield": 0.5
},
{
"subAccountId": "usr_def456",
"externalId": "user_99",
"units": 200,
"usdYield": 0.15
}
]
}
}incident.declared
Broadcast to all active partners when MyStocks declares a platform incident. Severity levels: P0 (critical) → P3 (minor).
{
"event": "incident.declared",
"timestamp": "2026-06-06T14:00:00Z",
"data": {
"id": "inc_abc123",
"title": "NGX market data delay",
"severity": "P2",
"affectedServices": [
"market-data",
"order-execution"
],
"status": "investigating",
"startedAt": "2026-06-06T14:00:00Z"
}
}incident.resolved
Broadcast to all active partners when an incident is closed. duration is a human-readable string (e.g. 2h 30m).
{
"event": "incident.resolved",
"timestamp": "2026-06-06T16:30:00Z",
"data": {
"id": "inc_abc123",
"title": "NGX market data delay",
"severity": "P2",
"status": "resolved",
"resolvedAt": "2026-06-06T16:30:00Z",
"duration": "2h 30m"
}
}Signature verification
Every delivery includes an x-mystocks-signature header containing an HMAC-SHA256 hex digest of the raw request body signed with your webhook secret. Always verify using a constant-time comparison to prevent timing attacks.
const crypto = require('crypto');
app.post('/webhooks/mystocks', express.raw({ type: '*/*' }), (req, res) => {
const sig = req.headers['x-mystocks-signature'];
const expected = crypto
.createHmac('sha256', process.env.MYSTOCKS_WEBHOOK_SECRET)
.update(req.body) // raw Buffer — do NOT parse JSON first
.digest('hex');
const verified = crypto.timingSafeEqual(
Buffer.from(sig, 'utf8'),
Buffer.from(expected, 'utf8'),
);
if (!verified) return res.status(401).send('Invalid signature');
const event = JSON.parse(req.body);
// handle event.event, event.data ...
res.status(200).send('OK');
});/webhooks/webhooks/{id}/webhooks/{id}/test/webhooks/{id}/deliveriesList registered webhooks, delete one, fire a test event, or inspect delivery history. POST /webhooks/{id}/test sends a test.event payload to the webhook URL immediately and logs the delivery — useful for verifying endpoint reachability and signature verification before going live. /deliveries returns each attempt with HTTP status code, response body snippet, duration (ms), and the retry schedule if the delivery failed. Retry schedule: immediate → 5 s → 30 s → 5 min → 30 min → 2 h (6 attempts max).
# Fire a test.event delivery immediately curl -X POST "https://mystocks.africa/api/v1/partner/webhooks/wh_abc123/test" \ -H "Authorization: Bearer pk_live_<key>" # Inspect delivery log curl "https://mystocks.africa/api/v1/partner/webhooks/wh_abc123/deliveries?limit=10" \ -H "Authorization: Bearer pk_live_<key>"
{
"deliveries": [
{
"id": "del_abc",
"eventId": "evt_abc123",
"event": "order.filled",
"status": 200,
"duration": 142,
"attemptedAt": "2026-06-04T11:30:01Z",
"success": true
},
{
"id": "del_def",
"eventId": "evt_def456",
"event": "deposit.confirmed",
"status": 503,
"duration": 8000,
"attemptedAt": "2026-06-04T10:00:00Z",
"success": false,
"nextRetryAt": "2026-06-04T10:05:00Z"
}
],
"count": 2,
"hasMore": false,
"nextCursor": null
}Reports & Analytics
Portfolio snapshots, revenue reporting, dividends, and audit logs across all sub-accounts. All reporting endpoints are read-only and safe to call on every page load.
/report/aumTotal assets under management across all sub-accounts, broken down by asset class (equities, bonds, funds, opportunities) and exchange. Supports ?format=csv for spreadsheet export.
{
"totalAum": 45230.5,
"currency": "USD",
"breakdown": {
"equities": 38400,
"bonds": 4200,
"funds": 2630.5,
"opportunities": 0
},
"byExchange": {
"NSE": 22100,
"NGX": 10300,
"JSE": 6000
}
}/report/positionsAll open equity positions across all sub-accounts. Use for a global portfolio overview or compliance snapshot. Supports ?exchange=, ?symbol=, and ?format=csv.
{
"positions": [
{
"subAccountId": "usr_abc123",
"externalId": "user_42",
"symbol": "SCOM.KE",
"quantity": 1000,
"avgCostUsd": 0.01269,
"currentPriceUsd": 0.0126,
"marketValue": 12.6,
"unrealizedPnl": -0.09
}
],
"totalValue": 38400,
"count": 1
}/report/feesTrading fees paid across all sub-accounts — base fees and any partner markup. Filter by ?from= / ?to= date range. Supports ?format=csv.
{
"period": {
"from": "2026-05-01",
"to": "2026-05-31"
},
"totalBaseFees": 62.25,
"totalMarkupFees": 18.75,
"totalFees": 81,
"tradeCount": 83,
"currency": "USD"
}/report/revenuePartner markup revenue breakdown per trade, with net payable. Filter by ?from= / ?to= date range. Supports ?format=csv.
{
"period": {
"from": "2026-05-01",
"to": "2026-05-31"
},
"totalMarkupRevenue": 124.5,
"totalTrades": 83,
"netPayable": 124.5,
"currency": "USD"
}/report/dividendsAggregated dividend report across all sub-accounts. Includes declared amount, USD conversion, and per-account breakdown. Supports ?from= / ?to=.
{
"dividends": [
{
"subAccountId": "usr_abc123",
"symbol": "SCOM.KE",
"declaredAt": "2026-05-20",
"amountPerShare": 0.00065,
"sharesHeld": 1000,
"totalUsd": 0.65,
"creditedAt": "2026-05-25T09:00:00Z"
}
],
"count": 1
}/report/invoiceMonthly fee statement with line items for platform fees, markups, and adjustments. Pass ?month=YYYY-MM to select a specific month. Supports ?format=csv.
{
"month": "2026-05",
"invoiceId": "inv_202605_prt_abc123",
"lineItems": [
{
"description": "Platform base fees",
"amount": 62.25
},
{
"description": "Partner markup revenue",
"amount": 124.5
}
],
"totalDue": 62.25,
"currency": "USD",
"status": "ISSUED"
}/dividends/calendarUpcoming dividend declarations for stocks traded on supported exchanges. partnerEligible: true flags dividends where at least one of your sub-accounts holds the stock.
{
"dividends": [
{
"symbol": "EQTY.KE",
"name": "Equity Group Holdings",
"declarationDate": "2026-07-15",
"exDividendDate": "2026-07-22",
"paymentDate": "2026-08-01",
"amountPerShare": 4,
"currency": "KES",
"partnerEligible": true
}
],
"count": 1
}/auditPaginated API call log with endpoint, method, status code, IP address, user-agent, and latency. Includes key management events (key.rotated, key.revoked, settings.updated). Supports ?from= / ?to= and cursor pagination.
{
"entries": [
{
"id": "aud_abc",
"endpoint": "/users/usr_abc123/trade",
"method": "POST",
"status": 202,
"ip": "102.89.1.1",
"latencyMs": 234,
"createdAt": "2026-06-04T09:45:00Z"
}
],
"count": 1,
"hasMore": true,
"nextCursor": "cursor_xyz"
}/usage90-day rolling API call analytics: requests per day, error rate, and current rate-limit window status. Use to monitor usage against your tier ceiling.
{
"tier": "starter",
"limitPerMinute": 100,
"currentWindow": {
"requestsUsed": 43,
"requestsRemaining": 57,
"resetsAt": "2026-06-04T09:46:00Z"
},
"daily": [
{
"date": "2026-06-04",
"requests": 1240,
"errors": 3
}
]
}SLA & Service Health
Real-time platform health status, active incidents, scheduled maintenance windows, and your partner SLA tier. Use to drive status banners in your app or alert your users when market data or order execution is degraded.
/slaReturns overall platform health, a list of any active incidents with severity and affected services, upcoming maintenance windows, and your SLA tier entitlements. Incidents are also broadcast via incident.declared and incident.resolved webhooks.
curl "https://mystocks.africa/api/v1/partner/sla" \ -H "Authorization: Bearer pk_live_<key>"
{
"status": "OPERATIONAL",
"tier": "starter",
"incidents": [],
"maintenance": [
{
"id": "mnt_abc123",
"title": "NGX data feed upgrade",
"scheduledStart": "2026-06-15T02:00:00Z",
"scheduledEnd": "2026-06-15T04:00:00Z",
"affectedServices": [
"market-data"
],
"status": "SCHEDULED"
}
],
"services": [
{
"name": "Market Data",
"status": "OPERATIONAL"
},
{
"name": "Order Execution",
"status": "OPERATIONAL"
},
{
"name": "Webhooks",
"status": "OPERATIONAL"
},
{
"name": "Sub-Accounts",
"status": "OPERATIONAL"
}
],
"checkedAt": "2026-06-09T10:00:00Z"
}Incident severity levels
| Level | Name | Impact |
|---|---|---|
| P0 | Critical | Full platform outage — all trading and market data unavailable. |
| P1 | Major | Core service degraded — order execution or market data impaired. |
| P2 | Minor | Partial degradation — some exchanges or endpoints affected. |
| P3 | Informational | Maintenance or cosmetic issue with no user impact. |
Interactive API Explorer
Try any endpoint live in the browser — generate code snippets in JavaScript, Python, Go, and 10+ other languages, and send requests directly against the sandbox.
API Versioning
The Partner API is versioned via the URL path. The current stable version is v1. The sandbox mirrors the same versioning scheme.
Production
https://mystocks.africa/api/v1/partnerSandbox
https://mystocks.africa/api/sandbox/v1Backwards-compatible changes (no version bump)
These changes will not break your existing integration. We make them freely within v1:
- Adding new optional fields to request bodies.
- Adding new fields to response objects.
- Adding new endpoints or HTTP methods.
- Adding new webhook event types.
- Adding new query parameters (all optional).
- Adding new error codes for new scenarios.
Breaking changes (require version bump to v2)
- Renaming or removing existing fields from request/response bodies.
- Changing the type of an existing field (e.g. number → string).
- Removing or renaming endpoints or HTTP methods.
- Changing authentication scheme.
- Changing error code semantics for existing scenarios.
Deprecation policy
Deprecated endpoints and fields receive a minimum 90-day sunset notice. You will be notified via:
- Email to your registered partner address.
- Deprecation response header on every call to the affected endpoint.
- A Sunset response header with the removal date.
- A changelog entry marked Deprecated.
# Headers present on deprecated endpoints: Deprecation: true Sunset: Sat, 01 Aug 2026 00:00:00 GMT Link: <https://mystocks.africa/partners/docs#changelog>; rel="deprecation"
Check for the Deprecation: true header in your HTTP client to detect deprecated usage before the sunset date.
Going Live — Production API
Once your integration is tested, switch to the production endpoints. The request and response shapes are identical — only the base URL and key format change.
| Sandbox | Production | |
|---|---|---|
| Base URL | https://mystocks.africa/api/sandbox/v1 | https://mystocks.africa/api/v1/partner |
| API key prefix | sk_sandbox_ | pk_live_ |
| Trade settlement | Instant (no queue) | PENDING → reviewed within 4 h → COMPLETED / REJECTED |
| Stock prices | Live market prices (production read-through) | Live market prices |
| Wallet funding | Auto $100k on register | Admin deposits real funds |
| Reset endpoint | Available | Not available |
Onboarding steps
- 1Register at mystocks.africa/partners/register and submit your application.
- 2MyStocks reviews your application within 1–2 business days.
- 3Upon approval, you receive your pk_live_ API key and access to the partner dashboard.
- 4Swap base URL and key prefix — no other code changes needed.
- 5Submit trades via POST /api/v1/partner/trade → orders appear in the admin review queue.
- 6Orders are reviewed within 4 business hours during exchange hours. Monitor status via GET /api/v1/partner/orders/{orderId} or subscribe to order.filled / order.rejected webhooks. Settlement cycles by exchange: GET /api/v1/partner/market/settlement.
Pre-launch checklist
Security
- ☐API key stored in environment variables — never committed to source control.
- ☐Webhook endpoint validates HMAC-SHA256 signature using timingSafeEqual before processing events.
- ☐Sub-account IDs are treated as internal identifiers — never exposed directly to end-users.
Reliability
- ☐Idempotency-Key set on all deposit, withdraw, and trade requests.
- ☐Exponential backoff implemented for 429 and 500 responses.
- ☐Webhook endpoint returns 2xx within 8 seconds; heavy processing deferred to a background queue.
- ☐Webhook delivery failures retried from the Webhook Deliveries endpoint or partner dashboard.
Integration correctness
- ☐All sandbox test scenarios passed: BUY, SELL, insufficient funds, KYC rejection.
- ☐Order status polling (or webhook) handles PENDING, FILLED, REJECTED, and CANCELLED states.
- ☐FX rate recorded on each deposit and withdrawal for your own audit trail.
- ☐Sub-account KYC tier verified before allowing trades above tier-1 limits.
- ☐Error codes 401, 403, 422, and 429 handled gracefully in your UI.
Observability
- ☐Audit log endpoint (GET /api/v1/partner/audit) integrated into your compliance reporting.
- ☐Usage analytics (GET /api/v1/partner/usage) monitored to stay within rate-limit tier.
- ☐Alert configured when X-RateLimit-Remaining drops below 20% of your tier ceiling.
Changelog
All notable API changes. Entries marked Deprecated will be removed on the date listed in the Sunset header — see API Versioning for the deprecation policy.
- addedGET /market/movers — paginated top gainers/losers per exchange. Parameters: exchange (required), direction (gainers|losers), limit (1–100), page. Response envelope includes meta.total_count, meta.has_next for cursor-free pagination.
- changedList endpoints /orders, /users, /users/{id}/orders, and /users/{id}/transactions use cursor pagination (cursor + nextCursor + hasMore); the OpenAPI spec and API Tester now reflect this instead of the legacy page parameter.
- fixedBatch quotes volume field: returns integer 0 (not null) for instruments with no recorded trades today.
- fixedBatch overflow error now returns code BATCH_LIMIT_EXCEEDED with max and received fields instead of the generic VALIDATION_ERROR.
- fixedBatch mode now validates each symbol against the format regex before hitting Firestore — malformed symbols (e.g. "[]") return 400 INVALID_SYMBOL with an invalid[] list.
- fixedSending ?symbols[]= (PHP/Ruby array bracket notation) returns a clear VALIDATION_ERROR pointing to the correct ?symbols=A,B,C syntax.
- changedError codes table expanded: MISSING_PARAM, INVALID_SYMBOL, UNKNOWN_EXCHANGE, INVALID_TYPE, and BATCH_LIMIT_EXCEEDED are now documented alongside their extra body fields.
- fixedBatch quotes response example corrected: notFound → not_found, notFoundCount → not_found_count to match actual API shape.
- addedOHLCV in chart endpoints: GET /stocks/{symbol}/chart, GET /etfs/{symbol}/chart, and GET /companies/{symbol}/chart now return open, high, low, close, volume on every priceHistory candle, plus parallel opens[], highs[], lows[] arrays alongside the existing prices[] and volumes[].
- addedopen field on stock and ETF detail: GET /stocks/{symbol} and GET /etfs/{symbol} now include the session opening price alongside dayHigh, dayLow, volume, and previousClose.
- fixedMissing logo SVGs now return a branded placeholder (two-letter ticker initials on a grey background) instead of an Application Error page, ensuring GET /logos/{slug}.svg always returns valid SVG content.
- addedExchange Traded Funds (ETFs) support: GET /etfs, GET /etfs/{symbol}, GET /etfs/{symbol}/history, GET /etfs/{symbol}/chart under the new ETFs namespace.
- addedPrice Charts: GET /stocks/{symbol}/chart and GET /etfs/{symbol}/chart returning pre-computed chart data (labels, prices, volumes, and objects list) with primary price feeds in local currency.
- addedMulti-Symbol Batch Market Quotes: GET /market/quotes supporting up to 50 stock or ETF symbols with concurrent parallel Firestore fetching.
- addedSandbox Scenario Simulation Headers: X-Sandbox-Force-Status and X-Sandbox-Force-Error to programmatically trigger failures and test retry logic.
- changedStructured Error model: transitioned global error responses to nested machine-readable application sub-error structure {"error": {"code", "message"}}.
- addedCryptographic Timing-Safe Signature Verification: embedded Express.js and FastAPI HMAC-SHA256 signature verification code examples using constant-time comparison algorithms.
- addedPOST /api/v1/partner/sandbox/deposit — credit virtual USD to a sandbox sub-account instantly. No master wallet deduction. Fires deposit.confirmed webhook with isSandbox: true.
- addedPOST /api/v1/partner/sandbox/trade — submit test trades for end-to-end webhook integration testing without moving real funds.
- addedGET /api/v1/partner/sandbox/orders — list and filter your sandbox test order history.
- addedAll sandbox webhook payloads include isSandbox: true to distinguish from production events.
- addedSandbox tab in the Partner Dashboard — fund test accounts, submit trades, and track order status without leaving the portal.
- addedSandbox Simulation Cheat Codes — submit trade quantities of 100 to instantly auto-fill, or 999 to instantly auto-reject.
- addedPOST /auto-register — idempotent sub-account provisioning; safe to call on every user login.
- addedGET /quote/{symbol} — real-time fee quote (gross, baseFee, partnerMarkupFee, totalCost) before order submission.
- addedGET /market/status — open/closed state for all exchanges with nextOpen timestamp.
- addedWebhook events: wallet.credited, incident.declared, incident.resolved.
- addedGET /webhooks/{id}/deliveries — cursor-paginated delivery history with HTTP status and duration per attempt.
- addedQuick Start, FX & Currency model, and Rate Limits sections added to documentation.
- changedDeposit body now accepts localAmount, localCurrency, fxRate for partner audit trail (previously accepted but silently ignored).
- fixedWebhook signature verification example updated to use timingSafeEqual — previous string === comparison was vulnerable to timing attacks.
- addedCompany endpoints: GET /companies, /companies/{symbol}, /companies/{symbol}/chart, /companies/{symbol}/news, /companies/tickers.
- addedGET /usage — 90-day API call analytics with current rate-limit window status.
- addedGET /audit — paginated API call log with IP, user-agent, and endpoint per call.
- addedPartner settings: markupBps — configure your own markup fee (basis points) on top of the 0.75% MyStocks base fee.
- addedPartner settings: SMTP configuration for partner-branded transactional emails (POST /settings for SMTP test).
- addedGET /report/revenue — partner earnings report (markups, referral fees, net payable).
- addedGET /report/invoice — monthly invoice with line items.
- addedPOST /api-keys/rotate and POST /api-keys/revoke for key lifecycle management.
- addedGET /api-keys/data-key — read-only key for safe use in client-side code.
- changedTrade request body now accepts optional stopLoss and takeProfit price thresholds.
- addedPOST /users/{userId}/subscribe — unified subscription for bonds, funds, private deals, and pre-IPO.
- addedPOST /users/{userId}/redeem — instant redemption for funds with redemptionType: instant.
- addedGET /dividends/calendar — upcoming dividend declarations with partnerEligible flag.
- addedGET /report/dividends — aggregated dividend report across all sub-accounts.
- addedWebhook event: dividend.paid.
- addedBonds & Fixed Income: GET /bonds, GET /bonds/{id} with yield curve.
- addedFunds & ETFs: GET /funds, GET /funds/{id} with distribution config.
- addedPrivate Credit & Pre-IPO: GET /opportunities, GET /opportunities/{id}.
- addedPOST /users/{userId}/kyc — KYC assertion endpoint for partner-conducted identity verification.
- addedInitial release: equity trading, sub-accounts, USD wallets, deposits, withdrawals, and webhooks.
- addedGET /stocks, GET /stocks/{symbol}/pulse, GET /portfolio.
- addedPOST /users, GET /users, GET /users/{userId}, PATCH /users/{userId}.
- addedPOST /users/{userId}/trade, GET /users/{userId}/orders, GET /users/{userId}/portfolio.
- addedPOST /webhooks, GET /webhooks. Events: trade.settled, trade.rejected, deposit.confirmed, withdraw.confirmed, kyc.updated, account.frozen.
Data Licensing
Market data accessed via the Partner API is sourced from licensed exchange feeds and MyStocks Africa's own research. Your use of that data is governed by your Partner Agreement and the terms below.
Permitted use
- ✓Display stock prices, quotes, and market data to your end-users within your application.
- ✓Cache price data for up to 24 hours to reduce API calls.
- ✓Display company profiles, logos, and descriptions as returned by the API.
- ✓Run portfolio analytics and performance calculations using your sub-accounts' data.
- ✓Use market intelligence articles as editorial content in your product.
- ✓Build derived data products (e.g. portfolio value, gain/loss) for your users.
Restricted use
- ✗Reselling or redistributing raw exchange data (prices, OHLCV, ticks) to third parties.
- ✗Creating a data feed, data product, or market data service using MyStocks data.
- ✗Sublicensing API access — all end-users must flow through MyStocks sub-accounts.
- ✗Bulk-exporting the full instrument catalogue beyond normal application use.
- ✗Using data to train machine learning models for resale without a separate data agreement.
- ✗Displaying data in a way that removes or obscures required exchange attribution.
Attribution requirements
When displaying market data to end-users, include the following attribution in a reasonably visible location in your UI:
Market data provided by MyStocks Africa
Some exchanges (NSE, JSE) require their own attribution on data derived from their feeds. MyStocks will notify you if a specific exchange imposes additional requirements.
Data accuracy & disclaimer
- Production API prices are real-time or near-real-time as received from exchange partners. Latency is typically under 60 seconds during trading hours.
- Sandbox prices are static test values and do not reflect any live market.
- MyStocks Africa makes no warranty of accuracy, completeness, or fitness for any particular purpose.
- Partners are solely responsible for compliance with applicable securities laws and regulations in their jurisdictions.
- Market data must not be used as the sole basis for investment advice to end-users without appropriate regulatory authorisation.
Intellectual property
Editorial content — company descriptions, market intelligence articles, sector summaries, and data enrichments — is the intellectual property of MyStocks Africa Limited. Exchange price data is subject to each exchange's own data license terms, which are incorporated into your Partner Agreement by reference.
Bulk data & research licensing
For bulk historical data exports, academic research, index construction, or commercial data partnerships, contact data@mystocks.africa. A separate data license agreement is required.