# MyStocks Africa Partner API — Full Reference > MyStocks Africa provides African investment infrastructure via REST API. This document is the authoritative machine-readable reference covering every endpoint, request/response shape, authentication model, FX model, rate limits, webhook payloads, and workflow needed to integrate the MyStocks Partner API. **Production base URL**: `https://mystocks.africa/api/v1/partner` **Sandbox base URL**: `https://mystocks.africa/api/sandbox/v1/partner` (exception: POST /register and POST /reset live at `/api/sandbox/v1` without the `/partner` segment) **Authentication**: `Authorization: Bearer pk_live_` (production) / `Authorization: Bearer sk_sandbox_` (sandbox) **Content-Type**: `application/json` for all POST/PATCH bodies **Docs**: https://mystocks.africa/partners/docs **Apply**: https://mystocks.africa/partners --- ## Quick Start (5 minutes) Six steps from zero to first executed trade using the sandbox: **Step 1 — Register:** ```bash curl -X POST https://mystocks.africa/api/sandbox/v1/register \ -H "Content-Type: application/json" \ -d '{ "businessName": "Acme", "email": "dev@acme.com" }' # Response: { "apiKey": "sk_sandbox_xxx", "walletBalance": 100000, "currency": "USD" } ``` **Step 2 — Create sub-account:** ```bash curl -X POST https://mystocks.africa/api/sandbox/v1/partner/users \ -H "Authorization: Bearer sk_sandbox_" \ -H "Content-Type: application/json" \ -d '{ "externalId": "user_42", "displayName": "Jane Doe", "email": "jane@example.com" }' # Response: { "subAccountId": "usr_xxx", "externalId": "user_42", "kycStatus": "NONE", "wallet": { "currency": "USD", "balance": 0 } } ``` **Step 3 — Deposit funds (you handle FX, report the USD equivalent):** ```bash curl -X POST https://mystocks.africa/api/sandbox/v1/partner/users/usr_xxx/deposit \ -H "Authorization: Bearer sk_sandbox_" \ -H "Idempotency-Key: dep_user42_001" \ -H "Content-Type: application/json" \ -d '{ "amount": 500, "localAmount": 655000, "localCurrency": "KES", "fxRate": 1310, "note": "mpesa_QHJ29SK" }' # Response: { "message": "Deposit successful.", "newSubBalance": 500, "currency": "USD" } ``` **Step 4 — Get a quote:** ```bash curl "https://mystocks.africa/api/sandbox/v1/partner/stocks/SCOM.KE" \ -H "Authorization: Bearer sk_sandbox_" # Response: { "symbol": "SCOM.KE", "price": 16.50, "usdPrice": 0.0126, ... } ``` **Step 5 — Place trade:** ```bash curl -X POST https://mystocks.africa/api/sandbox/v1/partner/users/usr_xxx/trade \ -H "Authorization: Bearer sk_sandbox_" \ -H "Idempotency-Key: trade_user42_001" \ -H "Content-Type: application/json" \ -d '{ "symbol": "SCOM.KE", "type": "BUY", "quantity": 1000 }' # Response: { "orderId": "ord_xxx", "status": "FILLED", ... } (sandbox: instant fill) ``` **Step 6 — Poll status (or use webhooks):** ```bash curl "https://mystocks.africa/api/sandbox/v1/partner/users/usr_xxx/orders?limit=1" \ -H "Authorization: Bearer sk_sandbox_" # Production: status progresses PENDING → COMPLETED (or REJECTED) after admin review ``` In production, orders move PENDING → PROCESSING → COMPLETED (or REJECTED) after admin review during exchange hours. --- ## Authentication Every request (except POST /register) requires an API key. Send via either: ```http # Option A (recommended) Authorization: Bearer pk_live_ # Option B x-api-key: pk_live_ ``` Key prefixes: `sk_sandbox_` (sandbox) | `pk_live_` (production) **Idempotency:** Pass `Idempotency-Key: ` on deposit, withdraw, and trade endpoints. MyStocks deduplicates by key for 24 hours and returns the cached response on retry. This prevents double-charges on mobile networks where a POST can succeed server-side but time out client-side. HTTP 409 is returned if a concurrent duplicate is in progress. --- ## FX & Currency Model MyStocks operates a split-responsibility FX model. | Flow | Who handles FX? | What partner provides | What MyStocks does | |------|----------------|----------------------|-------------------| | Funding (deposit) | Partner | amount (USD) + localAmount + localCurrency + fxRate | Records for audit trail; credits sub-account USD wallet | | Equity trading | MyStocks | — | Applies live spot rate at execution; settles in local market currency | | Dividends | MyStocks | — | Converts dividend to USD at spot; credits sub-account wallet | | Withdrawal | Partner | amount (USD) + optional fxRate applied | Debits USD wallet; partner pays user in local currency | Sub-account wallets are denominated in USD. FX risk between deposit and trade execution is borne by the end-user. MyStocks does not offer hedging. --- ## Rate Limits Limits are per API key per minute. | Tier | Requests/min | Notes | |------|-------------|-------| | Sandbox / Starter | 100 | Default for all new accounts | | Growth | 500 | Contact partnerships@mystocks.africa to upgrade | | Enterprise | 2,000 | Custom limits above 2,000 available | Every response includes: ```http X-RateLimit-Limit: 500 X-RateLimit-Remaining: 487 X-RateLimit-Reset: 1751400060 # Unix timestamp Retry-After: 0 # seconds to wait (0 if not limited) ``` HTTP 429 response: ```json { "error": "Rate limit exceeded. Retry after 1751400060.", "retryAfter": 1751400060 } ``` Webhook callbacks do not count toward rate limits. --- ## Sandbox vs Production | | Sandbox | Production | |---|---------|-----------| | Base URL | https://mystocks.africa/api/sandbox/v1/partner | https://mystocks.africa/api/v1/partner | | API key prefix | sk_sandbox_ | pk_live_ | | Trade settlement | Instant (no queue) | Pending → admin approval | | Stock prices | Static test values | Live market prices | | Wallet funding | Auto $100k on register | Admin deposits real funds | | Reset endpoint | Available (POST /reset) | Not available | --- ## Error Format All errors return: ```json { "error": "Human-readable error message." } ``` | Code | Meaning | |------|---------| | 200 | OK | | 201 | Created (register, create sub-account) | | 202 | Accepted — order queued, funds escrowed; settlement async | | 400 | Bad Request — missing or invalid parameters | | 401 | Unauthorized — API key missing, invalid, revoked, or expired | | 403 | Forbidden — partner not approved, sub-account frozen, or out-of-tier operation | | 404 | Not Found — symbol, sub-account, order, or instrument not found | | 409 | Conflict — concurrent idempotency-key collision; retry after first request resolves | | 422 | Unprocessable — business rule violation: insufficient funds/holdings, KYC required, fund not open | | 429 | Too Many Requests — check X-RateLimit-Remaining and X-RateLimit-Reset | | 500 | Internal Server Error — retry with exponential backoff | --- ## 1. Partner Registration (Sandbox) ### POST /register Create a new sandbox account. Returns an API key and seeds a $100,000 virtual wallet. Idempotent — re-registering with the same email returns the existing key. Body: ```json { "businessName": "Acme Corp", "email": "dev@acme.com" } ``` Response (201): ```json { "apiKey": "sk_sandbox_xxxxxxxxxxxxxxxx", "walletBalance": 100000, "currency": "USD" } ``` ### POST /reset _(sandbox only)_ Wipes all orders, holdings, and transactions for the calling sandbox account. Resets wallet to $100,000. Useful for starting fresh after a test cycle. Response: ```json { "message": "Sandbox account reset. Wallet restored to $100,000." } ``` --- ## 2. Partner Account ### GET /api/v1/partner/me Returns the calling partner's approval status and API key. Authenticated with a Firebase ID token (obtained after logging in via the Partner Portal). Use this to check application status or retrieve your active key. ```bash curl "https://mystocks.africa/api/v1/partner/me" \ -H "Authorization: Bearer " ``` Response: ```json { "status": "active", "apiKey": "pk_live_a1b2c3...", "businessName": "Acme Corp", "email": "dev@acme.com" } ``` `status` values: `"active"` (key issued, usable) | `"pending"` (under review) | `"approved"` (approved, key not yet issued) | `"rejected"` | `"suspended"` | `"not_found"` ### GET /api/v1/partner/account Returns the partner master account: business name, email, USD master wallet balance, total portfolio value across all sub-accounts, invested capital, and realized gains. Response: ```json { "businessName": "ACME Fintech", "email": "api@acmefintech.com", "wallet": { "currency": "USD", "balance": 250000.00 }, "portfolioValue": 1850000.00, "investedCapital": 1600000.00, "realizedGainUsd": 45000.00 } ``` ### GET /api/v1/partner/portfolio Returns all holdings aggregated across all sub-accounts with current value, cost basis, and P&L. Response: ```json { "holdings": [ { "id": "SCOM.KE", "symbol": "SCOM.KE", "name": "Safaricom PLC", "exchange": "NSE", "units": 500, "costBasis": 15.80, "amountInvested": 7900.00, "localCurrency": "KES", "localPrice": 16.50, "currentValue": 8250.00, "gainLoss": 350.00, "liquidityStatus": "LIQUID" } ], "count": 1, "totalValue": 8250.00, "totalInvested": 7900.00, "currency": "USD" } ``` --- ## 3. Market Data — Stocks ### GET /api/v1/partner/stocks Returns all active stocks across every exchange. Filter by exchange, sector, or free-text search. Query params: `exchange` (NSE|NGX|JSE|GSE|BRVM|LUSE|USE|DSE|EGX|BSE|SEM), `sector`, `search`, `assetType` (STOCK|ETF) Response: ```json { "stocks": [ { "id": "SCOM.KE", "symbol": "SCOM.KE", "name": "Safaricom PLC", "slug": "safaricom", "assetType": "STOCK", "exchange": "NSE", "currency": "KES", "sector": "Telecommunications", "description": "Safaricom is the largest telecommunications provider in Kenya.", "listingStatus": "ACTIVE", "price": 16.50, "usdPrice": 0.1274, "change": 0.25, "changePct": 1.54, "logo": { "imageUrl": "https://s3-symbol-logo.tradingview.com/safaricom--big.svg", "imageHint": "Safaricom" }, "logoUrl": "https://s3-symbol-logo.tradingview.com/safaricom--big.svg", "dayHigh": 16.80, "dayLow": 16.20, "volume": 4820000, "previousClose": 16.25, "lastPriceUpdate": "2026-05-01T10:30:00.000Z" } ], "count": 847 } ``` Logo fields: `logo` is an object `{ imageUrl, imageHint }` or `null`. `logoUrl` is a convenience alias for `logo.imageUrl` — use it directly as ``. Both are `null` until populated by the MyStocks admin team; check for null before rendering. ### GET /api/v1/partner/stocks/{symbol} Historical end-of-day closing prices for a stock plus the current live price as the trailing data point. Use for chart rendering. Path: `symbol` — stock symbol (e.g. SCOM.KE), slug (e.g. safaricom). Case-insensitive. Query params: `period` (1W|1M|3M|6M|1Y|ALL, default 1M) Response: ```json { "symbol": "SCOM.KE", "name": "Safaricom PLC", "exchange": "NSE", "currency": "KES", "currentPrice": 16.50, "priceUsd": 0.1274, "history": [ { "date": "2026-04-01", "close": 15.80 }, { "date": "2026-04-30", "close": 16.25 }, { "date": "live", "close": 16.50 } ] } ``` ### GET /api/v1/partner/stocks/{symbol}/pulse Corporate actions and market news for a symbol. Response: ```json { "symbol": "SCOM.KE", "events": [ { "id": "evt_001", "type": "DIVIDEND", "title": "Safaricom FY2025 Final Dividend", "amount": 0.65, "currency": "KES", "exDate": "2025-06-01", "payDate": "2025-06-30" } ], "count": 1 } ``` --- ## 4. Market Data — Companies ### GET /api/v1/partner/companies Returns company list with fundamentals. Different from /stocks — companies include profile data, not live prices. Query params: `exchange`, `sector`, `search`, `assetType` Response fields per company: `symbol`, `slug`, `assetType`, `sector`, `industry`, `currency`, `listingStatus`, `description`, `logoUrl` (string CDN URL or null), `marketCap`, `peRatio`, `dividendYield`, `founded`, `headquarters`, `website`. `logoUrl` is a direct CDN URL for the company logo suitable for use as ``. It is `null` until populated by the MyStocks admin team. ### GET /api/v1/partner/companies/{symbol} Full company profile: fundamentals, live price, logo, corporate actions (up to 20). Response includes all fields from the company list endpoint plus: `currentPrice`, `change`, `changePct`, `dayHigh`, `dayLow`, `volume`, `previousClose`, `lastPriceUpdate`, `corporateActions[]`. `logoUrl` is included: a CDN URL or `null`. ETF-specific fields (`expenseRatio`, `benchmarkIndex`, `topHoldings`) are included automatically when `assetType` is `"ETF"`. ### GET /api/v1/partner/companies/{symbol}/chart OHLCV price chart data optimized for candlestick or line chart rendering. Query params: `interval` (1d|1w|1m), `from` (ISO date), `to` (ISO date) Response: ```json { "symbol": "SCOM.KE", "candles": [ { "date": "2026-05-01", "open": 16.20, "high": 16.80, "low": 16.10, "close": 16.50, "volume": 4820000 } ] } ``` ### GET /api/v1/partner/companies/{symbol}/news Company-specific news articles. Curated editorial news, not social media. Query params: `limit` (default 10, max 50) ### GET /api/v1/partner/companies/tickers Returns a flat list of all ticker symbols available on the platform. Useful for populating autocomplete or validation. Response: ```json { "tickers": [ { "symbol": "SCOM.KE", "name": "Safaricom PLC", "exchange": "NSE", "slug": "safaricom" } ], "count": 847 } ``` --- ## 5. Market Status & Quote ### GET /api/v1/partner/market/status Returns current open/closed status for all African exchanges. All times calculated from real exchange trading hours. Query params: `exchange` (optional — if omitted, all exchanges returned) Single exchange response: ```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-05-10T10:30:00.000Z" } ``` All exchanges response: ```json { "anyOpen": true, "checkedAt": "2026-05-10T10:30:00.000Z", "exchanges": { "NSE": { "isOpen": true, "status": "OPEN", "nextOpen": null }, "NGX": { "isOpen": false, "status": "CLOSED", "nextOpen": "2026-05-11T08:30:00.000Z" }, "JSE": { "isOpen": true, "status": "OPEN", "nextOpen": null } } } ``` `anyOpen: true` — at least one exchange is currently accepting trades. ### GET /api/v1/partner/quote/{symbol} Real-time fee breakdown for a hypothetical trade without placing an order. Use this to show users what a BUY will cost or what a SELL will return before they confirm. Path: `symbol` — stock symbol or slug Query params: `type` (BUY|SELL, default BUY), `quantity` (integer, default 1) BUY quote response: ```json { "symbol": "SCOM.KE", "name": "Safaricom PLC", "exchange": "NSE", "currency": "KES", "type": "BUY", "quantity": 500, "localPrice": 16.50, "usdPrice": 0.127306, "gross": 63.65, "baseFee": 0.48, "partnerMarkupFee": 0.00, "fee": 0.48, "totalCost": 64.13, "walletBalance": 1250.00, "sufficientFunds": true, "feeRate": 0.75, "note": "This is a quote only. No order has been placed." } ``` SELL quote response: ```json { "symbol": "SCOM.KE", "type": "SELL", "quantity": 100, "gross": 12.73, "baseFee": 0.10, "fee": 0.10, "estimatedProceeds": 12.63, "walletBalance": 1250.00, "units": 250, "sufficientHoldings": true, "feeRate": 0.75, "note": "This is a quote only. No order has been placed." } ``` Fields: `gross` (qty × usdPrice before fees) | `baseFee` (0.75% MyStocks platform fee) | `partnerMarkupFee` (your markup, 0 if not configured) | `fee` (baseFee + partnerMarkupFee) | `totalCost` (BUY: gross+fee) | `estimatedProceeds` (SELL: gross−fee) | `sufficientFunds` (BUY: walletBalance ≥ totalCost) | `sufficientHoldings` (SELL: held units ≥ quantity) ### GET /api/v1/partner/market/quotes Batch real-time quotes for up to 50 symbols in one request. Avoids N parallel calls when loading watchlists. Query params: `symbols` — comma-separated list (e.g. `SCOM.KE,GLD.ZA,DANGCEM.NG`). Max 50; exceeding returns `400 BATCH_LIMIT_EXCEEDED`. Unresolvable symbols appear in `not_found` rather than failing the whole request. Always check `not_found_count`. ```json { "quotes": [ { "symbol": "SCOM.KE", "name": "Safaricom PLC", "exchange": "NSE", "currency": "KES", "price": 16.50, "usd_price": 0.0126, "change": 0.25, "change_pct": 0.015385, "volume": 4210000, "bid": null, "ask": null, "market_status": "OPEN" } ], "count": 1, "not_found": ["INVALID_XYZ"], "not_found_count": 1 } ``` `volume` is always an integer — `0` for instruments with no recorded trades today, never `null`. ### GET /api/v1/partner/market/movers Top price-change movers for an exchange, sorted by `change_pct`. Query params: `exchange` (required, e.g. NSE) | `direction` (gainers|losers, default gainers) | `limit` (1–100, default 10) | `page` (1-based, default 1) Only instruments with a recorded `changePct` value appear — zero-movement instruments are excluded. ```json { "data": [ { "symbol": "EABL.KE", "name": "East African Breweries", "exchange": "NSE", "currency": "KES", "price": 185.00, "usd_price": 1.423077, "change": 10.00, "change_pct": 0.057143, "volume": 312000, "market_status": "OPEN" } ], "meta": { "exchange": "NSE", "direction": "gainers", "total_count": 48, "page": 1, "per_page": 10, "has_next": true } } ``` --- ## 5b. ETFs ### GET /api/v1/partner/etfs List ETFs available on African exchanges. Includes fund-specific metadata: expense ratio, index tracked, risk level, and top holdings. Query params: `exchange` (optional) ```json { "etfs": [ { "id": "STXEME.ZA", "symbol": "STXEME.ZA", "name": "Satrix MSCI Emerging Markets ETF", "exchange": "JSE", "currency": "ZAR", "price": 75.00, "usdPrice": 4.55, "assetType": "ETF", "fundMetadata": { "brand": "Satrix", "expenseRatio": 0.38, "geographicalFocus": "Global Emerging Markets", "indexTracked": "MSCI Emerging Markets Index", "riskLevel": "HIGH", "managementStyle": "PASSIVE" } } ], "count": 1 } ``` ### GET /api/v1/partner/etfs/{symbol} Single ETF by symbol (e.g. `STXEME.ZA`). Same shape as the array item above. ### GET /api/v1/partner/etfs/{symbol}/chart Price history chart for an ETF. Same parameters and response shape as `GET /stocks/{symbol}/chart`. Query params: `period` (1d|1w|1m|3m|6m|1y|5y) ```json { "symbol": "STXEME.ZA", "currency": "ZAR", "period": "3m", "labels": ["2026-03-01", "2026-04-01", "2026-05-23"], "prices": [73.50, 74.20, 75.00], "volumes": [12000, 18500, 24000], "priceHistory": [ { "date": "2026-03-01T00:00:00.000Z", "price": 73.50, "usdPrice": 4.45 }, { "date": "2026-05-23T19:59:41.000Z", "price": 75.00, "usdPrice": 4.55 } ] } ``` ### GET /api/v1/partner/etfs/{symbol}/history Same as chart but returns the full `priceHistory` array without label/price/volume arrays. --- ## 6. Market Data — Bonds & Fixed Income ### GET /api/v1/partner/bonds Returns bonds, T-bills, Eurobonds, infrastructure bonds, and commercial papers. Query params: `instrumentType` (BOND|TREASURY_BILL|EUROBOND|COMMERCIAL_PAPER|INFRASTRUCTURE_BOND), `currency`, `exchange` Response: ```json { "bonds": [ { "id": "ke-treasury-91", "symbol": "KE91T", "name": "Kenya 91-Day T-Bill", "instrumentType": "TREASURY_BILL", "couponRate": 15.8, "maturityDate": "2025-08-01", "pricePerUnit": 100, "currency": "KES", "minInvestment": 50000, "status": "ACTIVE", "yield": 15.8 } ], "count": 42 } ``` ### GET /api/v1/partner/bonds/{id} Single bond by doc ID, slug, or symbol. Includes `marketYieldCurve` for chart rendering. --- ## 7. Market Data — Funds & ETFs ### GET /api/v1/partner/funds Returns all active funds: money market, yield, special, and opportunity funds. Query params: `category` (MONEY_MARKET|YIELD|SPECIAL|OPPORTUNITY), `currency` Response: ```json { "funds": [ { "id": "mmf-ke-001", "name": "MyStocks Money Market Fund", "category": "MONEY_MARKET", "pricePerUnit": 1.00, "currency": "KES", "annualizedReturn": 14.5, "minInvestment": 1000, "redemptionType": "instant", "status": "ACTIVE", "distributionConfig": { "frequency": "monthly", "autoReinvest": false } } ], "count": 6 } ``` ### GET /api/v1/partner/funds/{id} Single fund by doc ID, slug, or symbol. --- ## 8. Market Data — Private Credit & Pre-IPO ### GET /api/v1/partner/opportunities Returns private market deals and pre-IPO offerings. Query params: `type` (OPPORTUNITY|PRE_IPO|all, default all) Response: ```json { "opportunities": [ { "id": "deal-001", "assetType": "OPPORTUNITY", "name": "East African Solar Project A", "minInvestment": 5000, "targetAmount": 2000000, "currentRaised": 750000, "expectedReturn": "18-22%", "riskLevel": "medium", "currency": "USD", "status": "ACTIVE", "expectedExitDate": "2027-12-31" } ], "count": 8 } ``` ### GET /api/v1/partner/opportunities/{id} Single deal or pre-IPO by doc ID or slug. Returns `assetType`: "OPPORTUNITY" or "PRE_IPO". --- ## 9. Market Intelligence ### GET /api/v1/partner/market-intel Market news, analysis, and research articles. Query params: `symbol` (filter by stock symbol), `exchange`, `limit` (default 20, max 100) ### GET /api/v1/partner/market-intel/{id} Single article by slug or doc ID. Returns full body, author, tags. --- ## 10. Master Trading (Partner-Level) ### POST /api/v1/partner/trade Place a BUY or SELL order for the partner's own master account (not a sub-account). Funds are drawn from the master wallet. Requires `Idempotency-Key`. Body: ```json { "symbol": "SCOM.KE", "type": "BUY", "quantity": 1000 } ``` Response (202 Accepted): ```json { "orderId": "ord_xyz789", "status": "PENDING", "symbol": "SCOM.KE", "type": "BUY", "quantity": 1000, "message": "Order submitted for review." } ``` ### GET /api/v1/partner/orders List all master-level orders. Query params: `status` (PENDING|PROCESSING|COMPLETED|FILLED|REJECTED|CANCELLED), `limit`, `cursor` ### GET /api/v1/partner/orders/{orderId} Single master order by ID. Use for status polling. Response: ```json { "orderId": "ord_xyz789", "status": "FILLED", "symbol": "SCOM.KE", "type": "BUY", "quantity": 1000, "priceAtOrder": 16.50, "usdPriceAtOrder": 0.1274, "feeAmount": 0.96, "totalAmount": 128.36, "currency": "USD", "createdAt": "2026-05-01T09:00:00.000Z", "settledAt": "2026-05-01T10:15:00.000Z" } ``` ### DELETE /api/v1/partner/orders/{orderId} Cancel a pending master order. Only possible while status is PENDING. --- ## 11. Sub-Account Management Sub-accounts represent end-users in the partner's product. Each has a USD wallet, holdings, orders, and transaction history. The partner is responsible for KYC. ### POST /api/v1/partner/auto-register _(recommended)_ Idempotent sub-account provisioning. Returns existing account (200) or creates new one (201). Safe to call on every user login — no duplicate accounts created. Body: ```json { "uid": "user_42", "email": "alice@yourapp.com", "name": "Alice K.", "phone": "+254712345678", "country": "KE" } ``` Response (201 new, 200 existing): ```json { "subAccountId": "usr_abc123", "externalId": "user_42", "displayName": "Alice K.", "email": "alice@yourapp.com", "kycStatus": "NONE", "status": "active", "walletBalance": 0, "isNew": true } ``` `isNew: true` — use to decide whether to show an onboarding flow. `uid` is the idempotency key. ### POST /api/v1/partner/users Create a new sub-account (non-idempotent). Returns 409 if `externalId` is already taken. Body: ```json { "externalId": "usr_8821", "displayName": "Alice K.", "email": "alice@yourapp.com" } ``` Response (201): ```json { "subAccountId": "usr_abc123", "externalId": "usr_8821", "displayName": "Alice K.", "email": "alice@yourapp.com", "kycStatus": "NONE", "kycLevel": "NONE", "status": "active", "wallet": { "currency": "USD", "balance": 0 } } ``` ### GET /api/v1/partner/users List all sub-accounts. Query params: `externalId` (filter), `limit` (default 50, max 200) Response: ```json { "accounts": [ { "subAccountId": "usr_abc123", "externalId": "usr_8821", "displayName": "Alice K.", "kycStatus": "VERIFIED", "kycLevel": "BASIC", "status": "active", "walletBalance": 1250.00 } ], "count": 1 } ``` ### GET /api/v1/partner/users/{userId} Get a single sub-account's profile. ### PATCH /api/v1/partner/users/{userId} Update sub-account (displayName, email, status). Special actions: `{ "action": "freeze" }` or `{ "action": "unfreeze" }` to toggle account status. --- ## 12. Wallets & Deposits ### GET /api/v1/partner/users/{userId}/wallet ```json { "subAccountId": "usr_abc123", "externalId": "usr_8821", "wallet": { "currency": "USD", "balance": 300.00 } } ``` ### POST /api/v1/partner/users/{userId}/deposit Atomically moves funds from master wallet to sub-account wallet. You must have collected real money from your user before calling this. Requires `Idempotency-Key` for safe retry. Body: ```json { "amount": 38.50, "note": "Mpesa STK push ref KE2482", "localAmount": 5000, "localCurrency": "KES", "fxRate": 129.87 } ``` `amount` (required) — USD amount. `note` (optional) — shown in transaction history. `localAmount`, `localCurrency`, `fxRate` (optional) — stored for audit/compliance; not used for any calculation by MyStocks. Response: ```json { "message": "Deposit successful.", "subAccountId": "usr_abc123", "externalId": "usr_8821", "amount": 38.50, "currency": "USD", "newSubBalance": 138.50, "newMasterBalance": 49961.50, "localAmount": 5000, "localCurrency": "KES", "fxRate": 129.87 } ``` ### POST /api/v1/partner/users/{userId}/withdraw Atomically moves funds from sub-account back to master wallet. Partner then pays user via their own payment rails. Requires `Idempotency-Key`. Body: ```json { "amount": 200, "note": "User withdrawal request", "localAmount": 26000, "localCurrency": "KES", "fxRate": 130.00 } ``` Response: ```json { "message": "Withdrawal successful.", "subAccountId": "usr_abc123", "amount": 200, "currency": "USD", "newSubBalance": 300.00, "newMasterBalance": 49700.00 } ``` --- ## 13. KYC Assertion ### POST /api/v1/partner/users/{userId}/kyc Assert KYC status. Partners conduct their own KYC and report the result. Set status to "VERIFIED" to unlock trading for the sub-account. Body: ```json { "kycStatus": "VERIFIED", "kycProvider": "Smile Identity", "kycReference": "smile_ref_123456", "kycCompletedAt": "2025-05-01T09:00:00.000Z" } ``` Response: `{ "message": "KYC status updated to VERIFIED." }` --- ## 14. Sub-Account Trading ### POST /api/v1/partner/users/{userId}/trade Place a BUY or SELL for a sub-account. BUY funds escrowed atomically on submission. Requires `Idempotency-Key`. Body: ```json { "symbol": "SCOM.KE", "type": "BUY", "quantity": 100, "stopLoss": 14.50, "takeProfit": 18.00 } ``` `stopLoss` and `takeProfit` are optional price thresholds in USD. Response (202): ```json { "status": "PENDING", "orderId": "ord_xyz789", "subAccountId": "usr_abc123", "externalId": "usr_8821", "type": "BUY", "symbol": "SCOM.KE", "quantity": 100, "priceAtOrder": 16.50, "usdPriceAtOrder": 0.1274, "gross": 12.74, "fee": 0.10, "totalCost": 12.84, "currency": "USD", "note": "Order is pending admin approval. Funds reserved from sub-account wallet." } ``` Order lifecycle (production): `PENDING → PROCESSING → COMPLETED` | `REJECTED` | `CANCELLED`. Sandbox: instant `FILLED`. ### GET /api/v1/partner/users/{userId}/orders List sub-account orders. Query params: `status` (PENDING|PROCESSING|COMPLETED|FILLED|REJECTED|CANCELLED), `limit` (default 50, max 200) ### GET /api/v1/partner/users/{userId}/orders/{orderId} Single sub-account order. Use for status polling. Response: ```json { "orderId": "ord_xyz789", "type": "BUY", "status": "FILLED", "symbol": "SCOM.KE", "quantity": 100, "priceAtOrder": 16.50, "usdPriceAtOrder": 0.1274, "feeAmount": 0.10, "totalAmount": 12.84, "currency": "USD", "createdAt": "2026-05-01T09:00:00.000Z", "settledAt": "2026-05-01T10:15:00.000Z" } ``` ### DELETE /api/v1/partner/users/{userId}/orders/{orderId} Cancel a pending sub-account order. Only possible while status is PENDING. --- ## 15. Sub-Account Portfolio & Transactions ### GET /api/v1/partner/users/{userId}/portfolio Holdings and portfolio summary for a sub-account. Response: ```json { "subAccountId": "usr_abc123", "externalId": "usr_8821", "summary": { "walletBalance": 289.48, "portfolioValue": 2410.52, "investedCapital": 2100.00, "unrealisedPnl": 310.52, "totalValue": 2700.00 }, "holdings": [ { "symbol": "SCOM.KE", "stockName": "Safaricom PLC", "units": 100, "avgCost": 0.1274, "currentValue": 13.10, "investedCapital": 12.74, "unrealisedPnl": 0.36, "currency": "USD" } ] } ``` ### GET /api/v1/partner/users/{userId}/transactions Transaction history. Query params: `type` (DEPOSIT|WITHDRAW|INVEST|DISTRIBUTION|REDEEM|COMMIT|PRE_IPO_COMMITMENT), `from` (ISO date), `to` (ISO date), `limit` --- ## 16. Subscribe to Bonds, Funds, Private Deals ### POST /api/v1/partner/users/{userId}/subscribe Unified subscription endpoint for non-equity assets. Full amount escrowed immediately. BOND body: `{ "assetType": "BOND", "assetId": "ke-treasury-91", "units": 10 }` FUND body: `{ "assetType": "FUND", "assetId": "mmf-ke-001", "units": 500 }` OPPORTUNITY/PRE_IPO body: `{ "assetType": "OPPORTUNITY", "assetId": "deal-001", "amount": 5000.00 }` Response (202): ```json { "message": "Commitment registered.", "orderId": "ord_bond_001", "reservedUsd": 120.00, "status": "PENDING" } ``` Status lifecycle: `PENDING → PROCESSING → ALLOCATED` ### POST /api/v1/partner/users/{userId}/redeem Redeem fund units (only for funds with `redemptionType === "instant"`). Body: `{ "holdingId": "mmf-ke-001", "unitsToRedeem": 100 }` Response: ```json { "message": "Redemption successful. Funds credited to sub-account wallet.", "proceeds": 102.50, "fundName": "MyStocks Money Market Fund", "pricePerUnit": 1.025, "unitsRedeemed": 100 } ``` --- ## 17. Reports ### GET /api/v1/partner/report/aum Total AUM across all sub-accounts, broken down by asset class. ### GET /api/v1/partner/report/positions All open positions across all sub-accounts. ### GET /api/v1/partner/report/fees Trading fee report. Query params: `from`, `to` (ISO dates). Returns total fees, trade count, and monthly breakdown. ### GET /api/v1/partner/report/revenue Partner revenue report — markups, referral fees, and net earnings. Query params: `from`, `to`, `groupBy` (day|month) Response: ```json { "totalRevenue": 1250.00, "currency": "USD", "breakdown": [ { "period": "2026-05", "markupRevenue": 420.00, "trades": 180 } ] } ``` ### GET /api/v1/partner/report/invoice Monthly invoice for platform fees. Query params: `month` (YYYY-MM) Response: ```json { "invoiceId": "inv_2026_05", "month": "2026-05", "partnerName": "Acme Corp", "baseFees": 840.00, "markupRevenue": 420.00, "netPayable": 420.00, "currency": "USD", "lineItems": [ { "date": "2026-05-01", "trades": 12, "fee": 28.50 } ] } ``` --- ## 18. Fund Flow (Partner ↔ MyStocks) ### POST /api/v1/partner/topup Request a capital injection into the partner's master wallet. MyStocks settles via wire/SWIFT. Body: `{ "amount": 100000, "currency": "USD", "note": "Q2 capital allocation" }` ### GET /api/v1/partner/topup List top-up request history. ### POST /api/v1/partner/payout Request a payout from the partner's master wallet. Body: ```json { "amount": 50000, "currency": "USD", "bankDetails": { "bankName": "Stanbic Bank", "accountNumber": "0123456789", "accountName": "ACME Fintech Ltd", "swiftCode": "SBICKENX" } } ``` ### GET /api/v1/partner/payout List payout request history. --- ## 19. Dividends ### GET /api/v1/partner/dividends/calendar Upcoming dividend declarations for stocks held by the partner's sub-accounts. `partnerEligible: true` flags dividends where at least one sub-account holds the stock. ### GET /api/v1/partner/users/{userId}/dividends Dividend payment history for a specific sub-account. ### GET /api/v1/partner/report/dividends Aggregated dividend report across all sub-accounts. Query params: `from`, `to` (ISO dates) Response: ```json { "totalUsdReceived": 12450.00, "distributionCount": 87, "bySymbol": [{ "symbol": "SCOM.KE", "totalUsd": 4200.00, "count": 30 }], "bySubAccount": [{ "subAccountId": "usr_abc", "displayName": "John Doe", "totalUsd": 210.50 }] } ``` --- ## 20. Webhooks ### POST /api/v1/partner/webhooks Register a webhook endpoint. `secret` must be at least 16 characters. Body: ```json { "url": "https://yourapp.com/webhooks/mystocks", "events": ["trade.settled", "deposit.confirmed", "kyc.updated"], "secret": "your-secret-min-16-chars" } ``` Response: `{ "id": "wh_abc123", "url": "...", "events": [...], "status": "active" }` ### GET /api/v1/partner/webhooks List registered webhooks. ### DELETE /api/v1/partner/webhooks/{id} Delete a webhook. ### Available events ``` trade.settled deposit.confirmed wallet.credited trade.rejected withdraw.confirmed kyc.updated dividend.paid account.frozen incident.declared incident.resolved ``` ### Signature verification (Node.js) ```javascript import { createHmac, timingSafeEqual } from 'crypto'; function verifyWebhook(rawBody, signature, secret) { const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex'); // timingSafeEqual prevents timing attacks — never use === for HMAC comparison return timingSafeEqual(Buffer.from(expected), Buffer.from(signature)); } // Express / Next.js route handler: app.post('/webhooks/mystocks', express.raw({ type: '*/*' }), (req, res) => { const sig = req.headers['x-mystocks-signature']; // lowercase header name if (!verifyWebhook(req.body.toString(), sig, process.env.MYSTOCKS_WEBHOOK_SECRET)) { return res.status(401).json({ error: 'Invalid signature' }); } const { event, data, timestamp } = JSON.parse(req.body.toString()); // ... handle event res.json({ received: true }); }); ``` Header: `x-mystocks-signature` (lowercase). Value format: `sha256=`. ### Event payloads **trade.settled:** ```json { "event": "trade.settled", "timestamp": "2026-05-01T10:45:00.000Z", "data": { "subAccountId": "usr_abc123", "externalId": "user_42", "orderId": "ord_xyz789", "type": "BUY", "symbol": "SCOM.KE", "quantity": 500, "priceAtOrder": 16.50, "usdPriceAtOrder": 0.1274, "feeAmount": 0.48, "totalAmount": 64.18, "currency": "USD" } } ``` **trade.rejected:** ```json { "event": "trade.rejected", "timestamp": "2026-05-01T11:00:00.000Z", "data": { "subAccountId": "usr_abc123", "externalId": "user_42", "orderId": "ord_xyz789", "type": "BUY", "symbol": "SCOM.KE", "quantity": 500, "rejectionReason": "Insufficient liquidity on exchange" } } ``` **deposit.confirmed:** ```json { "event": "deposit.confirmed", "timestamp": "2026-05-01T09:12:00.000Z", "data": { "subAccountId": "usr_abc123", "externalId": "user_42", "amount": 38.50, "currency": "USD", "newBalance": 138.50, "localAmount": 5000, "localCurrency": "KES", "fxRate": 129.87 } } ``` **withdraw.confirmed:** ```json { "event": "withdraw.confirmed", "timestamp": "2026-05-01T14:30:00.000Z", "data": { "subAccountId": "usr_abc123", "externalId": "user_42", "amount": 25.00, "currency": "USD", "newBalance": 113.50, "localAmount": 3250, "localCurrency": "KES", "fxRate": 130.00 } } ``` **wallet.credited** — master wallet credited by MyStocks admin: ```json { "event": "wallet.credited", "timestamp": "2026-05-01T09:00:00.000Z", "data": { "amount": 10000, "newBalance": 10000, "currency": "USD" } } ``` **kyc.updated:** ```json { "event": "kyc.updated", "timestamp": "2026-05-01T08:00:00.000Z", "data": { "subAccountId": "usr_abc123", "externalId": "user_42", "kycStatus": "VERIFIED", "kycLevel": "FULL", "reference": "SMILES-KYC-789456" } } ``` **dividend.paid:** ```json { "event": "dividend.paid", "timestamp": "2026-04-25T10:00:00.000Z", "data": { "subAccountId": "usr_abc123", "externalId": "user_42", "symbol": "SCOM.KE", "amountUsd": 12.60, "units": 500, "dividendPerShare": 0.29, "localCurrency": "KES", "dividendId": "div_abc123" } } ``` **incident.declared** — platform-wide; all active partners receive this: ```json { "event": "incident.declared", "timestamp": "2026-05-01T06:15:00.000Z", "data": { "id": "inc_xyz123", "title": "Trading API degraded performance", "severity": "P1", "status": "investigating", "affectedServices": ["trading-api"], "startedAt": "2026-05-01T06:10:00.000Z", "message": "We are investigating elevated latency on order submission." } } ``` **incident.resolved:** ```json { "event": "incident.resolved", "timestamp": "2026-05-01T08:45:00.000Z", "data": { "id": "inc_xyz123", "title": "Trading API degraded performance", "severity": "P1", "status": "resolved", "resolvedAt": "2026-05-01T08:45:00.000Z", "duration": "2h 35m" } } ``` ### Delivery guarantee & retry schedule MyStocks attempts delivery immediately, then retries: 5s → 30s → 5m → 30m → 2h. After 6 failed attempts, delivery is marked `failed`. Endpoint must return HTTP 2xx within 8 seconds. Monitor failed deliveries in the partner dashboard. ### GET /api/v1/partner/webhooks/{id}/deliveries Cursor-paginated delivery history for a specific webhook. Each entry: event, HTTP status returned, duration, success/failure. Query params: `limit` (default 50, max 200), `nextCursor` --- ## 21. API Key Management ### GET /api/v1/partner/api-keys/data-key Returns a read-only data API key for use in client-side applications (only grants read access to market data — no trading or wallet operations). ### POST /api/v1/partner/api-keys/rotate Rotates the calling API key. Old key is revoked immediately; new key is returned. Response: `{ "newKey": "pk_live_yyy...", "revokedAt": "2026-05-01T12:00:00.000Z" }` ### POST /api/v1/partner/api-keys/revoke Permanently revokes the calling API key. Use with caution — this cannot be undone. --- ## 22. Observability ### GET /api/v1/partner/audit Paginated list of every API call made by your key: endpoint, method, IP, user-agent, timestamp. Ordered newest-first. Query params: `limit` (default 50, max 200), `before` (ISO timestamp cursor) Response: ```json { "count": 3, "hasMore": true, "entries": [ { "id": "aud_1", "endpoint": "/api/v1/partner/users/usr_abc/trade", "method": "POST", "ip": "41.139.20.5", "userAgent": "Riven-App/2.1.0", "timestamp": "2026-03-28T10:02:31.000Z" } ] } ``` ### GET /api/v1/partner/usage Daily API call volumes for up to 90 days plus current rate limit window. Query params: `days` (default 30, max 90) Response: ```json { "keyId": "pk_live_a1b2c3...", "tier": "growth", "periodDays": 30, "totalCalls": 14820, "daily": [{ "date": "2026-03-01", "calls": 420 }, { "date": "2026-03-02", "calls": 518 }], "currentWindow": { "limit": 500, "remaining": 487, "resetAt": "2026-03-28T10:03:00.000Z" } } ``` --- ## 23. Partner Settings ### GET /api/v1/partner/settings Returns all partner-level settings: business profile, SMTP config, notification preferences, markup fee configuration. ### PATCH /api/v1/partner/settings Update partner settings. Configurable fields include: - `businessName`, `supportEmail`, `logoUrl`, `primaryColor` - `markupBps` — partner markup fee in basis points (e.g. 25 = 0.25% on top of MyStocks 0.75%) - `smtp` — `{ host, port, user, pass, from }` for partner-branded transactional emails - `notifyOnTrade`, `notifyOnDeposit`, `notifyOnKyc` — boolean notification toggles ### POST /api/v1/partner/settings _(SMTP test)_ Sends a test email using the partner's configured SMTP settings. Response: `{ "message": "Test email sent to you@partner.com" }` --- ## 24. SLA ### GET /api/v1/partner/sla Returns real-time system health for all covered services, active incidents with update timeline, upcoming maintenance windows, past 30-day incident history, and the calling partner's SLA tier commitments. SLA portal: https://mystocks.africa/partners/sla SLA Agreement: https://mystocks.africa/partners/sla/docs --- ## Exchange Coverage | Exchange | Code | Country | Asset Types | Timezone | |----------|------|---------|------------|---------| | Nairobi Securities Exchange | NSE | Kenya | Equities, T-Bills, Bonds | Africa/Nairobi | | Nigerian Exchange Group | NGX | Nigeria | Equities, Bonds, ETFs | Africa/Lagos | | Johannesburg Stock Exchange | JSE | South Africa | Equities, ETFs, Bonds | Africa/Johannesburg | | Ghana Stock Exchange | GSE | Ghana | Equities | Africa/Accra | | Bourse Régionale des Valeurs Mobilières | BRVM | West Africa (UEMOA) | Equities | Africa/Abidjan | | Lusaka Securities Exchange | LuSE | Zambia | Equities | Africa/Lusaka | | Uganda Securities Exchange | USE | Uganda | Equities | Africa/Kampala | | Dar es Salaam Stock Exchange | DSE | Tanzania | Equities | Africa/Dar_es_Salaam | --- ## Fees - **Trading fee**: 0.75% of gross trade value (base, applied to all equity trades) - **Partner markup**: configurable via settings (`markupBps`); applied on top of the 0.75% base; visible in quote response as `partnerMarkupFee` - **Subscription/redemption**: no additional fees on bond/fund subscriptions - **Deposit/withdrawal**: no platform fee; partner handles FX conversion --- ## Official TypeScript SDK The official SDK is published as `@mystocks-africa/partner-sdk` on npm. ### Install ```bash npm install @mystocks-africa/partner-sdk ``` Requires Node.js 18+ (native fetch + Web Crypto API). Pass a custom `fetch` via `options.fetch` for Node 16 or Cloudflare Workers. ### Initialise ```typescript import { MyStocksClient } from '@mystocks-africa/partner-sdk'; const client = new MyStocksClient({ apiKey: 'sk_sandbox_...', // sk_sandbox_ for dev, pk_live_ for production environment: 'sandbox', // or 'production' }); ``` ### Resource namespaces | Namespace | Description | |---|---| | `client.market` | Stocks, ETFs, quotes, movers, OHLCV, exchanges, companies | | `client.subAccounts` | Create, deposit, withdraw, trade, KYC, portfolio | | `client.trading` | Partner master-account trading and order management | | `client.webhooks` | Register, test, inspect delivery history | | `client.reports` | AUM, positions, fees, revenue, invoices | | `client.account` | Profile, settings, API keys, audit log, usage, SLA | | `client.fundFlow` | Master wallet top-up and payout requests | | `client.assetClasses` | Bonds, funds, opportunities, market intel, dividend calendar | ### Idempotency Pass `idempotencyKey` on deposit, withdraw, and trade calls: ```typescript await client.subAccounts.deposit(userId, { amount: 500 }, { idempotencyKey: 'dep_001' }); ``` ### Error handling All errors throw `MyStocksError` (extends `Error`) with `code`, `status`, `requestId`: ```typescript import { MyStocksError } from '@mystocks-africa/partner-sdk'; try { await client.subAccounts.trade(userId, { symbol: 'SCOM.KE', type: 'BUY', quantity: 1000 }); } catch (err) { if (MyStocksError.isInsufficientFunds(err)) { /* top-up prompt */ } if (MyStocksError.isKycRequired(err)) { /* KYC redirect */ } if (MyStocksError.isRateLimited(err)) { /* back off */ } } ``` ### Webhook verification ```typescript import { verifyWebhookSignature } from '@mystocks-africa/partner-sdk'; const valid = await verifyWebhookSignature(rawBody, sig, process.env.WEBHOOK_SECRET); if (!valid) return res.status(401).end(); ``` Uses `crypto.subtle` — works in Node.js 18+, Cloudflare Workers, Vercel Edge. ### SDK source Source lives in `packages/sdk-ts/` in the main repository. Regenerate OpenAPI types: `cd packages/sdk-ts && npm run generate:types` --- ## Contact & Access - Apply for API access: https://mystocks.africa/partners - API documentation: https://mystocks.africa/partners/docs - SLA portal: https://mystocks.africa/partners/sla - Support: support@mystocks.africa - Data licensing: data@mystocks.africa - SLA issues: sla@mystocks.africa - Partnerships: partnerships@mystocks.africa