# 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://partners.mystocks.africa/partners/docs **Apply**: https://mystocks.africa/partners --- ## Public GEO, Dataset, and Citation Assets Use these public pages when an AI assistant, search crawler, analyst, or partner needs citation-ready context outside the API reference: - Data Catalog: https://mystocks.africa/data - Answers Hub: https://mystocks.africa/answers - Methodology: https://mystocks.africa/methodology - Data Sources: https://mystocks.africa/data-sources - African Exchange Statistics Dataset: https://mystocks.africa/african-stock-exchanges/statistics - African Exchange Statistics CSV/JSON: https://mystocks.africa/african-stock-exchanges/statistics.csv | https://mystocks.africa/african-stock-exchanges/statistics.json - African Exchange Trading Hours Dataset: https://mystocks.africa/african-stock-exchanges/trading-hours - African Exchange Trading Hours CSV/JSON: https://mystocks.africa/african-stock-exchanges/trading-hours.csv | https://mystocks.africa/african-stock-exchanges/trading-hours.json - African Market Regulator Directory: https://mystocks.africa/african-stock-exchanges/regulators - African Market Regulator Directory CSV/JSON: https://mystocks.africa/african-stock-exchanges/regulators.csv | https://mystocks.africa/african-stock-exchanges/regulators.json - Africa ETF and Fund Access Dataset: https://mystocks.africa/data/etf-fund-access - Africa ETF and Fund Access CSV/JSON: https://mystocks.africa/data/etf-fund-access.csv | https://mystocks.africa/data/etf-fund-access.json - African Private Market Access Dataset: https://mystocks.africa/data/private-market-access - African Private Market Access CSV/JSON: https://mystocks.africa/data/private-market-access.csv | https://mystocks.africa/data/private-market-access.json - State of African Markets Report: https://mystocks.africa/reports/state-of-african-markets - African Exchange Statistics Report: https://mystocks.africa/reports/african-exchange-statistics - Africa ETF and Fund Access Guide: https://mystocks.africa/guides/africa-etf-and-fund-access - Private Markets Access Guide: https://mystocks.africa/guides/private-markets-access - IPO and Pre-IPO Monitor: https://mystocks.africa/monitors/ipo-pre-ipo Key answer-engine pages: - https://mystocks.africa/answers/can-us-investors-buy-african-stocks - https://mystocks.africa/answers/what-is-the-largest-stock-exchange-in-africa - https://mystocks.africa/answers/how-to-invest-in-african-etfs - https://mystocks.africa/answers/how-do-i-compare-african-stock-exchanges - https://mystocks.africa/answers/what-data-sources-power-african-market-research - https://mystocks.africa/answers/what-is-an-african-money-market-fund - https://mystocks.africa/answers/how-do-etfs-and-funds-differ-in-africa - https://mystocks.africa/answers/what-should-i-check-before-a-pre-ipo-investment - https://mystocks.africa/answers/how-to-access-african-private-markets --- ## Quick Start (5 minutes) Seven 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 — Assert KYC (required before trading, in sandbox and production):** ```bash curl -X POST https://mystocks.africa/api/sandbox/v1/partner/users/usr_xxx/kyc \ -H "Authorization: Bearer sk_sandbox_" \ -H "Idempotency-Key: kyc_user42_001" \ -H "Content-Type: application/json" \ -d '{ "status": "VERIFIED", "level": "BASIC", "reference": "kyc_provider_ref_123" }' # Response: { "message": "KYC status updated.", "kycStatus": "VERIFIED", "kycLevel": "BASIC" } # Trading before KYC returns 403 { "error": { "code": "KYC_REQUIRED", ... } } ``` **Step 5 — Get a quote (returns the required single-use quoteId, valid 60s):** ```bash curl "https://mystocks.africa/api/sandbox/v1/partner/quote/SCOM.KE?type=BUY&quantity=1000&subAccountId=usr_xxx" \ -H "Authorization: Bearer sk_sandbox_" # Response: { "quoteId": "qt_xxx", "quoteExpiresAt": "...", "usdPrice": 0.0126, "gross": 12.60, "fee": 0.09, "totalCost": 12.69, ... } ``` **Step 6 — Place trade (pass the quoteId from Step 5 within 60s):** ```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, "quoteId": "qt_xxx" }' # Response: { "orderId": "ord_xxx", "status": "FILLED", ... } (sandbox: instant fill) ``` **Step 7 — 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:** The `Idempotency-Key: ` header (8–200 chars) is REQUIRED on all money-moving and mutating endpoints — deposit, withdraw, trade, KYC, subscribe, redeem, top-up, payout, and webhook registration — in both production and sandbox. Requests without it return 400 `MISSING_PARAM`. 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 managed FX model over a USD ledger. | Flow | Who handles FX? | What partner provides | What MyStocks does | |------|----------------|----------------------|-------------------| | Funding (deposit) | MyStocks managed FX | amount/amountUsd in USD, or amount + currency such as KES | Converts supported non-USD amounts to USD; credits sub-account USD ledger | | Equity trading | MyStocks managed FX | quoteId from GET /quote/{symbol} | Converts local exchange price to USD, returns fxRate/fxSource/fxConversion on quote and order | | Dividends | MyStocks managed FX | — | Converts dividend to USD at spot; credits sub-account wallet | | Withdrawal | MyStocks managed FX | amount/amountUsd in USD, or amount + currency such as KES | Converts supported non-USD cash-out amount to USD; debits sub-account USD ledger | Use `GET /api/v1/partner/fx/rates` for the managed FX table. Sub-account wallets use USD as the ledger currency, while wallet/account/buying-power responses expose `wallets[]` display balances in supported currencies. MyStocks does not offer hedging or rate locks in this release. --- ## 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 Production starts as a controlled pilot. MyStocks configures approved partners, customer and transaction limits, manual treasury review, daily reconciliation freshness, a named operational owner, a 24/7 money/order incident contact, feature flags, and an immediate suspension control. Pilot restrictions use `PILOT_RESTRICTED`; exhausted financial limits use `PILOT_LIMIT_EXCEEDED`. See `/partners/docs/controlled-production-pilot`. General availability is evidence-gated. It requires automated treasury matching, a tested/buildable Python SDK 1.x release, published SLO and versioning policies, an independent security review with zero unresolved critical/high findings, signed load/failover evidence, and a current BALANCED external broker/CSD reconciliation. See `/partners/docs/general-availability`. | | 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": { "code": "VALIDATION_ERROR", "message": "Human-readable remediation text." } } ``` Use `error.code` for programmatic handling and `error.message` for logs or UI text. Some v1 compatibility aliases may still emit a string-shaped `error`; new partner integrations should expect the structured object shape documented in OpenAPI and `/partners/docs/errors`. | 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." } ``` ### POST /api/v1/partner/apply Public partner application (no auth) — submit business details to request access. Throttled and enumeration-safe. Returns an application reference; approval and key issuance are handled by the MyStocks team (check status via `GET /api/v1/partner/me`). ### GET /api/v1/partner/upgrade-request · POST /api/v1/partner/upgrade-request `POST` requests a tier/limit upgrade (e.g. sandbox → production, higher rate limit, larger credit line). `GET ?type=` returns the status of an existing request. Authenticated with your API key. ### Integration test tools _(deterministic sandbox certification)_ `POST /test-tools/deposit`, `POST /test-tools/trade`, `GET /test-tools/orders`, and sandbox-only `PATCH /test-tools/orders/{orderId}` exercise deposit and order lifecycles without real funds. On the `sk_sandbox_` base, choose `PENDING`, `FILL`, `PARTIAL_FILL`, `REJECT`, `CANCEL`, or `FAIL_SETTLEMENT`, then advance pending orders without staff intervention. Wallet reservations, holdings, execution records, and signed webhooks move together. All mutating calls require `Idempotency-Key`. The production base retains the older live-key tool and deprecated `/sandbox/deposit`, `/sandbox/trade`, and `/sandbox/orders` aliases for compatibility. --- ## 2. Partner Account ### GET /api/v1/partner/me Returns the calling partner's approval status and a **masked** view of their live key. Authenticated with a Firebase ID token (obtained after logging in via the Partner Portal). Use this to check application status. `maskedKey` is for display only and is **not a usable credential** — sending it as a Bearer token returns 401. API keys are stored as SHA-256 hashes, so the raw `pk_live_` key is shown exactly once (at issue or rotation) and cannot be retrieved later. If you have lost it, rotate the key. ```bash curl "https://mystocks.africa/api/v1/partner/me" \ -H "Authorization: Bearer " ``` Response: ```json { "status": "active", "maskedKey": "pk_live_a1b2c3d4...9f2e", "businessName": "Acme Corp", "email": "dev@acme.com" } ``` ### POST /api/v1/partner/session Exchanges a Firebase ID token (a Partner Portal login) for a short-lived `ms_oauth_` access token that can be sent as `Authorization: Bearer ` to any partner endpoint. This is how a signed-in browser session calls the API, given the raw key is unrecoverable. The token mirrors the underlying key's scopes and type exactly, so a portal session can never do more than the raw key could. It expires after 1 hour. Server-to-server integrations should use their `pk_live_` key directly, or `POST /oauth/token` — not this endpoint. ```bash curl -X POST "https://mystocks.africa/api/v1/partner/session" \ -H "Authorization: Bearer " ``` Response: ```json { "accessToken": "ms_oauth_9f2e...", "tokenType": "Bearer", "expiresIn": 3600, "keyType": "full" } ``` `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 holdings for the partner master trading account only. It does not consolidate sub-accounts. Response: ```json { "accountType": "MASTER", "summary": { "walletBalance": 2500, "portfolioValue": 8250, "investedCapital": 7900, "unrealisedPnl": 350, "totalValue": 10750 }, "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" } ``` ### GET /api/v1/partner/portfolio/history Daily raw-equity history for the master trading account only. Query params: `period` (1M|3M|6M|1Y|ALL), `from`, `to`. The series is not cash-flow adjusted, so deposits and withdrawals affect equity changes and the response must not be presented as investment P&L. ### GET /api/v1/partner/portfolio/performance Cash-flow-adjusted master-account performance. Query params: `period` (1M|3M|6M|1Y|ALL), `from`, `to`, and optional exchange-qualified `benchmark`. Returns daily P&L, Modified Dietz daily returns, geometrically linked cumulative TWR, and optional benchmark/excess return. Completed internal transfers and completed external top-ups/payouts are cash-flow adjusted. Benchmark comparison is price return only and excludes dividends and FX. ### GET /api/v1/partner/buying-power Settlement-aware buying power for the master account: settled cash available to trade now, unsettled proceeds still in the settlement cycle, and the withdrawable balance (excludes unsettled funds). Withdrawing more than the withdrawable balance returns `UNSETTLED_FUNDS`. (Per sub-account: `GET /api/v1/partner/users/{userId}/buying-power`.) ### GET /api/v1/partner/tax-lots Open tax lots (FIFO cost-basis layers) for the master account, for accurate gain/loss accounting. Query params: `symbol` (optional filter). (Per sub-account: `GET /api/v1/partner/users/{userId}/tax-lots`.) ### GET /api/v1/partner/gains Realized capital gains for the master account, computed FIFO against closed lots. Query params: `symbol`, `from`, `to` (ISO dates). (Per sub-account: `GET /api/v1/partner/users/{userId}/gains`.) --- ## 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": "Communication Services", "description": "Safaricom is the largest telecommunications provider in Kenya.", "listingStatus": "ACTIVE", "price": 16.50, "usdPrice": 0.1274, "change": 0.25, "changePct": 0.015385, "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. Partner-facing sector labels and `sector` filters are normalized (trimmed, title-cased, and mapped to canonical common labels such as Financials and Communication Services). ### GET /api/v1/partner/stocks/{symbol} Delayed quote for a single stock: current observed price (local + USD), day range, and an embedded closing-price history for the requested range (with the latest price appended as the trailing `Live` point). Use for a detail page + inline chart in one call. `changePct` is a decimal return and is derived from `price` versus `previousClose` when the stored field is absent. Path: `symbol` — stock symbol (e.g. SCOM.KE), slug (e.g. safaricom), or ISIN. Case-insensitive. Query params: `range` (1W|1M|3M|6M|1Y|ALL, default 3M) Response: ```json { "symbol": "SCOM.KE", "name": "Safaricom PLC", "isin": "KE0000000067", "slug": "safaricom", "exchange": "NSE", "currency": "KES", "sector": "Communication Services", "listingStatus": "ACTIVE", "price": 16.50, "usdPrice": 0.127306, "currentPrice": 16.50, "change": 0.25, "changePct": 0.015385, "open": 16.30, "dayHigh": 16.80, "dayLow": 16.20, "volume": 4820000, "previousClose": 16.25, "lastPriceUpdate": "2026-05-01T10:30:00.000Z", "historyConfidence": "HIGH", "lastEodUpdate": "2026-04-30T15:00:00.000Z", "range": "3M", "priceHistory": [ { "date": "2026-02-01", "price": 15.80 }, { "date": "2026-04-30", "price": 16.25 }, { "date": "2026-05-01T10:30:00.000Z", "price": 16.50, "label": "Live" } ], "count": 64 } ``` `price` is the canonical live-price field; `currentPrice` is a deprecated alias — prefer `price`. `usdPrice` is `null` when no valid FX/market price is available. ### GET /api/v1/partner/stocks/{symbol}/history Pure OHLCV candle series (no live-quote fields) — use for candlestick charts and technical analysis. Path: `symbol` — stock symbol, slug, or ISIN. Case-insensitive. Query params: `period` (1D|1W|1M|3M|6M|1Y|3Y|5Y|ALL, default 1M; `MAX` = `ALL`) Response: ```json { "symbol": "SCOM.KE", "period": "1M", "candles": [ { "date": "2026-04-01", "open": 15.80, "high": 15.95, "low": 15.70, "close": 15.85, "volume": 3120000 }, { "date": "2026-04-30", "open": 16.20, "high": 16.40, "low": 16.10, "close": 16.25, "volume": 4010000 } ] } ``` `open`/`high`/`low` fall back to `close` on days where only a closing print exists; `volume` is `0` (never `null`) when unrecorded. ### GET /api/v1/partner/stocks/{symbol}/candles Preferred chart-first historical endpoint. Returns delayed OHLCV candles in the listing currency with explicit inclusive dates, server-side aggregation, volume availability, and OHLC provenance. Path: `symbol` — exchange-qualified stock symbol (recommended) or unambiguous bare ticker. Query params: `interval` (`1d`|`1w`|`1mo`, default `1d`), `from` and `to` (`YYYY-MM-DD`, inclusive), and `adjustment` (`raw` only). Maximum ranges are 366 days for `1d`, 1,826 days for `1w`, and 3,653 days for `1mo`. Minute/hour bars, WebSocket candle updates, adjusted prices, and server-side technical indicators are not available. Response: ```json { "symbol": "SCOM.KE", "exchange": "NSE", "currency": "KES", "interval": "1d", "adjustment": "raw", "candles": [ { "timestamp": "2026-08-07T00:00:00.000Z", "open": 16.2, "high": 16.75, "low": 16.1, "close": 16.5, "volume": 2450000 } ], "meta": { "from": "2026-01-01", "to": "2026-08-10", "count": 147, "source": "delayed_eod", "asOf": "2026-08-07T00:00:00.000Z", "ohlcQuality": "reported", "volumeAvailable": true, "adjusted": false } } ``` `ohlcQuality` is `reported`, `mixed`, or `close_derived`. When an older source row has only a close, open/high/low fall back to close so the candle remains renderable without hiding its provenance. ### 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 · GET /api/v1/partner/market/clock Returns current open/closed status for all African exchanges. All times calculated from real exchange trading hours. `market/clock` is an identical alias. Both return `serverTime`, holiday-aware `nextOpen`, `nextClose`, and the current `session` while open. 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": "2026-05-11T06:00:00.000Z", "nextClose": "2026-05-10T12:00:00.000Z", "serverTime": "2026-05-10T10:30:00.000Z", "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} The required pre-trade quote. Returns a fee breakdown **and** a single-use `quoteId` that must be passed to the matching trade call. The quote is valid for `quoteTtlSeconds` (60s) — after `quoteExpiresAt`, or once used, placing a trade with it returns `409 STALE_QUOTE`. Fetch a fresh quote per confirmation sheet. Path: `symbol` — stock symbol or slug Query params: `type` (BUY|SELL, default BUY), `quantity` **or** `cashValue` (mutually exclusive — `cashValue` sizes a notional/fractional order in USD), `subAccountId` (optional — quote against a sub-account wallet/holdings instead of the master account) BUY quote response: ```json { "quoteId": "qt_9f3c...", "quoteTtlSeconds": 60, "quoteIssuedAt": "2026-05-10T10:30:00.000Z", "quoteExpiresAt": "2026-05-10T10:31:00.000Z", "sourceUpdatedAt": "2026-05-10T10:29:40.000Z", "dataFreshnessSeconds": 20, "symbol": "SCOM.KE", "name": "Safaricom PLC", "exchange": "NSE", "accountType": "MASTER", "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 { "quoteId": "qt_71ab...", "quoteTtlSeconds": 60, "quoteExpiresAt": "2026-05-10T10:31:00.000Z", "symbol": "SCOM.KE", "type": "SELL", "quantity": 100, "accountType": "MASTER", "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: `quoteId` (single-use, pass to POST /trade) | `quoteExpiresAt`/`quoteTtlSeconds` (60s validity) | `dataFreshnessSeconds` (age of the underlying price) | `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). For a `cashValue` quote, `quantity` is indicative — place the trade with the same `cashValue`. ### 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/snapshot One-call bundle per symbol: the delayed `quote` (asOf, stale, dataQuality), today's `dailyBar`, the `prevDailyBar`, and the latest coarse `intradayBar` sample. Replaces the 3–4 round-trips a dashboard makes. Query params: single mode `symbol` + `exchange`; batch mode `symbols` (comma-separated, ≤50). Single mode returns one snapshot under `data`; batch mode returns a symbol→snapshot map plus `not_found`. `dailyBar` is derived from the live quote until the end-of-day candle is written. `intradayBar` is `null` until the 3×/day intraday capture has run for the symbol. ```json { "data": { "SCOM.KE": { "symbol": "SCOM.KE", "quote": { "symbol": "SCOM.KE", "price": 35.5, "changePct": -0.004237, "asOf": "2026-07-15T11:45:02.000Z", "stale": false }, "dailyBar": { "timestamp": "2026-07-15T00:00:00.000Z", "open": 35.4, "high": 35.9, "low": 35.0, "close": 35.5, "volume": 45394 }, "prevDailyBar": { "timestamp": "2026-07-14T00:00:00.000Z", "open": 35.2, "high": 35.5, "low": 34.9, "close": 35.4, "volume": 38220 }, "intradayBar": null } }, "not_found": [] } ``` ### 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) Instruments use stored `changePct` when present or derive it from `price` versus `previousClose`; rows lacking both inputs are excluded. 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 } } ``` ### GET /api/v1/partner/market/ohlcv End-of-day OHLCV candles for one symbol. Query params: `symbol` (required), `exchange` (required), `interval` (default `1d`), `from`, `to` (ISO dates). Response: `{ symbol, exchange, interval, candles: [{ date, open, high, low, close, volume }] }`. ### GET /api/v1/partner/market/exchanges Static directory of the supported exchanges with `code`, name, country, `currency`, trading hours, timezone, and settlement cycle. No params. Use to build exchange pickers. ### GET /api/v1/partner/market/status (single/all) · GET /api/v1/partner/market/holidays `GET /api/v1/partner/market/holidays` — exchange holiday calendar. Query params: `exchange` (optional), `from`, `to` (ISO dates). Response: `{ holidays: [{ exchange, date, name }] }`. Combine with `market/status` to know when an order will queue to the next session. ### GET /api/v1/partner/market/settlement Settlement cycles per exchange plus the live fill target. Query params: `exchange` (optional). Returns `processingWindow` — `targetTurnaroundMinutes` (5, in-session), the outside-hours "queued to next session" rule, and per-exchange settlement cycle (e.g. T+3). Use to set fill-time expectations in your UI. > **Deprecated aliases:** `GET /api/v1/partner/market-data/quotes`, `/market-data/movers`, `/market-data/exchanges`, and `/market-data/ohlcv` still work but are deprecated — use the `/market/*` paths above. Read-only data keys must call `/market/*`. --- ## 5b. ETFs ### GET /api/v1/partner/assets Unified catalog for stocks, ETFs, bonds, and funds. Query params: `assetClass`, `exchange`, `status`, `tradable`, `search`, `limit`, `cursor`. Each item has a stable `assetId`, nullable `symbol`, normalized status, `tradable`/`fractionable`/`shortable`/`marginable` flags, detailed `capabilities`, `orderRules`, and settlement metadata. Unknown lot and tick sizes are returned as null. ### 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) **Production trading is a two-step flow.** Step 1: `GET /api/v1/partner/quote/{symbol}?type=&quantity=[&subAccountId=]` returns the fee breakdown AND a `quoteId`. Step 2: pass that `quoteId` in the trade body. quoteIds are single-use, bound to the exact account/symbol/side/quantity, and expire **60 seconds** after issue — a stale or reused quoteId returns 409 (fetch a fresh quote and retry). Quotes are indicative: settlement executes at market price within a deviation band (default ±10% of the quoted price); beyond that the dealing desk must explicitly acknowledge before settling. The sandbox mirrors this contract exactly (same quoteId requirement, issued by the sandbox quote endpoint). Optional OMS fields on all trade calls: `clientOrderId` (≤80 chars, unique per partner — retry/dedupe handle), `timeInForce` (`GTC` default = live until executed/cancelled; `DAY` = auto-cancelled after 24h; `GTD` = auto-cancelled at required `expiresAt`; `IOC` is NOT supported). Expiry refunds BUY escrow, releases SELL reservations, and fires an `order.cancelled` webhook. ### 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` header and `quoteId` in the body. Body: ```json { "symbol": "SCOM.KE", "type": "BUY", "quantity": 1000, "quoteId": "qt_9f2c81d4b7a3", "clientOrderId": "my-ord-10001" } ``` 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 master order. Possible while status is PENDING (MARKET order at the dealing desk) or WORKING (resting LIMIT/STOP/STOP_LIMIT order). BUY escrow is refunded and SELL unit reservations released atomically. ### PATCH /api/v1/partner/orders/{orderId} Modify (replace) a resting LIMIT/STOP/STOP_LIMIT order on the master account, keeping the same `orderId`. Only WORKING orders can be modified — a MARKET order is already PENDING at the desk and can only be cancelled. Check `replaceable` on the order. Supply any combination of `limitPrice`, `stopPrice`, `quantity`. Repriced on the same FX basis and fee rates captured at placement. A BUY re-escrows the difference (400 `INSUFFICIENT_FUNDS` if the wallet cannot cover an increase); a SELL adjusts reserved units. Requires `Idempotency-Key`. Emits `order.replaced`. The sub-account equivalent is `PATCH /api/v1/partner/users/{userId}/orders/{orderId}`. Body: ```json { "limitPrice": 18.50, "quantity": 200 } ``` --- ## 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 { "status": "VERIFIED", "level": "BASIC", "provider": "sumsub", "reference": "kyc_session_88721", "idDocumentType": "NATIONAL_ID", "idNumber": "24681012", "dateOfBirth": "1990-04-17", "nationality": "KE", "taxResidency": "KE", "sanctionsResult": "CLEAR", "riskRating": "LOW", "employmentStatus": "EMPLOYED", "sourceOfFunds": "SALARY", "sourceOfWealth": "EMPLOYMENT_INCOME", "annualIncomeBand": "50000_100000", "netWorthBand": "100000_500000", "investmentExperience": "LIMITED", "investmentObjectives": ["GROWTH", "RETIREMENT"], "suitabilityStatus": "SUITABLE", "appropriatenessStatus": "SUITABLE", "fatcaStatus": "NOT_US_PERSON", "crsTaxResidencies": [{ "country": "KE", "taxId": "A123456789B" }], "marketAccountReferences": [{ "market": "NSE", "brokerAccountNumber": "BRK-123456", "csdNumber": "CSD-987654" }] } ``` Required fields are `status` and `level`. Optional structured compliance fields cover identity evidence, sanctions/PEP screening, source of funds/wealth, income/net-worth bands, investment experience/objectives, suitability/appropriateness, FATCA/CRS declarations, and market-specific broker/CSD references. Sensitive identifiers are stored as fingerprint plus last4 only. Response: ```json { "message": "KYC status updated.", "subAccountId": "usr_abc123", "kycStatus": "VERIFIED", "kycLevel": "BASIC", "profile": { "idDocumentType": "NATIONAL_ID", "idNumberLast4": "1012", "nationality": "KE", "taxResidency": "KE", "sanctionsResult": "CLEAR", "riskRating": "LOW" } } ``` --- ## Notification Devices ### GET /api/v1/partner/users/{userId}/devices List mobile/web push-token records registered for a sub-account. Raw APNs/FCM/Expo/Web Push tokens are never returned; responses include `tokenLast4` only. ### POST /api/v1/partner/users/{userId}/devices Register or update a device token for partner-owned mobile notification fanout. Requires `Idempotency-Key`. Body: ```json { "token": "ExpoPushToken[xxxxxxxxxxxxxxxxxxxxxx]", "platform": "ios", "provider": "expo", "appId": "com.afritrade.app", "deviceId": "device-42" } ``` ### DELETE /api/v1/partner/users/{userId}/devices/{deviceId} Soft-revoke a registered notification device. Requires `Idempotency-Key`. --- ## 14. Sub-Account Trading ### GET /api/v1/partner/users/{userId}/preflight Run a side-effect-free eligibility check before requesting a firm quote. Required query parameters are `symbol`, `type` (`BUY` or `SELL`), and either positive `quantity` or positive `cashValue` (mutually exclusive). The response combines account/KYC status, market session, indicative price, buying power or holdings, estimated fees, settlement expectations, `eligible`, and any `blockingReasons`. Sandbox path: `/api/sandbox/v1/partner/users/{userId}/preflight`. ### POST /api/v1/partner/users/{userId}/trade Place a BUY or SELL for a sub-account. BUY funds escrow atomically on submission. Requires `Idempotency-Key`. `MARKET` orders, including calls where `orderType` is omitted, require a single-use `quoteId` from `GET /quote/{symbol}?subAccountId={userId}` fetched within the last 60 seconds; stale or reused quoteIds return `409 STALE_QUOTE`. Resting `LIMIT`, `STOP`, and `STOP_LIMIT` orders do not use `quoteId`; they supply `limitPrice` and/or `stopPrice`, return `WORKING`, and activate when their trigger conditions are met. Use either `quantity` or `cashValue`, never both. The sandbox mirrors this contract exactly. MARKET body: ```json { "symbol": "SCOM.KE", "type": "BUY", "quantity": 100, "quoteId": "qt_9f2c81d4b7a3", "stopLoss": 14.50, "takeProfit": 18.00 } ``` `stopLoss` and `takeProfit` are optional price thresholds in USD. Resting LIMIT body: ```json { "symbol": "SCOM.KE", "type": "BUY", "orderType": "LIMIT", "quantity": 100, "limitPrice": 16.25, "timeInForce": "GTC", "clientOrderId": "my-limit-10001" } ``` Cash-value MARKET body: ```json { "symbol": "SCOM.KE", "type": "BUY", "cashValue": 50, "quoteId": "qt_cash_9f2c81d4b7a3" } ``` 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: MARKET orders fill instantly in sandbox and enter the production execution queue. LIMIT/STOP/STOP_LIMIT orders rest as `WORKING` in both environments and can be filled, rejected, or cancelled. ### GET /api/v1/partner/users/{userId}/orders List sub-account orders. Query params: `status` (PENDING|WORKING|PROCESSING|COMPLETED|FILLED|REJECTED|CANCELLED), `symbol` (exact exchange-qualified symbol, e.g. SCOM.KE), `cursor`, `limit` (default 50, max 200). Symbol/status filtering is index-safe in production and sandbox. ### 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 an active PENDING or WORKING sub-account order in production or sandbox. BUY cash and SELL unit reservations are released. --- ## 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}/portfolio/history Daily raw-equity history for one partner-owned sub-account. Query params: `period` (1M|3M|6M|1Y|ALL), `from`, `to`. The series includes wallet cash and holdings and is explicitly not cash-flow adjusted P&L. ### GET /api/v1/partner/users/{userId}/portfolio/performance Cash-flow-adjusted performance for one partner-owned sub-account. Query params: `period` (1M|3M|6M|1Y|ALL), `from`, `to`, and optional exchange-qualified `benchmark`. Completed deposits and withdrawals are external cash flows. Returns daily P&L, Modified Dietz daily returns, linked cumulative TWR, and optional benchmark/excess return. ### GET /api/v1/partner/users/{userId}/transactions Transaction history. Query params: `type` (DEPOSIT|WITHDRAWAL|INVEST|SELL|DISTRIBUTION|REDEEM|FEE|TRANSFER_IN|TRANSFER_OUT), `from` (ISO date), `to` (ISO date), `cursor`, `limit`. Completed rows include `settledAt`; legacy completed rows return their immutable completion/creation timestamp when the original field was absent. ### GET /api/v1/partner/users/{userId}/watchlist List the symbols a sub-account has saved. Response: `{ subAccountId, watchlist: [{ symbol, addedAt }] }`. ### POST /api/v1/partner/users/{userId}/watchlist Add a symbol to the sub-account watchlist. Body: `{ "symbol": "SCOM.KE" }`. Requires `Idempotency-Key`. ### DELETE /api/v1/partner/users/{userId}/watchlist/{symbol} Remove a symbol from the watchlist. Requires `Idempotency-Key`. --- ## 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 } ] } ``` --- ### GET /api/v1/partner/report/reconciliation Daily reconciliation pack: cash ledger, securities ledger, unsettled trades, fees, dividends, corporate actions, and custody positions across the master account and all sub-accounts. JSON includes `proof.status`, a cash roll-forward, and per-instrument beneficial-versus-custody exceptions. Query params: `asOf` (YYYY-MM-DD) or `from`/`to` range; `format=csv§ion=summary|cash|securities|unsettled|fees|dividends|custody_positions` downloads one section as CSV. ### GET /api/v1/partner/orders/{orderId}/executions ### GET /api/v1/partner/users/{userId}/orders/{orderId}/executions OMS-style execution reports — every order lifecycle transition (accepted, processing, completed, rejected, cancelled) with timestamps, for partner-level and sub-account orders. A filled legacy order that predates the immutable journal returns one compatibility FILL marked `synthetic: true` and `sourceEvidence: LEGACY_ORDER_STATE`, never an unexplained empty list. ### GET /api/v1/partner/client-activity Unified reverse-chronological activity feed across all sub-accounts: trades, deposits, withdrawals, subscriptions, redemptions, KYC changes. Supports `limit` and cursor pagination. --- ## 17b. Enterprise Security & Certification (production only) ### GET /api/v1/partner/security ### PATCH /api/v1/partner/security Read/update the enterprise security policy for your key: `ipAllowlist`, `requireSignedRequests`, `requireMtls`, `keyRotationDays`, scoped-key governance, and security evidence URLs. Once signed requests or mTLS are enabled, later write requests must satisfy the configured controls. ### POST /api/v1/partner/oauth/token OAuth 2.0 client-credentials grant (requires `oauthClientCredentialsEnabled` in the security policy). Send the `pk_live_` key as client secret via HTTP Basic auth, form body, or JSON body; returns a 15-minute `ms_oauth_` Bearer token, optionally narrowed to a scope subset (e.g. `market:read trading:read`). ### GET /api/v1/partner/certification ### PATCH /api/v1/partner/certification ### POST /api/v1/partner/certification Go-live certification checklist (sandbox golden path, concurrency/idempotency, webhook retry, failover drill, reconciliation exports, enterprise security, MyStocks approval). PATCH partner-owned checks with `status`, `evidenceUrl`, `notes`; POST requests the final MyStocks go-live review. --- ## 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. ### GET /api/v1/partner/float Master-account funding headroom: current USD master balance, your `creditLimitUsd` line, `lowBalanceThresholdUsd`, and `availableToFundUsd` (balance + credit line still available to fund sub-accounts). Poll this, or subscribe to the `float.low` event, to remit before end-user funding calls start failing. ### PATCH /api/v1/partner/float Set your low-balance alert threshold (`lowBalanceThresholdUsd`) — the level at which a `float.low` webhook/stream event fires. Requires `Idempotency-Key`. --- ## 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/dividends/{symbol}/history Historical dividend declarations for a single symbol (ex-date, pay-date, amount, currency), regardless of holdings. Query params: `limit`. Market-data endpoint — usable with a read-only data key. ### 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. ### POST /api/v1/partner/webhooks/{id}/test Fire a signed test delivery to a registered webhook so you can verify signature handling and endpoint reachability. Requires `Idempotency-Key`. Returns the HTTP status your endpoint responded with. ### 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` ### GET /api/v1/partner/stream Real-time event stream over Server-Sent Events (`text/event-stream`) — the polling-free alternative to (or complement of) webhooks. Emits the same events (`order.filled`, `order.rejected`, `order.cancelled`, `order.triggered`, `deposit.confirmed`, `withdraw.confirmed`, `kyc.updated`, `dividend.paid`, `float.low`, …). Auth: `Authorization: Bearer pk_live_…` (server) or `?access_token=` for a browser `EventSource` (which can't set headers). Resume after a disconnect with the `Last-Event-ID` header or `?since=` (replays up to the last hour). The stream self-closes near 110s; `EventSource` reconnects automatically. --- ## 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" }` ### GET /api/v1/partner/pricing Returns your effective markup pricing: the default `markupBps` plus any per-symbol / per-exchange / per-asset-class overrides that determine the `partnerMarkupFee` added on top of the 0.75% MyStocks base fee. ### PATCH /api/v1/partner/pricing Update your markup pricing (default `markupBps` and override rules). Applies to future quotes and trades. Requires `Idempotency-Key`. --- ## 23b. Price Alerts Back the `price.alert` webhook. Register a threshold on a symbol; when the LIVE price crosses it while the exchange is open, `price.alert` fires (webhook + SSE stream). Production keys only — not mirrored in sandbox. Thresholds are in the instrument's **local trading currency** — the same basis as a quote's `price`. Alerts are evaluated on the same ~15-minute polling cycle as quotes, so an alert fires on the first poll after the crossing, not at the instant of the tick. Do not use a price alert as an execution trigger; place a resting LIMIT or STOP order instead. Maximum 200 active alerts. ### POST /api/v1/partner/price-alerts Create a price alert. Requires `Idempotency-Key` and a full key (`pk_live_`). Body: ```json { "symbol": "SCOM.KE", "exchange": "NSE", "condition": "above", "threshold": 20.00, "repeat": false, "clientAlertId": "my-ref-1" } ``` `repeat: false` (default) fires once then disarms. `repeat: true` re-arms, but only after the price crosses back through the threshold, so a price hovering at the threshold cannot spam the endpoint. State is one of `ARMED`, `TRIGGERED`, `DISARMED`. ### GET /api/v1/partner/price-alerts List your alerts. Optional `?state=ARMED|TRIGGERED|DISARMED` and `?limit=` (max 200). ### GET /api/v1/partner/price-alerts/{alertId} Fetch a single alert. ### DELETE /api/v1/partner/price-alerts/{alertId} Remove an alert. Requires `Idempotency-Key` and a full key. --- ## 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` --- ## Partner Operations Evidence - `GET /documents` — list PDF document templates and recent generation evidence. - `POST /documents` — generate an account statement, compliance summary, or partner-wide operations-summary PDF. - `GET /notification-rules` — retrieve notification rules and the supported event/channel catalogues. - `PUT /notification-rules` — replace the validated notification policy (maximum 50 rules). - `GET /reconciliation-signoffs` — list dated reconciliation review sign-offs. - `POST /reconciliation-signoffs` — record reconciliation review evidence and exception counts. Writes require an `Idempotency-Key`. Customer-specific documents are restricted to sub-accounts owned by the authenticated partner. Generated-document records and reconciliation sign-offs are retained as audit evidence. --- ## Contact & Access - Apply for API access: https://mystocks.africa/partners - API documentation: https://partners.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 ## Corporate Governance API Corporate-action discovery and authoritative terms: - `GET /corporate-actions` — list events across symbols and exchanges; filters: symbol, exchange, type, status, limit. - `GET /corporate-actions/{actionId}` — retrieve authoritative terms, options, deadlines, defaults, and documents. - `GET /users/{userId}/corporate-actions/{actionId}/entitlement` — retrieve record-date entitlement. - `GET /users/{userId}/corporate-actions/{actionId}/elections` — track the latest election revision and custodian evidence. - `POST /users/{userId}/corporate-actions/{actionId}/elections` — submit or amend an election; requires Idempotency-Key. Shareholder meetings and proxy voting: - `GET /shareholder-meetings` — list AGM, EGM, court, class, and other meetings. - `GET /shareholder-meetings/{meetingId}` — retrieve meeting details, proxy documents, resolutions, and ballot choices. - `GET /users/{userId}/shareholder-meetings/{meetingId}/ballot` — retrieve record-date voting entitlement and ballot. - `GET /users/{userId}/shareholder-meetings/{meetingId}/votes` — track vote revisions and custodian-confirmation evidence. - `POST /users/{userId}/shareholder-meetings/{meetingId}/votes` — submit or amend resolution-level votes; requires Idempotency-Key. Successful writes start in `PENDING_CUSTODIAN`. Downstream acceptance is established only when the instruction becomes `ACCEPTED`; retain the custodian reference, accepted timestamp, and confirmation document URL. Production and sandbox expose the same paths. Sandbox terms, eligibility, and evidence are simulated and are not proof of a live market instruction. ## Partner Console Access and Support Human console access is separate from machine API scopes. Organization members receive system or custom roles; permissions are checked against the current member record on every console request. These routes require a short-lived member-bound console session and are not available to raw partner API keys: - `GET /team` and `PATCH /team` — organization, members, role catalogue, and privileged-role MFA policy. - `POST /team/invites` and `DELETE /team/invites` — create or cancel verified-email invitations. - `PATCH /team/members/{memberId}` — change roles, suspend, or restore a member; the final Owner is protected. - `GET /team/roles`, `POST /team/roles`, `PATCH /team/roles/{roleId}`, and `DELETE /team/roles/{roleId}` — inspect and manage custom roles. - `GET /sessions` and `DELETE /sessions` — inspect or immediately revoke console sessions. - `GET /support` and `POST /support` — list or open organization-scoped P0–P3 support cases. - `GET /support/{ticketId}` and `PATCH /support/{ticketId}` — retrieve details or resolve/close a case. - `POST /support/{ticketId}/messages` — add a partner-visible case reply. Support cases carry tier-specific acknowledgement and next-update clocks plus optional request, order, sub-account, and transaction references. Do not include credentials or unredacted identity/payment data. ## Maker-Checker and Enterprise Access Human high-risk operations can be governed by an organization maker-checker policy. The initiator cannot approve their own request; evidence is bound to the canonical payload fingerprint and execution idempotency key. Approved executions send `X-MS-Approval-Id` with the original mutation. - `GET/PATCH /approvals/policies` — read or configure action rules, thresholds, approver count, expiry, and delegation. - `GET/POST /approvals` and `GET/PATCH /approvals/{approvalId}` — request, inspect, approve, reject, cancel, or delegate operations. - `GET/PATCH /enterprise/sso` — configure platform-activated SAML or OIDC SSO. - `GET/POST/DELETE /enterprise/scim` — manage one-time SCIM provisioning tokens. - `GET/POST/PATCH /enterprise/access-reviews` — list/start reviews and configure recurring certification. - `GET/PATCH /enterprise/access-reviews/{reviewId}` — inspect subjects and record access decisions. - `GET /enterprise/evidence` — download a no-cache JSON evidence package with a SHA-256 Digest header. SCIM 2.0 is served from `/api/scim/v2` and supports Users, Groups, and ServiceProviderConfig. SCIM tokens are shown once, stored as hashes, and scoped to one organization. Removing a user revokes active console access. # On-prem deployment Source: https://partners.mystocks.africa/partners/docs/onprem ## What runs on your infrastructure? The on-prem image hosts the partner workspace and a restricted HTTP proxy on your own HTTPS domain. MyStocks Cloud remains authoritative for trading, balances, membership, permissions and licensing. An active cloud connection is required: this is not an offline exchange. Reviewed October 3, 2026. Use the approved release digest and installation instructions supplied for your organization. ## Start here 1. [Request onboarding and access](https://partners.mystocks.africa/partners/onprem#request). Choose temporary evaluation or discuss Enterprise pricing with sales. 2. [Prepare and install](/partners/docs/onprem-installation): host, registry, domain, TLS and installer options. 3. [Activate and manage keys](/partners/docs/onprem-activation): verification, expiry, renewal and revocation. 4. [Connect applications](/partners/docs/onprem-connectivity): sign-in, API credentials, sandbox and proxy behavior. 5. [Operate and troubleshoot](/partners/docs/onprem-operations): readiness, upgrades, rollback and support. ## Hosted versus on-prem responsibilities | Responsibility | MyStocks-hosted workspace | On-prem workspace | | --- | --- | --- | | Workspace hosting and TLS | MyStocks | Your infrastructure team | | Server patches, backups and monitoring | MyStocks | Your infrastructure team | | Local proxy, DNS and release upgrades | MyStocks | Your infrastructure team | | API authorization, trading and balances | MyStocks Cloud | MyStocks Cloud | | Team permissions and identity policies | Authorized organization administrators | Authorized organization administrators | | Installation license and domain approval | Not an on-prem requirement | MyStocks administrator approves; your team installs securely | ## What does the license include? A license permits the installation to connect. It does not expand your API entitlements, supported markets, permissions or sandbox capabilities. Temporary evaluation has an administrator-set expiry. Permanent activation has no scheduled expiry but remains revocable. Pricing, scope and service terms are agreed with sales; this guide makes no additional availability or latency guarantee. ## Can we operate without cloud connectivity? No. Readiness and protected API access fail closed when license verification cannot reach MyStocks. Public pages may still load while the installation is not ready. Plan monitoring and recovery around both local infrastructure and the cloud dependency. ## Public addresses The public documentation home is [partners.mystocks.africa/partners/docs](https://partners.mystocks.africa/partners/docs). Cloud production API calls continue to use `https://mystocks.africa/api/v1/partner`; moving documentation does not change API endpoints. For agents, use the [documentation catalog](/partners/docs/catalog.json), [plain-text source](/partners/docs/raw/onprem), and [OpenAPI contract](https://mystocks.africa/openapi.json). Never include credentials in prompts, examples or support tickets. # Install the on-prem platform Source: https://partners.mystocks.africa/partners/docs/onprem-installation [Journey overview](/partners/docs/onprem) · Next: [activation and keys](/partners/docs/onprem-activation) ## Before installation - Obtain an approved partner organization, active full API credential and authorized team accounts. - Prepare a Linux AMD64 or ARM64 host with Docker Engine, Docker Compose and Python 3. Capacity is agreed during onboarding; no universal sizing guarantee is implied. - Control an exact public domain and publish its supplied DNS TXT ownership challenge. Point A and any AAAA records to working addresses; remove stale IPv6 records. - Allow inbound 80/443 as required by your HTTPS proxy and certificate challenge. Keep the application port private (default loopback port 3000). - Allow DNS and outbound HTTPS to MyStocks, Firebase authentication and your approved image registry. Maintain accurate system time. - Obtain a domain-bound license through a secure channel, registry reader access and an immutable approved image digest. Registry access and the installation license are separate credentials. - Have the identity administrator register your installation domain in the authoritative Firebase project's authorized domains. - Use Safari 16.4+, Chrome 111+ or Firefox 128+ for the workspace. ## Default installation: Docker and Caddy Authenticate Docker to the registry specified in your release instructions before installation. Download and review the installer; do not pipe an unreviewed script into a privileged shell. ```sh curl -fsSLo install-onprem.sh https://mystocks.africa/install-onprem.sh # Review the file. Replace the domain and image with approved values. sudo bash install-onprem.sh --domain trading.example.com \ --image REGISTRY/IMAGE@sha256:DIGEST \ --mode docker --proxy caddy ``` Enter the license at the hidden terminal prompt. For automation, use `--key-file PATH` with a file readable only by its owner; never put a license in command arguments, shell history or browser code. The installer verifies the license before pulling the image and writes a protected environment file under `/opt/mystocks` by default. The installer uses flags rather than an interactive deployment-choice menu. Docker and Caddy are the defaults. ## Select a proxy | Flag | Required preparation | | --- | --- | | `--proxy caddy` | Working public DNS and certificate-challenge reachability; Caddy obtains TLS certificates. | | `--proxy nginx` | Install the generated configuration, provide certificates at its configured paths and validate with `nginx -t`. Certificate issuance is your responsibility. | | `--proxy traefik` | Provide the existing proxy network, `websecure` entrypoint and `letsencrypt` resolver. Select a network with `--traefik-network`. | | `--proxy none` | Supply and manage an external TLS reverse proxy yourself. | Preserve the verified domain's Host header, keep upstream port 3000 private, and disable response buffering for server-sent events. Do not disable TLS certificate verification. The generated installer configuration differs from the repository's example Compose networking; use one complete configuration rather than mixing fragments. ## Native service option Use `--mode bare-metal --proxy nginx` or `--proxy none` on Debian 12 or Ubuntu 24.04 with Node.js 22 and systemd. Docker is still required to extract the matching glibc image bundle. The application runs as a dedicated unprivileged service user; this is not a Docker-free installation path. ## Verify before accepting traffic ```sh curl --fail --show-error https://trading.example.com/api/live curl --fail --show-error https://trading.example.com/api/health ``` Liveness alone is insufficient. Confirm readiness, certificate hostname and chain, authorized sign-in and a read-only API call. Then test authorized sandbox workflows, rejection of anonymous/cross-partner requests, and restart recovery in a dedicated test installation. `--render-only` produces configuration with an invalid fixture credential; it does not deploy or verify a working installation. Continue with [API connectivity](/partners/docs/onprem-connectivity) and [operations](/partners/docs/onprem-operations). # On-prem activation and keys Source: https://partners.mystocks.africa/partners/docs/onprem-activation [Journey overview](/partners/docs/onprem) · Next: [connect applications](/partners/docs/onprem-connectivity) ## Request and provision access 1. Submit the [OnPrem Access form](https://partners.mystocks.africa/partners/onprem#request) with your organization, contact, exact domain and deployment requirements. Use the same form for a renewal request. 2. An administrator reviews the request and supplies a DNS TXT ownership challenge. Publish the exact record, then ask the administrator to verify it. 3. The administrator chooses temporary access with an exact expiry date/time or permanent, revocable access. Confirm the expiry timezone with your administrator. 4. Receive the one-time installation credential through your organization's secure delivery process. Activation email includes installation instructions, not the secret. Obtain registry access and an approved image digest separately. 5. Install the key on the approved server and confirm readiness. A license does not automatically create team membership, enroll MFA or configure your identity provider. ## Temporary versus permanent Temporary access ends at its configured expiry. Permanent access has no scheduled expiry, but an administrator can revoke it. Both remain bound to the approved domain and partner and require successful cloud verification. Application receipt and lifecycle notifications communicate progress. Email delivery is not proof that the installation is active: verify the administrative state and `/api/health` on the deployed server. ## What happens when a key expires? Protected calls are denied and the installation presents an access wall with a request link. Submit a new request and wait for approval; a renewal can be temporary or permanent. The administrator must install the replacement credential and recheck readiness. There is no offline grace period. An active stream can remain open until its bounded lifetime ends; server-sent event connections reconnect at most every five minutes to revalidate authorization. Do not describe expiry as instant termination of every existing connection. ## Replace or recover a key Use the administrator's Replace key action. Issuance revokes the old key, so coordinate the change with the server operator. Rerun the reviewed installer with the same approved domain and image digest, entering the replacement at the hidden prompt (or via an owner-only key file). The installer verifies it, replaces the environment configuration and recreates/restarts the service. A lost one-time credential must be reissued. Do not email keys, paste them into support tickets, or restore a revoked key during rollback. Keep configuration backups in a secret manager with restricted access. ## Change the installation domain A key issued for one domain does not authorize another. Ask an administrator to approve and verify the new domain, update authorized sign-in domains, DNS and TLS, and provide the appropriate replacement activation. Confirm rejection of the old configuration according to the approved migration plan. # On-prem API and identity connectivity Source: https://partners.mystocks.africa/partners/docs/onprem-connectivity [Journey overview](/partners/docs/onprem) · Next: [operations and troubleshooting](/partners/docs/onprem-operations) ## How requests reach MyStocks Your application calls your approved HTTPS installation domain. The on-prem server forwards permitted requests to MyStocks with a server-only installation credential and the caller's own API key or user token. Cloud partner identity, scopes, rate limits and permissions remain authoritative; the installation key is not a replacement for API authentication. | Purpose | Address | | --- | --- | | Cloud production API | `https://mystocks.africa/api/v1/partner` | | On-prem production proxy | `https://YOUR-DOMAIN/api/v1/partner` | | Cloud sandbox API | `https://mystocks.africa/api/sandbox/v1/partner` | | On-prem sandbox proxy | `https://YOUR-DOMAIN/api/sandbox/v1/partner` | Use the endpoint's authentication requirements from [Authentication](/partners/docs/auth) and [OpenAPI](https://mystocks.africa/openapi.json). Start with a permitted read-only request from the [Quick Start](/partners/docs/quickstart), changing only the origin to your approved installation. Keep credentials server-side; do not embed installation secrets in SDKs or browser requests. ## Sandbox boundaries Use sandbox credentials only with sandbox paths and confirm the required sub-account identifiers. An on-prem license does not enable unavailable sandbox enterprise/team/security operations or master trade/payout operations. Follow the [sandbox guide](/partners/docs/paper-demo-trading) and each endpoint's documented availability. ## Sign-in, SSO and MFA Users sign in with their real MyStocks accounts and must belong to the licensed organization. The installer obtains public Firebase client configuration through license verification; never install a Firebase administrative credential in this image. For SAML/OIDC, configure the active provider in the authoritative Firebase/Identity Platform project and partner SSO settings. Rerun the installer after changing the provider configuration. The exact provider identity is checked; a license alone does not enroll users. Membership creation through invitations or just-in-time provisioning remains subject to configured organization policies. Enrolled Firebase TOTP authenticator challenges are supported. Enroll or recover factors through your identity administrator before enforcing MFA. SMS challenge UI and authenticator enrollment are not included in this image. Verify your actual identity provider; acceptance with one test provider does not certify every vendor configuration. ## Proxy and edge behavior Preserve the approved Host header and use verified HTTPS upstream connections. Configure streaming without response buffering. The application proxy implements HTTP and server-sent events; an edge proxy accepting WebSocket upgrades does not establish a WebSocket API contract. Do not inject trusted certificate-attestation headers from clients. Any required mutual-TLS integration must use the approved trusted proxy configuration and server-side attestation secret; certificate headers alone are not authorization. Cloud SCIM uses the gateway URL returned by the cloud console. ## Acceptance checks - Readiness succeeds on the public domain and login resolves the expected organization. - A permitted read succeeds; anonymous, wrong-domain and cross-partner requests fail. - Authorized sandbox actions work within their documented restrictions, with no production money movement. - Streaming reconnects and revalidates authorization; credentials are absent from URLs and application logs. - Revocation, expiry and cloud-disconnection behavior are exercised in a dedicated test installation. # Operate and troubleshoot on-prem Source: https://partners.mystocks.africa/partners/docs/onprem-operations [Journey overview](/partners/docs/onprem) · [Installation](/partners/docs/onprem-installation) · [Key renewal](/partners/docs/onprem-activation) ## Liveness is not readiness `/api/live` checks the running process. `/api/health` performs a fresh license check and returns 503 when the license is invalid, expired or revoked, cloud verification is unavailable, or required sign-in configuration is missing. A page loading or container running does not prove the gateway is ready. Monitor the public HTTPS URL, readiness, certificate expiry and application logs. Keep secrets and authorization headers out of logs and alerts. ## Diagnose by symptom | Symptom | Check next | | --- | --- | | Registry pull fails | Separate registry reader permission, correct registry and approved immutable image digest. A license does not grant registry login. | | DNS or TLS failure | A/AAAA records, hostname, certificate chain, ports 80/443 and proxy challenge configuration. Compare IPv4 and IPv6 reachability. | | `/api/live` succeeds, `/api/health` returns 503 | License status/domain, outbound cloud access, system time and runtime sign-in configuration. Do not bypass verification. | | API returns 401 | Required caller API credential or user token; installation credential alone is insufficient. | | API returns 403 | Organization membership, scope, SSO/MFA requirements and permitted domain; inspect the response error code. | | Login rejects the domain/provider | Authorized Firebase domain and the exact organization provider ID; rerun installer after approved configuration changes. | | Sandbox operation is rejected | Published endpoint availability and required sub-account fields before assuming a platform failure. | | Events stall or repeatedly disconnect | Proxy buffering/timeouts and outbound connectivity. Reconnection within five minutes is expected. | | Access wall appears | Expiry/revocation and cloud verification; submit a renewal request when needed. | For a default installer-managed Docker deployment, inspect service status without displaying the secret environment file: ```sh sudo docker compose --project-directory /opt/mystocks \ -f /opt/mystocks/docker-compose.yml ps ``` For native deployment, inspect `systemctl status mystocks-onprem.service`. If you selected a different output directory, substitute it in operational commands. Review logs locally and redact credentials before sharing diagnostics. ## Upgrade and rollback 1. Obtain the next approved digest and confirm cloud compatibility. Back up configuration securely and record the prior approved digest. 2. Test the release in a dedicated installation, including readiness, login, permitted reads, sandbox and license lifecycle. 3. Schedule the change. Rerun the installer with the new digest, verified domain and current valid license; preserve your proxy/mode choices explicitly. 4. Recheck public TLS, readiness and authenticated functionality after restart. 5. If rollback is required, use the prior cloud-compatible image and current valid credentials. Never restore a revoked key or an obsolete image that allowed anonymous proxy access. The installer performs no production data migration. Your team owns server patches, TLS, local configuration backups and operational monitoring; agree incident responsibilities and service terms during onboarding. ## Ask for help Contact support@mystocks.africa with the installation domain, image digest, UTC timestamp, failing path, HTTP status and request ID where available. Include redacted symptoms and whether liveness/readiness pass. Never send passwords, tokens, installation keys or complete environment files.