# Quick Start

> Go from zero to your first executed trade — register, create a sub-account, deposit, quote, and trade — all against the MyStocks sandbox with no approval required.

Go from zero to your first executed trade — all against the sandbox, no approval required.

<QuickstartChecklist items={[
  { title: 'Register', detail: 'Get a sandbox key from the partner portal session.', endpoint: 'POST /register' },
  { title: 'Create user', detail: 'Create an isolated wallet for your end-user.', endpoint: 'POST /users' },
  { title: 'Deposit', detail: 'Credit the user wallet with partner-managed FX details.', endpoint: 'POST /users/{userId}/deposit' },
  { title: 'Optional: assert KYC', detail: 'Exercise the production-style KYC assertion flow; sandbox trading does not require it.', endpoint: 'POST /users/{userId}/kyc' },
  { title: 'Quote', detail: 'Fetch a single-use quoteId that expires after 60 seconds.', endpoint: 'GET /quote/{symbol}' },
  { title: 'Trade', detail: 'Submit the order with the matching quoteId and idempotency key.', endpoint: 'POST /users/{userId}/trade' },
]} />

<Steps>

<Step>

### Register and get your API key

<TryEndpoint id="createRegister" />

Sign in (or sign up) on the [partner portal](/partners/sandbox) first — `/register` authenticates
with your **Firebase ID token** from that session, not an API key. The easiest path is the portal's
"Create Key" or "Rotate Key" button, which calls this endpoint for you. Rotation is explicit because
calling it again invalidates the previous sandbox key.

```bash
curl -X POST https://mystocks.africa/api/sandbox/v1/register \
  -H "Authorization: Bearer <firebase-id-token>"
```

```json
{ "apiKey": "sk_sandbox_xxxxxxxxxxxxxxxx", "walletBalance": 100000, "currency": "USD" }
```

</Step>

<Step>

### Create a sub-account for your end-user

<TryEndpoint id="createUsers" />

Each of your end-users gets an isolated wallet. Alternatively use
`POST /api/sandbox/v1/partner/auto-register` (idempotent — safe to call on every login).

<CodeTabs
  curl={`curl -X POST https://mystocks.africa/api/sandbox/v1/partner/users \\
  -H "Authorization: Bearer sk_sandbox_<your_key>" \\
  -H "Content-Type: application/json" \\
  -d '{ "externalId": "user_42", "displayName": "Jane Doe", "email": "jane@example.com" }'`}
  node={`const user = await fetch("https://mystocks.africa/api/sandbox/v1/partner/users", {
  method: "POST",
  headers: { Authorization: "Bearer sk_sandbox_<your_key>", "Content-Type": "application/json" },
  body: JSON.stringify({ externalId: "user_42", displayName: "Jane Doe", email: "jane@example.com" }),
}).then((res) => res.json());`}
  python={`user = requests.post(
    "https://mystocks.africa/api/sandbox/v1/partner/users",
    headers={"Authorization": "Bearer sk_sandbox_<your_key>"},
    json={"externalId": "user_42", "displayName": "Jane Doe", "email": "jane@example.com"},
).json()`}
  sdk={`const user = await client.subAccounts.create({
  externalId: "user_42",
  displayName: "Jane Doe",
  email: "jane@example.com",
});`}
/>

```json
{
  "subAccountId": "usr_xxxxxxxxxxxx",
  "externalId": "user_42",
  "displayName": "Jane Doe",
  "kycStatus": "NONE",
  "status": "active",
  "wallet": { "currency": "USD", "balance": 0 }
}
```

</Step>

<Step>

### Deposit funds

<TryEndpoint id="createDeposit" />

After collecting your user's local-currency payment, send its local amount and currency. MyStocks
uses managed FX to calculate the USD ledger credit and returns the conversion metadata.

```bash
curl -X POST https://mystocks.africa/api/sandbox/v1/partner/users/usr_xxxxxxxxxxxx/deposit \
  -H "Authorization: Bearer sk_sandbox_<your_key>" \
  -H "Idempotency-Key: dep_user42_001" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 65000, "currency": "KES", "note": "Mpesa ref mpesa_QHJ29SK" }'
```

```json
{ "message": "Deposit successful.", "subAccountId": "usr_xxxxxxxxxxxx", "amount": 500, "currency": "USD", "fxRate": 130, "fxSource": "MYSTOCKS_MANAGED_FX", "newSubBalance": 500, "newMasterBalance": 99500 }
```

</Step>

<Step>

### Optional: assert KYC

<TryEndpoint id="createKyc" />

You run KYC in your own onboarding flow, then assert the result. The sandbox permits synthetic
customers to trade without KYC so a new integration can reach its first fill immediately. Production
still requires `VERIFIED`; an unverified production sub-account receives `403 KYC_REQUIRED`. Use this
optional sandbox step, or the `KYC_REQUIRED` simulation scenario in the API Tester, to exercise your
production error and recovery UX.

```bash
curl -X POST https://mystocks.africa/api/sandbox/v1/partner/users/usr_xxxxxxxxxxxx/kyc \
  -H "Authorization: Bearer sk_sandbox_<your_key>" \
  -H "Idempotency-Key: kyc_user42_001" \
  -H "Content-Type: application/json" \
  -d '{ "status": "VERIFIED", "level": "BASIC", "reference": "kyc_provider_ref_123" }'
```

```json
{ "message": "KYC status updated.", "subAccountId": "usr_xxxxxxxxxxxx", "kycStatus": "VERIFIED", "kycLevel": "BASIC" }
```

</Step>

<Step>

### Get a quote (returns the required quoteId)

<TryEndpoint id="getQuote" />

Every trade — sandbox and production — starts with a quote. It returns the latest delayed price observation, the full fee
breakdown, and a single-use `quoteId` that expires after 60 seconds.

```bash
curl "https://mystocks.africa/api/sandbox/v1/partner/quote/SCOM.KE?type=BUY&quantity=1000&subAccountId=usr_xxxxxxxxxxxx" \
  -H "Authorization: Bearer sk_sandbox_<your_key>"
```

```json
{
  "quoteId": "qt_9f2c81d4b7a3",
  "quoteExpiresAt": "2026-07-07T09:46:00Z",
  "quoteTtlSeconds": 60,
  "symbol": "SCOM.KE",
  "name": "Safaricom PLC",
  "exchange": "NSE",
  "currency": "KES",
  "type": "BUY",
  "quantity": 1000,
  "localPrice": 16.5,
  "usdPrice": 0.0126,
  "gross": 12.6,
  "fee": 0.09,
  "totalCost": 12.69,
  "sufficientFunds": true
}
```

</Step>

<Step>

### Place a trade on behalf of the sub-account

<TryEndpoint id="createUserTrade" />

Pass the `quoteId` from the previous step within 60 seconds. You can use the full exchange-qualified
symbol (`SCOM.KE`) or just the bare ticker (`SCOM`) — the API resolves it automatically.

<CodeTabs
  curl={`curl -X POST https://mystocks.africa/api/sandbox/v1/partner/users/usr_xxxxxxxxxxxx/trade \\
  -H "Authorization: Bearer sk_sandbox_<your_key>" \\
  -H "Idempotency-Key: trade_user42_001" \\
  -H "Content-Type: application/json" \\
  -d '{ "symbol": "SCOM.KE", "type": "BUY", "quantity": 1000, "quoteId": "qt_9f2c81d4b7a3" }'`}
  node={`const order = await fetch("https://mystocks.africa/api/sandbox/v1/partner/users/usr_xxxxxxxxxxxx/trade", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_sandbox_<your_key>",
    "Idempotency-Key": "trade_user42_001",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ symbol: "SCOM.KE", type: "BUY", quantity: 1000, quoteId: "qt_9f2c81d4b7a3" }),
}).then((res) => res.json());`}
  python={`order = requests.post(
    "https://mystocks.africa/api/sandbox/v1/partner/users/usr_xxxxxxxxxxxx/trade",
    headers={"Authorization": "Bearer sk_sandbox_<your_key>", "Idempotency-Key": "trade_user42_001"},
    json={"symbol": "SCOM.KE", "type": "BUY", "quantity": 1000, "quoteId": "qt_9f2c81d4b7a3"},
).json()`}
  sdk={`const order = await client.subAccounts.trade(
  "usr_xxxxxxxxxxxx",
  { symbol: "SCOM.KE", type: "BUY", quantity: 1000, quoteId: "qt_9f2c81d4b7a3" },
  { idempotencyKey: "trade_user42_001" },
);`}
/>

```json
{
  "status": "FILLED",
  "orderId": "ord_xxxxxxxxxxxx",
  "subAccountId": "usr_xxxxxxxxxxxx",
  "type": "BUY",
  "symbol": "SCOM.KE",
  "quantity": 1000,
  "totalCost": 12.69,
  "newSubBalance": 487.31,
  "note": "Order filled instantly in sandbox. Live production trades are PENDING until filled or rejected."
}
```

</Step>

</Steps>

## Check order history (or use webhooks)

```bash
curl "https://mystocks.africa/api/sandbox/v1/partner/users/usr_xxxxxxxxxxxx/orders?limit=1" \
  -H "Authorization: Bearer sk_sandbox_<your_key>"
```

In sandbox, sub-account trades settle instantly (`FILLED`) with no admin queue. In production,
status progresses `PENDING → FILLED` (or `REJECTED`) through the execution flow during exchange hours. v1 may emit legacy `COMPLETED` as an alias of `FILLED`.

## Settlement timeline — production

| Phase | Timeline | Notes |
| --- | --- | --- |
| MyStocks live fill target | Within 5 minutes | Market orders submitted during exchange hours. Outside hours → queued for next session. |
| Exchange settlement (most exchanges) | T+3 business days | NSE, NGX, JSE, GSE, BRVM, ZSE, BSE, LUSE, DSE, USE, MSE, CSE, SEM — title transfers 3 business days after execution. |
| Exchange settlement (EGX) | T+2 business days | Egyptian Exchange — title transfers 2 business days after execution. |

The **MyStocks execution target** is when your `PENDING` order becomes `FILLED` — this triggers the
`order.filled` webhook and credits shares/proceeds to the wallet. The **exchange settlement cycle**
(T+2/T+3) governs when the underlying title and custody transfer through the exchange depository; it
does not affect wallet balances in the API. Full per-exchange data is at `GET /api/v1/partner/market/settlement`.

## Order lifecycle

<FlowDiagram
  direction="horizontal"
  nodes={[
    { label: 'POST /trade', sublabel: 'submit order', tone: 'accent' },
    { label: 'PENDING', sublabel: 'funds escrowed' },
    { label: 'FILLED', sublabel: 'order.filled', tone: 'success' },
  ]}
  edgeLabels={['', 'internal book fills automatically · target 5 min']}
/>

A `PENDING` order can instead terminate as `REJECTED` (BUY escrow refunded · `order.rejected`) or,
while still `PENDING`, be `CANCELLED` by your `DELETE` (`order.cancelled`).

<Callout type="info">
  **Sandbox only:** orders skip the escrow wait and resolve immediately as `FILLED`. A live order can be
  cancelled while it is still `PENDING` (or `WORKING`, for resting limit/stop orders) — once it reaches a
  terminal state (`FILLED`, `REJECTED`, `CANCELLED`, or `EXPIRED`) the DELETE returns HTTP 409.
</Callout>

### Cancelling a PENDING order

While an order is still `PENDING`, cancel it with a DELETE. For BUY orders the escrowed funds are
refunded atomically; SELL orders carry no wallet impact so cancellation is immediate. Once an order
has filled (`FILLED`) or otherwise reached a terminal state (`REJECTED`, `CANCELLED`, `EXPIRED`) it can no
longer be cancelled (HTTP 409). A
successful cancellation fires an `order.cancelled` webhook so downstream systems react without polling.

```bash
# Cancel a sub-account order
curl -X DELETE "https://mystocks.africa/api/v1/partner/users/usr_xxxxxxxxxxxx/orders/ord_xxxxxxxxxxxx" \
  -H "Authorization: Bearer pk_live_<your_key>" \
  -H "Idempotency-Key: cancel-subaccount-order-ord_xxxxxxxxxxxx"

# Cancel a master-account order
curl -X DELETE "https://mystocks.africa/api/v1/partner/orders/ord_xxxxxxxxxxxx" \
  -H "Authorization: Bearer pk_live_<your_key>" \
  -H "Idempotency-Key: cancel-master-order-ord_xxxxxxxxxxxx"
```

### Modifying a resting order (replace)

A resting `LIMIT`/`STOP`/`STOP_LIMIT` order sits in status `WORKING` until its price triggers. While
`WORKING` you can modify its `limitPrice`, `stopPrice`, and/or `quantity` with a `PATCH` — the order
keeps the **same `orderId`**, and the BUY escrow or SELL unit reservation is adjusted by the delta
atomically. Increasing a BUY escrow beyond the available balance returns `400 INSUFFICIENT_FUNDS`; the
`replaceable` field on the order tells you when a modify is allowed. Market orders cannot be modified —
cancel and re-submit instead. Requires an `Idempotency-Key`.

```bash
# Re-price and re-size a WORKING resting order (same orderId)
curl -X PATCH "https://mystocks.africa/api/v1/partner/users/usr_xxxxxxxxxxxx/orders/ord_xxxxxxxxxxxx" \
  -H "Authorization: Bearer pk_live_<your_key>" \
  -H "Idempotency-Key: replace_ord_xxxxxxxxxxxx_1" \
  -H "Content-Type: application/json" \
  -d '{ "limitPrice": 21.5, "quantity": 150 }'
```

A successful modify fires an `order.replaced` webhook and writes a `REPLACE` execution report.

<Callout type="info">
  **Skip polling — use webhooks.** Register a webhook URL to receive `order.filled`, `order.rejected`,
  and `order.cancelled` events in real time. See [Webhooks](/partners/docs/webhooks), and the
  [Going Live](/partners/docs/going-live) checklist when you're ready to switch to production.
</Callout>
