# MyStocks Africa Partner API — Full Reference > MyStocks Africa provides African investment infrastructure via REST API. This document is the authoritative machine-readable reference covering every endpoint, request/response shape, authentication model, FX model, rate limits, webhook payloads, and workflow needed to integrate the MyStocks Partner API. **Production base URL**: `https://mystocks.africa/api/v1/partner` **Sandbox base URL**: `https://mystocks.africa/api/sandbox/v1/partner` (exception: POST /register and POST /reset live at `/api/sandbox/v1` without the `/partner` segment) **Authentication**: `Authorization: Bearer pk_live_` (production) / `Authorization: Bearer sk_sandbox_` (sandbox) **Content-Type**: `application/json` for all POST/PATCH bodies **Docs**: https://mystocks.africa/partners/docs **Apply**: https://mystocks.africa/partners --- ## 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://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.