# Trading

> Place BUY and SELL orders for your master account or any sub-account. MARKET orders use quoteId; LIMIT/STOP orders rest without quotes. Covers order types, time-in-force, history, cancellation, ledger, and portfolio.

Place BUY and SELL orders on behalf of your master account or any sub-account. **MARKET** orders use a
two-step quote -> trade flow in both sandbox and production: fetch a quote, then place the order with
that `quoteId` within 60 seconds. Resting `LIMIT`, `STOP`, and `STOP_LIMIT` orders are placed without
a quoteId because your request supplies the trigger price; they rest as `WORKING` until activated.
Sandbox market orders fill instantly; live production market orders submitted during exchange
hours target `PENDING → FILLED / REJECTED` within 5 minutes. v1 may emit legacy `COMPLETED` as an alias of `FILLED`. Always use `Idempotency-Key` on trade
calls.

Production and sandbox enforce the same single-use quote rule. Sandbox claims the quote atomically
only after funds or holdings validation, so an `INSUFFICIENT_FUNDS` rejection does not consume an
otherwise valid quote. Reusing a successfully claimed quote returns `409 STALE_QUOTE`.

1\. Quote: GET /quote/{symbol}

2\. PENDING: POST /trade — escrowed

3\. FILLED: order.filled

The full state machine — including LIMIT/STOP, cancellation, and expiry — is in
[Order & Money Lifecycles](/partners/docs/lifecycles).

## Get a quote

`GET` `/quote/{symbol}`

[Try endpoint: getQuote](https://partners.mystocks.africa/partners/docs/api-tester?endpoint=getQuote)

**Step 1 of every MARKET trade (sandbox and production).** Returns the fee breakdown -- gross value, base fee
(0.75%), optional partner markup, total cost (BUY) or estimated proceeds (SELL), sufficient-funds check
\-- and a `quoteId` that the subsequent MARKET `/trade` call **requires**. Resting LIMIT/STOP orders skip
this endpoint. Quotes are single-use and expire after **60 seconds** (`quoteExpiresAt`); a stale or reused quoteId returns `409 STALE_QUOTE`, and a
quoteId whose symbol/side/quantity does not match the order returns `409 QUOTE_ORDER_MISMATCH`. Quotes
are indicative, not binding: settlement executes at the market price, protected by a deviation band
(default +/-10% of the quoted price).

type (string, required): BUY or SELL.

quantity (number, optional): Number of whole shares (positive integer) — the order placed against this quote must match it. For fractional investing use cashValue instead. Mutually exclusive with cashValue.

cashValue (number, optional): USD amount for cash-mode (fractional) investing. Mutually exclusive with quantity. The trade must use the SAME cashValue.

subAccountId (string, optional): Sub-account ID. Omit to quote against the master wallet. The quoteId is bound to this account context, symbol, side, and sizing mode.

```sh
curl "https://mystocks.africa/api/v1/partner/quote/SCOM.KE?type=BUY&quantity=500&subAccountId=usr_abc123" -H "Authorization: Bearer pk_live_<key>"
```

```javascript
const res = await fetch(
"https://mystocks.africa/api/v1/partner/quote/SCOM.KE?type=BUY&quantity=500&subAccountId=usr_abc123",
{ headers: { Authorization: "Bearer pk_live_<key>" } }
);
const quote = await res.json();
```

```python
import requests

res = requests.get(
  "https://mystocks.africa/api/v1/partner/quote/SCOM.KE",
  params={"type": "BUY", "quantity": 500, "subAccountId": "usr_abc123"},
  headers={"Authorization": "Bearer pk_live_<key>"},
)
quote = res.json()
```

```sdk
const quote = await client.trading.getQuote("SCOM.KE", {
type: "BUY",
quantity: 500,
subAccountId: "usr_abc123",
});
```

```json
{ "quoteId": "qt_9f2c81d4b7a3", "quoteExpiresAt": "2026-07-07T09:46:00Z", "quoteTtlSeconds": 60, "symbol": "SCOM.KE", "exchange": "NSE", "currency": "KES", "type": "BUY", "quantity": 500, "localPrice": 16.5, "usdPrice": 0.127306, "gross": 63.65, "baseFee": 0.48, "partnerMarkupFee": 0.0, "fee": 0.48, "totalCost": 64.13, "sufficientFunds": true, "feeRate": 0.75, "note": "Pass quoteId to POST /trade within 60 seconds. No order has been placed." }
```

## Mobile quote refresh

The 60-second TTL protects quote-to-fill price integrity and is not extended for backgrounded mobile
sessions. Treat `quoteExpiresAt` as the source of truth: display a countdown, refresh automatically when
10 seconds remain, and request a new quote whenever the app resumes. Never submit an expired `quoteId`.
Refreshing a quote does not create an order and is safe without an idempotency key; only the subsequent
trade mutation requires one. Preserve the user's symbol, side, and sizing mode, but require confirmation
again if the refreshed total cost, proceeds, fee, or quantity changes.

## Place a trade

`POST` `/trade` *(master)* · `POST` `/users/{userId}/trade` *(sub-account)*

[Try endpoint: createUserTrade](https://partners.mystocks.africa/partners/docs/api-tester?endpoint=createUserTrade)

Place a BUY or SELL order. BUY escrows the cost (gross + fee) from the wallet immediately; SELL checks
that the sub-account holds the required units. The symbol accepts exchange-qualified (`SCOM.KE`) or
bare ticker (`SCOM`).

symbol (string, required): Exchange-qualified ticker or unambiguous bare ticker.

quantity (number, optional): Share-sized order quantity. Use either quantity or cashValue, never both. Whole-share markets require integers; fractional-enabled markets allow decimals.

cashValue (number, optional): USD notional for cash-mode fractional investing. Use either cashValue or quantity, never both. MARKET trades must match the quoted cashValue.

quoteId (string, optional): Required only for MARKET orders, including when orderType is omitted. Must come from GET /quote/{symbol}; single-use, expires after 60s. Do not send for LIMIT/STOP/STOP\_LIMIT.

orderType (string, optional): MARKET (default) | LIMIT | STOP | STOP\_LIMIT. Resting order types do not use quoteId and return status WORKING.

limitPrice (number, optional): Local-currency limit price. Required for LIMIT and STOP\_LIMIT.

stopPrice (number, optional): Local-currency trigger price. Required for STOP and STOP\_LIMIT.

clientOrderId (string, optional): Your internal order reference (<=80 chars). Unique per partner -- a safe retry/dedupe handle alongside Idempotency-Key.

timeInForce (string, optional): GTC (default), DAY (auto-cancelled after 24h), or GTD (auto-cancelled at expiresAt). Expiry refunds BUY escrow / releases SELL reservations and fires order.cancelled. IOC is not supported.

expiresAt (string, optional): ISO-8601. Required when timeInForce is GTD.

stopLoss (number, optional): Auto-sell price floor (USD). Optional.

takeProfit (number, optional): Auto-sell price ceiling (USD). Optional.

takeProfit (number, optional): Auto-sell price ceiling (USD). Optional.

```sh
# Step 1 - quote (quoteId expires in 60s)
curl "https://mystocks.africa/api/v1/partner/quote/SCOM.KE?type=BUY&quantity=1000&subAccountId=usr_abc123" -H "Authorization: Bearer pk_live_<key>"

# Step 2 - place the order with that quoteId
curl -X POST "https://mystocks.africa/api/v1/partner/users/usr_abc123/trade" -H "Authorization: Bearer pk_live_<key>" -H "Idempotency-Key: trade_user42_001" -H "Content-Type: application/json" -d '{"symbol":"SCOM.KE","type":"BUY","quantity":1000,"quoteId":"qt_9f2c81d4b7a3","clientOrderId":"my-ord-10001"}'
```

```javascript
const order = await fetch(
"https://mystocks.africa/api/v1/partner/users/usr_abc123/trade",
{
  method: "POST",
  headers: {
    Authorization: "Bearer pk_live_<key>",
    "Idempotency-Key": "trade_user42_001",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    symbol: "SCOM.KE",
    type: "BUY",
    quantity: 1000,
    quoteId: "qt_9f2c81d4b7a3",
    clientOrderId: "my-ord-10001",
  }),
},
).then((res) => res.json());
```

```python
import requests

order = requests.post(
  "https://mystocks.africa/api/v1/partner/users/usr_abc123/trade",
  headers={
      "Authorization": "Bearer pk_live_<key>",
      "Idempotency-Key": "trade_user42_001",
      "Content-Type": "application/json",
  },
  json={
      "symbol": "SCOM.KE",
      "type": "BUY",
      "quantity": 1000,
      "quoteId": "qt_9f2c81d4b7a3",
      "clientOrderId": "my-ord-10001",
  },
).json()
```

```sdk
const order = await client.subAccounts.trade(
"usr_abc123",
{
  symbol: "SCOM.KE",
  type: "BUY",
  quantity: 1000,
  quoteId: "qt_9f2c81d4b7a3",
  clientOrderId: "my-ord-10001",
},
{ idempotencyKey: "trade_user42_001" },
);
```

```json
{ "status": "PENDING", "orderId": "ord_abc123", "quoteId": "qt_9f2c81d4b7a3", "clientOrderId": "my-ord-10001", "timeInForce": "GTC", "subAccountId": "usr_abc123", "type": "BUY", "symbol": "SCOM.KE", "quantity": 1000, "priceAtOrder": 16.5, "usdPriceAtOrder": 0.0126, "gross": 12.6, "fee": 0.09, "totalCost": 12.69, "note": "Order is pending. Funds reserved from sub-account wallet." }
```

Production returns `PENDING` with the reserved price as `priceAtOrder` (local) and `usdPriceAtOrder`.
In **sandbox** the same call resolves instantly to `FILLED` and instead carries `usdPrice`,
`localPrice`, `currency`, and the post-trade `newSubBalance`.

> **Sandbox cheat codes.** Quantity `100` → instant auto-fill (`FILLED`). Quantity `999` → instant
> auto-reject (`REJECTED`). Use them to test your webhook handlers without waiting for the admin queue.

## Production execution behavior

Production orders are operationally constrained by the exchange, the broker, and the available market
data. Treat these guarantees as part of your client-state model:

partial fills (behavior, optional): Orders may fill partially. Use order status plus execution reports to show filledQuantity, remainingQuantity, and averageFillPrice when present.

average price (field, optional): averageFillPrice is the weighted average of fills, not necessarily the quote price or the last market price.

slippage (guardrail, optional): MARKET orders execute at available market prices inside the configured deviation band (default +/-10%). Orders outside that band are rejected or left pending for manual handling.

halts and auctions (market state, optional): If an exchange is halted, in auction, closed, or missing a valid market price, MARKET orders are rejected or held pending; resting orders remain WORKING until normal trading resumes and their trigger is crossed.

order book depth (coverage, optional): The Partner API does not contractually expose Level-2 depth. Use quoted/last prices for retail UX, and design advanced depth views only after a market-data vendor agreement is in place.

## List & fetch orders

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

[Try endpoint: listOrders](https://partners.mystocks.africa/partners/docs/api-tester?endpoint=listOrders)

List orders for the master account or a specific sub-account (cursor-paginated), or fetch a single
order by ID to poll status. Order fields include `status`, `rejectionCode`, `rejectionReason`, `fee`,
`baseFee`, `partnerMarkupFee`, and timestamps. Filter by `?status=PENDING`, `?symbol=`, or
`?from=`/`?to=` date range.

For sub-accounts, `?symbol=SCOM.KE` is an exact, case-insensitive filter and can be combined with
`status` in production or sandbox without requiring a database composite index.

```json
{ "orders": [{ "id": "ord_abc123", "symbol": "SCOM.KE", "type": "BUY", "status": "FILLED", "quantity": 1000, "priceAtOrder": 16.5, "totalAmount": 12.69, "feeAmount": 0.09, "currency": "USD", "rejectionCode": null, "settledAt": "2026-06-04T11:30:00Z", "createdAt": "2026-06-04T09:45:00Z" }], "count": 1, "hasMore": false, "nextCursor": null }
```

## Cancel an order

`DELETE` `/orders/{orderId}` · `DELETE` `/users/{userId}/orders/{orderId}`

[Try endpoint: deleteOrders](https://partners.mystocks.africa/partners/docs/api-tester?endpoint=deleteOrders)

Cancel an order while it is still `PENDING` (market orders awaiting fill) or `WORKING` (resting limit/stop
orders). BUY escrow is refunded atomically. Returns HTTP 409 once the order has reached a terminal state
(`FILLED`, `CANCELLED`, `EXPIRED`, or `REJECTED`). A successful cancellation fires an `order.cancelled` webhook.

Sandbox uses the same cancellation behavior for active resting/simulator orders, including BUY cash
refunds and SELL unit-reservation release.

## Transaction ledger

`GET` `/users/{userId}/transactions`

Full wallet ledger for a sub-account — every credit and debit in chronological order: deposits,
withdrawals, trade escrows (`INVEST`), trade proceeds (`SELL`), dividend distributions (`DISTRIBUTION`),
fund redemptions (`REDEEM`), and fees. Cursor-paginated.

`settledAt` is populated for completed transactions. For a legacy completed row created before that
field existed, the API returns its immutable completion/creation timestamp as compatibility evidence.

type (string, optional): DEPOSIT | WITHDRAWAL | INVEST | SELL | DISTRIBUTION | REDEEM | FEE | TRANSFER\_IN | TRANSFER\_OUT

from (date, optional): On or after this date (YYYY-MM-DD, UTC).

to (date, optional): On or before this date (YYYY-MM-DD, UTC).

cursor (string, optional): Opaque cursor from nextCursor. Omit for first page.

limit (integer, optional): Page size, default 50, max 200.

```json
{ "transactions": [{ "id": "txn_abc123", "type": "DEPOSIT", "status": "COMPLETED", "direction": "CREDIT", "amount": 500, "currency": "USD", "reference": "dep_riven_user_42_1743152580", "createdAt": "2026-06-01T10:00:00Z" }, { "id": "txn_xyz789", "type": "INVEST", "status": "PENDING", "direction": "DEBIT", "amount": 64.13, "currency": "USD", "description": "BUY SCOM.KE x500", "reference": "ord_def456" }], "count": 2, "hasMore": false, "nextCursor": null }
```

## Portfolio

`GET` `/portfolio` *(master)* · `GET` `/users/{userId}/portfolio` *(sub-account)*

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

```json
{ "walletBalance": 487.31, "totalValue": 512.31, "holdings": [{ "symbol": "SCOM.KE", "name": "Safaricom PLC", "quantity": 1000, "avgCostUsd": 0.01269, "currentPriceUsd": 0.0126, "marketValue": 12.6, "unrealizedPnl": -0.09, "currency": "KES" }] }
```
