API Reference

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 Tester

Quick Start

5 min

Go 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

bash
curl -X POST https://mystocks.africa/api/sandbox/v1/register \
  -H "Content-Type: application/json" \
  -d '{ "businessName": "Acme", "email": "dev@acme.com" }'
json
{
  "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).

bash
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"
  }'
json
{
  "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.

bash
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"
  }'
json
{
  "message": "Deposit successful.",
  "subAccountId": "usr_xxxxxxxxxxxx",
  "amount": 500,
  "currency": "USD",
  "newSubBalance": 500,
  "newMasterBalance": 99500
}

Step 4 — Check the stock price

bash
curl "https://mystocks.africa/api/sandbox/v1/partner/stocks/SCOM.KE" \
  -H "Authorization: Bearer sk_sandbox_<your_key>"
json
{
  "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.

bash
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
  }'
json
{
  "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)

bash
curl "https://mystocks.africa/api/sandbox/v1/partner/users/usr_xxxxxxxxxxxx/orders?limit=1" \
  -H "Authorization: Bearer sk_sandbox_<your_key>"
json
{
  "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

PhaseTimelineNotes
MyStocks order reviewWithin 4 business hoursOrders submitted during market hours. Outside hours → queued for next session.
Exchange settlement (most exchanges)T+3 business daysNSE, NGX, JSE, GSE, BRVM, ZSE, BSE, LUSE — title transfers 3 business days after execution.
Exchange settlement (EGX)T+2 business daysEgyptian 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

POST /trade
submit order
PENDING
funds escrowed
PROCESSING
admin reviewing
or
COMPLETED
shares/proceeds credited · order.filled
REJECTED
BUY escrow refunded · order.rejected
CANCELLED
partner DELETE while PENDING · order.cancelled

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).

bash
# 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.

SymbolNameExchangeCurrencyExample (live price)
SCOM.KESafaricom PLCNSEKESlive KES price
EQTY.KEEquity Group HoldingsNSEKESlive KES price
DANGCEM.NGDangote CementNGXNGNlive NGN price
ZENITH.NGZenith BankNGXNGNlive NGN price
MTN.ZAMTN GroupJSEZARlive ZAR price
GCBBANK.GHGCB BankGSEGHSlive GHS price
SNTS.BFSonatelBRVMXOFlive XOF price
ZANACO.ZMZambia National Commercial BankLUSEZMWlive 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.

1

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.

StepEndpointWhat it does
1POST /usersCreate an isolated sub-account (wallet + order book) for each user.
2POST /users/:id/kycAssert KYC status from your own verification flow. Required before trading.
3POST /users/:id/depositMove USD from your master wallet to the sub-account. Pass Idempotency-Key.
4GET /market/quotes?symbol=SCOM.KEFetch live price + fee quote before showing the trade screen.
5POST /users/:id/tradePlace a BUY or SELL order. Responds 202 — order is PENDING, funds escrowed.
6GET /users/:id/orders/:orderIdPoll for status changes (PENDING → PROCESSING → COMPLETED).
7Webhook: order.filled / order.rejectedReceive settlement notification instead of polling. Recommended over step 6.
8GET /users/:id/portfolioShow updated holdings and unrealised P&L after settlement.
javascript
const BASE = 'https://mystocks.africa/api/v1/partner';
const KEY  = process.env.MYSTOCKS_API_KEY;
const h    = { 'x-api-key': KEY, 'Content-Type': 'application/json' };

// 1. Create sub-account
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.
2

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.

StepEndpointWhat it does
1GET /users/:id/portfolioLive holdings: units, avg cost, current USD price, unrealised P&L.
2GET /market/quotes?symbols=A,B,CRefresh prices for all held symbols in one batch request (max 50).
3GET /stocks/:symbol/chartLoad price history chart data for each holding (1W, 1M, 3M, 1Y).
4GET /users/:id/dividendsShow dividend income history with per-share amounts and pay dates.
5GET /dividends/calendarShow upcoming dividend declarations for stocks the user holds.
6GET /report/aumPartner-level: aggregate AUM across all sub-accounts (CSV export available).
7GET /report/positionsOpen positions summary by symbol across all sub-accounts.
8Webhook: dividend.paidNotify users the moment a dividend is credited to their wallet.
javascript
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.
3

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.

StepEndpointWhat it does
1POST /api-keys/data-keyCreate a read-only pk_data_ key safe to embed in client code.
2GET /market/quotes?symbols=A,B,CFetch live prices for up to 50 symbols in one call. Poll every 15–30 s.
3GET /stocks/:symbol/chart?period=1MLoad pre-shaped chart data (open, close, change %) for a given period.
4GET /stocks/:symbol/history?period=1MLoad raw OHLCV candles for custom chart rendering.
5GET /market/movers?direction=gainersTop gainers or losers by exchange. Refresh once per minute.
6GET /market/statusShow whether an exchange is open or closed before displaying prices.
7GET /companies/:symbolCompany profile, fundamentals, logo URL, and last 20 corporate actions.
8GET /market-intelEditorial market intelligence articles to add context alongside prices.
javascript
// 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)

http
# Sandbox
Authorization: Bearer sk_sandbox_<your_key>

# Production
Authorization: Bearer pk_live_<your_key>

Option B — Custom header

http
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)

/stocks/stocks/{symbol}/stocks/{symbol}/chart/stocks/{symbol}/history/stocks/{symbol}/pulse/etfs/etfs/{symbol}/etfs/{symbol}/chart/etfs/{symbol}/history/market/quotes/market/status/market/movers/market/holidays/market/settlement/bonds/bonds/{id}/funds/funds/{id}/companies/companies/{symbol}/companies/{symbol}/chart/companies/{symbol}/news/companies/tickers/market-intel/market-intel/{id}/opportunities/dividends/calendar

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.

http
POST /api/v1/partner/users/{userId}/deposit
Authorization: Bearer pk_live_<your_key>
Idempotency-Key: dep_riven_user_42_1743152580   # unique per attempt

Use 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.

POST/api-keys/rotate

Generate 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.

bash
curl -X POST "https://mystocks.africa/api/v1/partner/api-keys/rotate" \
  -H "Authorization: Bearer pk_live_<current_key>"
json
{
  "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.

POST/api-keys/revoke

Permanently 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.

bash
curl -X POST "https://mystocks.africa/api/v1/partner/api-keys/revoke" \
  -H "Authorization: Bearer <firebase-id-token>"
json
{
  "message": "Key revoked. Contact support@mystocks.africa to issue a replacement.",
  "revokedAt": "2026-06-09T10:05:00Z"
}
POST/api-keys/data-key
GET/api-keys/data-key
DELETE/api-keys/data-key

Create, 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.

bash
curl -X POST "https://mystocks.africa/api/v1/partner/api-keys/data-key" \
  -H "Authorization: Bearer pk_live_<key>"
json
{
  "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.

GET/settings
PATCH/settings

Fetch or update partner settings. SMTP password is never returned — only a hasPassword: true boolean confirms it is set.

FieldTypeRequiredDescription
logoUrlstringNoHTTPS URL to your company logo (PNG or SVG). Shown in the dashboard and partner-branded emails.
smtp.hoststringNoSMTP server hostname.
smtp.portintegerNoSMTP port (e.g. 587 for STARTTLS, 465 for implicit TLS).
smtp.userstringNoSMTP username.
smtp.passwordstringNoSMTP password. Write-only — never returned in GET responses.
smtp.fromstringNoFrom address for outbound emails, e.g. noreply@acme.com.
bash
curl "https://mystocks.africa/api/v1/partner/settings" \
  -H "Authorization: Bearer pk_live_<key>"
json
{
  "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"
}
POST/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.

bash
curl -X POST "https://mystocks.africa/api/v1/partner/settings" \
  -H "Authorization: Bearer pk_live_<key>"
json
{
  "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.

FlowWho handles FX?What you provideWhat MyStocks does
Funding (deposit)Partneramount + localAmount + fxRateRecords for audit trail; credits USD wallet
Equity tradingMyStocksApplies live spot rate at execution; settles in local market currency
DividendsMyStocksConverts declared dividend to USD at spot; credits sub-account wallet
WithdrawalPartneramount + fxRate appliedDebits 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.

json
// 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

New

Expand 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.

bash
curl "https://mystocks.africa/api/sandbox/v1/partner/etfs?exchange=JSE" \
  -H "Authorization: Bearer sk_sandbox_<your_key>"
json
{
  "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=1y

ETFs Charts

GET /etfs/STXEME.ZA/chart?period=3m
bash
curl "https://mystocks.africa/api/sandbox/v1/partner/etfs/STXEME.ZA/chart?period=3m" \
  -H "Authorization: Bearer sk_sandbox_<your_key>"
json
{
  "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.

bash
curl "https://mystocks.africa/api/sandbox/v1/partner/market/quotes?symbols=SCOM.KE,GLD.ZA,INVALID_XYZ" \
  -H "Authorization: Bearer sk_sandbox_<your_key>"
json
{
  "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.

bash
curl "https://mystocks.africa/api/sandbox/v1/partner/market/status?exchange=NSE" \
  -H "Authorization: Bearer sk_sandbox_<your_key>"

Normal trading day — open

json
{
  "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

json
{
  "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

exchange — required, e.g. NSE
direction — gainers | losers (default: gainers)
limit — 1–100, default 10
page — 1-based, default 1

Meta envelope

total_count — total movers across all pages
page — current page number
per_page — items on this page
has_next — true if more pages remain
bash
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>"
json
{
  "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

exchange — optional, e.g. NSE. Omit for all.
from — YYYY-MM-DD, defaults to today
to — YYYY-MM-DD, defaults to +12 months

Supported exchanges

NSENGXJSEGSEBRVMZSEBSELUSEEGX
bash
# 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>"
json
{
  "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.

bash
curl "https://mystocks.africa/api/sandbox/v1/partner/market-data/exchanges" \
  -H "Authorization: Bearer sk_sandbox_<key>"
json
{
  "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.

FieldTypeRequiredDescription
symbolstringYesExchange-qualified symbol, e.g. SCOM.KE.
exchangestringYesExchange code, e.g. NSE.
fromstringNoStart date (YYYY-MM-DD, UTC).
tostringNoEnd date (YYYY-MM-DD, UTC).
intervalstringNoCandle interval — currently only 1d supported.
bash
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>"
json
{
  "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., 429 Rate Limited, 500 Internal 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)

json
{
  "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)

json
{
  "error": {
    "code": "NOT_FOUND",
    "message": "Stock SCOM.KE not found."
  }
}

Validation error (400)

json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Ambiguous symbol \"MTN\" matches multiple listings: MTN.GH, MTN.NG. Use an exchange-qualified symbol."
  }
}

Server error (500)

json
{
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "Upstream data source unavailable. Retry with exponential backoff."
  }
}
FieldTypeRequiredDescription
200OKNoRequest succeeded.
201CreatedNoResource created (register, create sub-account).
202AcceptedNoOrder accepted and queued for admin review. Returned by trade, subscribe. Funds are escrowed immediately; settlement is asynchronous.
400Bad RequestNoMissing or invalid parameters — check the error message.
401UnauthorizedNoAPI key missing, invalid, revoked, or expired.
403ForbiddenNoAction not permitted. Causes: partner account not yet approved, sub-account frozen, or attempting an operation outside your tier.
404Not FoundNoStock symbol, sub-account, holding, or instrument not found.
409ConflictNoIdempotency collision — a concurrent request with the same Idempotency-Key is still in progress. Retry after the first request resolves.
422UnprocessableNoBusiness rule violation: insufficient wallet balance, KYC required, sub-account frozen, fund not open for subscription, or units exceed available holdings.
429Too Many RequestsNoRate limit exceeded. Check X-RateLimit-Remaining and X-RateLimit-Reset headers. Default: 100 req/min (starter), 500 (growth), 2000 (enterprise).
500Internal Server ErrorNoServer-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.

FieldTypeRequiredDescription
AUTH_MISSING401NoNo API key supplied. Send Authorization: Bearer pk_live_... or X-Api-Key header.
AUTH_INVALID401NoKey format is wrong or the key does not exist in the registry.
AUTH_SUSPENDED403NoPartner account is temporarily suspended. Contact support@mystocks.africa.
AUTH_REVOKED403NoKey permanently revoked. A new key must be issued — revocation cannot be undone.
RATE_LIMITED429NoToo many requests. Back off and retry after the X-RateLimit-Reset timestamp.
MISSING_PARAM400NoA required query or body parameter is absent. Check error.param (or error.params[]) for the field name.
INVALID_SYMBOL400NoSymbol 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_EXCHANGE400NoExchange code not recognised. Supported codes: NSE, NGX, JSE, GSE, BRVM, ZSE, BSE, LUSE, EGX.
INVALID_TYPE400NoA parameter has the wrong type — e.g. a numeric value was passed where a string exchange code is required.
VALIDATION_ERROR400NoRequest body or query param failed validation. Check error.message for the specific field.
BATCH_LIMIT_EXCEEDED400NoBatch request exceeds the maximum symbol count. Check error.max for the limit and error.received for how many were sent.
NOT_FOUND404NoRequested resource (stock, sub-account, order, instrument) does not exist.
CONFLICT409NoConcurrent request conflict: idempotency-key collision or order state changed before the operation completed.
INSUFFICIENT_FUNDS400NoWallet balance too low. Top up the master wallet or deposit funds into the sub-account first.
KYC_REQUIRED403NoSub-account KYC is not verified. Assert KYC via POST /users/{userId}/kyc then retry.
FORBIDDEN403NoAuthenticated but not authorised. Common causes: read-only data key used on a write endpoint, or operation outside your tier.
INTERNAL_ERROR500NoUnexpected 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.

FieldTypeRequiredDescription
INSUFFICIENT_FUNDSstringNoSub-account or master wallet had insufficient balance at time of review.
KYC_REQUIREDstringNoSub-account KYC is not verified. Assert via POST /users/{userId}/kyc before retrying.
MARKET_CLOSEDstringNoThe exchange was closed or on a public holiday when the order was reviewed.
COMPLIANCE_HOLDstringNoOrder flagged for compliance review. Contact support.
TECHNICAL_ISSUEstringNoA technical issue prevented execution. Safe to retry.
OTHERstringNoReason 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.

TierRequests / minNotes
Sandbox / Starter100Default for all new sandbox and starter accounts.
Growth500Available on the Growth plan. Contact us to upgrade.
Enterprise2,000Custom limits available above 2,000 req/min — contact partnerships@mystocks.africa.

Rate-limit headers (every response)

http
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

json
{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded. Tier: starter (100 req/min). Resets in 17s."
  }
}

Backoff example (Node.js)

javascript
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

FieldTypeRequiredDescription
limitintegerNoItems per page. Default and max vary by endpoint (see table below). Clamped to the endpoint maximum if exceeded.
cursorstringNoOpaque 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

FieldTypeRequiredDescription
countintegerNoNumber of items in this page (≤ limit).
hasMorebooleanNotrue if at least one more page exists. false means this is the last page.
nextCursorstring | nullNoPass as ?cursor= on the next request. null when hasMore is false.

Limit defaults and maximums by endpoint

EndpointDefaultMax
GET /users100500
GET /orders50200
GET /users/{userId}/orders50200
GET /users/{userId}/transactions50200
GET /audit50200
GET /webhooks/{id}/deliveries20100

Fetching all pages — JavaScript example

javascript
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.

GET/stocks

Returns 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.

FieldTypeRequiredDescription
exchangestringNoFilter by exchange code: NSE, NGX, JSE, GSE, BRVM, LUSE, USE, DSE, EGX, BSE, ZSE.
sectorstringNoFilter by sector name (e.g. Banking, Telecommunications).
listingStatusstringNoACTIVE (default), SUSPENDED, or DELISTED.
limitintegerNoItems per page. Default 100, max 500.
cursorstringNoPagination cursor from the previous response.
bash
curl "https://mystocks.africa/api/sandbox/v1/partner/stocks?exchange=NSE&limit=5" \
  -H "Authorization: Bearer sk_sandbox_<key>"
json
{
  "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"
}
GET/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).

bash
curl "https://mystocks.africa/api/sandbox/v1/partner/stocks/SCOM.KE" \
  -H "Authorization: Bearer sk_sandbox_<key>"
json
{
  "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"
}
GET/stocks/{symbol}/chartalso: /etfs/{symbol}/chart

Pre-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.

FieldTypeRequiredDescription
periodstringYes1d, 1w, 1m, 3m, 6m, 1y, or 5y.
GET/stocks/{symbol}/pulse

Recent news headlines, corporate announcements, and analyst sentiment for a stock. Use to populate a “News” tab or price-movement explanation in your UI.

bash
curl "https://mystocks.africa/api/sandbox/v1/partner/stocks/SCOM.KE/pulse" \
  -H "Authorization: Bearer sk_sandbox_<key>"
json
{
  "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

GET/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.

bash
curl "https://mystocks.africa/api/sandbox/v1/partner/companies/SCOM.KE" \
  -H "Authorization: Bearer sk_sandbox_<key>"
json
{
  "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
}
GET/companies/{symbol}/news

Curated news and regulatory announcements for the company. Same shape as /pulse but sourced from exchange filings and financial newswires. Supports ?limit= and ?cursor=.

GET/companies/tickers

Canonical 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.

bash
curl "https://mystocks.africa/api/sandbox/v1/partner/companies/tickers" \
  -H "Authorization: Bearer sk_sandbox_<key>"
json
{
  "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
}
GET/market-intel

Editorial 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.

FieldTypeRequiredDescription
symbolstringNoFilter to articles tagged with a specific stock symbol, e.g. SCOM.KE.
exchangestringNoFilter articles by exchange code, e.g. NGX, NSE.
limitintegerNoMax results. Default 20, max 100.
bash
curl "https://mystocks.africa/api/sandbox/v1/partner/market-intel?exchange=NGX&limit=5" \
  -H "Authorization: Bearer sk_sandbox_<key>"
json
{
  "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
}
GET/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.

bash
curl "https://mystocks.africa/api/sandbox/v1/partner/market-intel/ngx-q2-2026-market-wrap" \
  -H "Authorization: Bearer sk_sandbox_<key>"
json
{
  "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.

POST/users

Create 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.

FieldTypeRequiredDescription
externalIdstringYesYour internal user ID. Must be unique per partner.
displayNamestringNoFull name shown in the MyStocks admin console.
emailstringNoUser email address.
bash
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"}'
json
{
  "subAccountId": "usr_abc123",
  "externalId": "user_42",
  "displayName": "Jane Doe",
  "email": "jane@example.com",
  "kycStatus": "NONE",
  "kycLevel": "NONE",
  "status": "active",
  "walletBalance": 0
}
POST/auto-register

Idempotent 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.

FieldTypeRequiredDescription
uidstringYesYour internal user identifier. Acts as idempotency key.
emailstringNoEmail address.
namestringNoDisplay name.
phonestringNoPhone in E.164 format, e.g. +254712345678.
countrystringNoISO 3166-1 alpha-2 country code.
GET/users
GET/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.

PATCH/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.

bash
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"}'
POST/users/{userId}/deposit

Credit 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.

FieldTypeRequiredDescription
amountnumberYesUSD to credit. Deducted from your master wallet.
localAmountnumberNoLocal-currency equivalent (for audit trail only).
localCurrencystringNoISO 4217 code, e.g. KES, NGN, GHS.
fxRatenumberNoLocal units per USD that you applied.
notestringNoReference string shown in transaction history.
bash
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"}'
json
{
  "message": "Deposit successful.",
  "subAccountId": "usr_abc123",
  "amount": 500,
  "currency": "USD",
  "newSubBalance": 500,
  "newMasterBalance": 99500
}
POST/users/{userId}/withdraw

Debit 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.

FieldTypeRequiredDescription
amountnumberYesUSD to move to your master wallet.
localAmountnumberNoLocal equivalent for audit trail.
localCurrencystringNoISO 4217 code.
fxRatenumberNoRate you applied.
notestringNoReference for transaction history.
json
{
  "message": "Withdrawal successful.",
  "subAccountId": "usr_abc123",
  "amount": 200,
  "currency": "USD",
  "newSubBalance": 300,
  "newMasterBalance": 99700
}
POST/users/{userId}/kyc

Assert 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.

FieldTypeRequiredDescription
statusstringYesNONE | PENDING | VERIFIED
levelstringYesNONE | BASIC (ID verified) | FULL (enhanced due diligence)
referencestringNoYour KYC session ID for audit correlation.
bash
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"}'
json
{
  "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.

GET/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.

FieldTypeRequiredDescription
typestringYesBUY or SELL.
quantityintegerYesNumber of shares.
userIdstringNoSub-account ID. Omit to quote against the master wallet.
bash
curl "https://mystocks.africa/api/sandbox/v1/partner/quote/SCOM.KE?type=BUY&quantity=500" \
  -H "Authorization: Bearer sk_sandbox_<key>"
json
{
  "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."
}
POST/trademaster account
POST/users/{userId}/tradesub-account

Place 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.

FieldTypeRequiredDescription
symbolstringYesExchange-qualified ticker or unambiguous bare ticker.
typestringYesBUY or SELL.
quantityintegerYesWhole shares. Must be a positive integer.
stopLossnumberNoAuto-cancel price floor (USD). Optional.
takeProfitnumberNoAuto-settle price ceiling (USD). Optional.
bash
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}'
json
{
  "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.

GET/orders
GET/orders/{orderId}
GET/users/{userId}/orders

List 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.

json
{
  "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
}
DELETE/orders/{orderId}
DELETE/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.

GET/users/{userId}/transactions

Full 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).

ParamTypeDescription
typestringFilter: DEPOSIT | WITHDRAWAL | INVEST | SELL | DISTRIBUTION | REDEEM | FEE | TRANSFER_IN | TRANSFER_OUT
fromdateInclude transactions on or after this date (YYYY-MM-DD, UTC)
todateInclude transactions on or before this date (YYYY-MM-DD, UTC)
cursorstringOpaque cursor from nextCursor. Omit for first page
limitintegerPage size, default 50, max 200
json
{
  "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
}
GET/portfoliomaster
GET/users/{userId}/portfoliosub-account

Holdings with current market value, cost basis, unrealized P&L, and currency. Also includes fund and bond positions if the account holds subscriptions.

json
{
  "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.

GET/bonds
GET/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=.

bash
curl "https://mystocks.africa/api/sandbox/v1/partner/bonds?instrumentType=TREASURY_BILL" \
  -H "Authorization: Bearer sk_sandbox_<key>"
json
{
  "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
}
GET/funds
GET/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.

json
{
  "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
}
GET/opportunities

Curated 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".

FieldTypeRequiredDescription
typestringNoOPPORTUNITY | PRE_IPO. Omit for all types.
bash
curl "https://mystocks.africa/api/sandbox/v1/partner/opportunities?type=PRE_IPO" \
  -H "Authorization: Bearer sk_sandbox_<key>"
json
{
  "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
}
GET/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.

bash
curl "https://mystocks.africa/api/sandbox/v1/partner/opportunities/TREEPZ-PRE-IPO" \
  -H "Authorization: Bearer sk_sandbox_<key>"
json
{
  "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"
}
POST/users/{userId}/subscribe

Subscribe 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.

FieldTypeRequiredDescription
assetTypestringYesBOND | FUND | OPPORTUNITY | PRE_IPO
assetIdstringYesFirestore doc ID from the relevant list endpoint.
unitsnumberNoWhole units. Required for BOND and FUND.
amountnumberNoUSD commitment. Required for OPPORTUNITY and PRE_IPO.
bash
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}'
POST/users/{userId}/redeem

Redeem 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.

FieldTypeRequiredDescription
holdingIdstringYesSub-account holding doc ID (same as the fund doc ID).
unitsToRedeemnumberYesUnits 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.

GET/account

Master wallet balance plus an aggregate portfolio summary across all sub-accounts. Useful as a dashboard home-screen data source.

bash
curl "https://mystocks.africa/api/v1/partner/account" \
  -H "Authorization: Bearer pk_live_<key>"
json
{
  "masterWalletBalance": 94500,
  "totalAum": 45230.5,
  "subAccountCount": 142,
  "currency": "USD"
}
POST/topup
GET/topup

Submit 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.

FieldTypeRequiredDescription
amountnumberYesUSD amount requested.
referencestringNoYour internal transfer reference for reconciliation.
bash
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"}'
json
{
  "id": "tup_abc123",
  "amount": 10000,
  "currency": "USD",
  "status": "PENDING",
  "reference": "wire_TXN987654",
  "createdAt": "2026-06-09T10:00:00Z"
}
POST/payout
GET/payout

Submit 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.

FieldTypeRequiredDescription
amountnumberYesUSD amount to repatriate.
referencestringNoYour internal reference for reconciliation.
bash
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"}'
json
{
  "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.

GET/users/{userId}/watchlist

Returns all watchlisted stocks for a sub-account, ordered by most recently added. Live price and day-change percent are included where data is available.

bash
curl https://mystocks.africa/api/v1/partner/users/usr_abc123/watchlist \
  -H "x-api-key: pk_live_KEY"
json
{
  "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"
  }
}
POST/users/{userId}/watchlist

Adds 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.

FieldTypeRequiredDescription
symbolstringYesTicker symbol. Exchange-qualified (SCOM.KE) preferred. Bare tickers (SCOM) are auto-resolved.
bash
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"}'
json
{
  "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"
  }
}
DELETE/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.

bash
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.

POST/webhooks
FieldTypeRequiredDescription
urlstringYesHTTPS endpoint. Must respond 2xx within 8 s.
eventsstring[]YesArray of event type strings to subscribe to.
secretstringNoHMAC signing secret (min 16 chars). Generated automatically if omitted.
bash
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"
  }'
json
{
  "id": "wh_abc123",
  "url": "https://yourapp.com/webhooks/mystocks",
  "events": [
    "order.filled",
    "order.rejected",
    "deposit.confirmed",
    "kyc.updated"
  ],
  "status": "active"
}

All webhook events

EventTrigger
order.pendingAn order was submitted and is awaiting admin review.
order.filledAn order was approved and executed. Shares/proceeds credited.
order.rejectedAn order was declined. Includes rejectionCode and rejectionReason.
order.cancelledAn order was cancelled via DELETE /orders/{id}.
trade.settledAlias for order.filled — use order.filled in new integrations.
trade.rejectedAlias for order.rejected — use order.rejected in new integrations.
deposit.confirmedA sub-account deposit was recorded.
withdraw.confirmedA sub-account withdrawal was processed.
wallet.creditedMaster wallet received a top-up from MyStocks ops.
kyc.updatedA sub-account KYC status changed.
account.frozenA sub-account was frozen by partner or by MyStocks compliance.
dividend.paidA dividend was received and credited to a sub-account.
incident.declaredA platform incident affecting your integration was opened.
incident.resolvedA 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.

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "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).

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "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).

json
{
  "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).

json
{
  "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.

javascript
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');
});
GET/webhooks
DELETE/webhooks/{id}
POST/webhooks/{id}/test
GET/webhooks/{id}/deliveries

List 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).

bash
# 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>"
json
{
  "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.

GET/report/aum

Total assets under management across all sub-accounts, broken down by asset class (equities, bonds, funds, opportunities) and exchange. Supports ?format=csv for spreadsheet export.

json
{
  "totalAum": 45230.5,
  "currency": "USD",
  "breakdown": {
    "equities": 38400,
    "bonds": 4200,
    "funds": 2630.5,
    "opportunities": 0
  },
  "byExchange": {
    "NSE": 22100,
    "NGX": 10300,
    "JSE": 6000
  }
}
GET/report/positions

All open equity positions across all sub-accounts. Use for a global portfolio overview or compliance snapshot. Supports ?exchange=, ?symbol=, and ?format=csv.

json
{
  "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
}
GET/report/fees

Trading fees paid across all sub-accounts — base fees and any partner markup. Filter by ?from= / ?to= date range. Supports ?format=csv.

json
{
  "period": {
    "from": "2026-05-01",
    "to": "2026-05-31"
  },
  "totalBaseFees": 62.25,
  "totalMarkupFees": 18.75,
  "totalFees": 81,
  "tradeCount": 83,
  "currency": "USD"
}
GET/report/revenue

Partner markup revenue breakdown per trade, with net payable. Filter by ?from= / ?to= date range. Supports ?format=csv.

json
{
  "period": {
    "from": "2026-05-01",
    "to": "2026-05-31"
  },
  "totalMarkupRevenue": 124.5,
  "totalTrades": 83,
  "netPayable": 124.5,
  "currency": "USD"
}
GET/report/dividends

Aggregated dividend report across all sub-accounts. Includes declared amount, USD conversion, and per-account breakdown. Supports ?from= / ?to=.

json
{
  "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
}
GET/report/invoice

Monthly fee statement with line items for platform fees, markups, and adjustments. Pass ?month=YYYY-MM to select a specific month. Supports ?format=csv.

json
{
  "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"
}
GET/dividends/calendar

Upcoming dividend declarations for stocks traded on supported exchanges. partnerEligible: true flags dividends where at least one of your sub-accounts holds the stock.

json
{
  "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
}
GET/audit

Paginated 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.

json
{
  "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"
}
GET/usage

90-day rolling API call analytics: requests per day, error rate, and current rate-limit window status. Use to monitor usage against your tier ceiling.

json
{
  "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.

GET/sla

Returns 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.

bash
curl "https://mystocks.africa/api/v1/partner/sla" \
  -H "Authorization: Bearer pk_live_<key>"
json
{
  "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

LevelNameImpact
P0CriticalFull platform outage — all trading and market data unavailable.
P1MajorCore service degraded — order execution or market data impaired.
P2MinorPartial degradation — some exchanges or endpoints affected.
P3InformationalMaintenance or cosmetic issue with no user impact.
Full API Playground Available

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.

Explore API Reference

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/partner

Sandbox

https://mystocks.africa/api/sandbox/v1

Backwards-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.
http
# 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.

SandboxProduction
Base URLhttps://mystocks.africa/api/sandbox/v1https://mystocks.africa/api/v1/partner
API key prefixsk_sandbox_pk_live_
Trade settlementInstant (no queue)PENDING → reviewed within 4 h → COMPLETED / REJECTED
Stock pricesLive market prices (production read-through)Live market prices
Wallet fundingAuto $100k on registerAdmin deposits real funds
Reset endpointAvailableNot available

Onboarding steps

  1. 1Register at mystocks.africa/partners/register and submit your application.
  2. 2MyStocks reviews your application within 1–2 business days.
  3. 3Upon approval, you receive your pk_live_ API key and access to the partner dashboard.
  4. 4Swap base URL and key prefix — no other code changes needed.
  5. 5Submit trades via POST /api/v1/partner/trade → orders appear in the admin review queue.
  6. 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.

v2.3June 2026
  • 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.
v2.2June 2026
  • 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.
v2.1May 2026
  • 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.
v2.0May 2026
  • 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.
v1.9May 2026
  • 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.
v1.8April 2026
  • 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).
v1.7March 2026
  • 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.
v1.6January 2026
  • 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.
v1.5November 2025
  • 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.
v1.0September 2025
  • 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.