Skip to content
MyStocks Developers
API v1

API v1

Current stable contract

Versioning policyRelease changelog
Sandbox console
Operate

Webhooks

Register HTTPS endpoints for real-time event notifications, HMAC-SHA256-signed over the raw body. Full event catalogue, per-event payloads, signature-verification code (Node & Python), at-least-once delivery semantics, and the delivery log.

Register HTTPS endpoints to receive real-time event notifications. MyStocks signs every delivery with an HMAC-SHA256 signature over the raw request body using your webhook secret. Your endpoint must respond HTTP 2xx within 8 seconds; heavy processing should be deferred to a background queue. Failed deliveries are retried with exponential backoff — check the delivery log via GET /webhooks/{id}/deliveries.

For mobile foreground UX, use GET /stream Server-Sent Events to receive the same partner events with near-real-time delivery and resume via Last-Event-ID / ?since=. Webhooks and SSE carry order, wallet, KYC, dividend, account, and market-status events; they do not provide tick-by-tick quotes or Level-2 order book depth. Poll /market/quotes for fresh displayed prices, and use the /users/{userId}/devices endpoints to manage your own APNs/FCM/Expo/Web Push fanout.

Register a webhook

POST

/webhooks

Register webhook

Full endpoint documentation
Sandbox availablecreateWebhooks

Registers a new HTTPS webhook endpoint. MyStocks sends a POST to your URL for each subscribed event. Verification — every delivery includes an x-mystocks-signature header: HMAC-SHA256(secret, rawBody) hex-encoded. Verify it before processing. Retries — failed deliveries are retried with exponential back-off: up to 6 attempts total (immediate, then 5 s, 30 s, 5 min, 30 min, 2 h). Your endpoint must respond 2xx within 8 seconds or the delivery attempt is marked failed. Delivery semantics — at-least-once. The same event can be delivered more than once (e.g. your endpoint responded slowly and the delivery was retried), and events are not guaranteed to arrive in order. Deduplicate by event id and treat handlers as idempotent.

Parameters

This endpoint has no path or query parameters.

Headers

AuthorizationstringRequired

Bearer sk_sandbox_<your_key> for sandbox requests.

Idempotency-KeystringheaderRequired

Unique string that makes write calls safe to retry on network failure. Required for money movement, trading, KYC, subscriptions, and webhook creation. Deduplicated for 24 hours -- duplicate keys return the cached response. HTTP 409 if a concurrent request with the same key is still in progress.

Example: dep_user42_1743152580

Request body

urlstringRequired

HTTPS endpoint that receives POST requests. Must respond 2xx within 8 seconds.

Example: "https://yourapp.com/webhooks/mystocks"

eventsstring[]Required

Event types to subscribe to.

Example: ["trade.settled","deposit.confirmed","kyc.updated"]

secretstring

HMAC-SHA256 signing secret (min 16 chars). MyStocks sends x-mystocks-signature on every delivery.

Example: "my-signing-secret-min-16-chars"

Response fields

idstring

Unique webhook ID.

Example: "wh_abc123"

urlstring

Example: "https://yourapp.com/webhooks/mystocks"

eventsstring[]

Subscribed event types.

statusactive | disabled

Response codes

201Webhook registered.

Event catalogue

EventTrigger
order.pendingAn order was submitted and is awaiting live fill.
order.filledAn order was filled and executed. Shares/proceeds credited.
order.rejectedAn order was declined. Includes rejectionCode and rejectionReason.
order.cancelledAn order was cancelled via DELETE /orders/{orderId}.
order.replacedA resting order was modified via PATCH /users/{userId}/orders/{orderId} (same orderId).
trade.settledAlias for order.filled — use order.filled in new integrations.
trade.rejectedAlias for order.rejected — use order.rejected in new integrations.
deposit.confirmedA sub-account deposit was recorded.
withdraw.confirmedA sub-account withdrawal was processed.
wallet.creditedMaster wallet received a top-up from MyStocks ops.
kyc.updatedA sub-account KYC status changed. Inspect kycStatus: VERIFIED unlocks trading; REJECTED includes reason evidence and remains blocked.
account.frozenA sub-account was frozen (or unfrozen) by partner or MyStocks compliance.
account.closedA sub-account was soft-closed/offboarded via DELETE /users/{userId}.
dividend.paidA dividend was received and credited to a sub-account.
corporateaction.declaredGeneric corporate-action event for backwards compatibility.
corporateaction.splitStock split, reverse split, or bonus issue affecting holdings.
corporateaction.suspensionTrading suspension or halt affecting a listed security.
corporateaction.delistingDelisting event for a listed security.
corporateaction.rights_issueRights issue or similar subscription entitlement.
corporateaction.symbol_changedTicker/symbol migration, rename, or exchange code change.
quote.expiredA tradeable quote reached its 60-second TTL without being used.
price.alertA price alert you registered crossed its threshold. See Price alerts.
market.statusAn exchange changed phase (OPEN/CLOSED/HOLIDAY).
incident.declaredA platform incident affecting your integration was opened.
incident.resolvedA previously declared incident was closed.

Price alerts

price.alert does not fire on its own — you subscribe to the thresholds you care about. Register one with POST /price-alerts; threshold is in the instrument's local trading currency, the same basis as a quote's price.

Alerts are evaluated against the live price and only while the exchange is open, so a stale closing price can never trigger one. Because quotes are polled on a ~15-minute cycle (see Data freshness), an alert fires on the first poll after the crossing — not at the instant of the tick. Do not use price.alert as an execution trigger; use a resting LIMIT or STOP order, which is evaluated on the same cycle but actually places the trade.

List with GET /price-alerts, inspect one with GET /price-alerts/{alertId}, and remove one with DELETE /price-alerts/{alertId}. You may hold up to 200 active alerts.

POST

/price-alerts

Create a price alert

Full endpoint documentation
Production onlycreatePriceAlert

Registers a price alert. When the latest delayed price observation crosses the threshold while the exchange is open, a price.alert webhook fires and the event is streamed on GET /stream. threshold is 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 per partner. Requires a full key (pk_live_).

Parameters

This endpoint has no path or query parameters.

Headers

AuthorizationstringRequired

Bearer sk_sandbox_<your_key> for sandbox requests.

Idempotency-KeystringheaderRequired

Unique string that makes write calls safe to retry on network failure. Required for money movement, trading, KYC, subscriptions, and webhook creation. Deduplicated for 24 hours -- duplicate keys return the cached response. HTTP 409 if a concurrent request with the same key is still in progress.

Example: dep_user42_1743152580

Request body

symbolstringRequired

Example: "SCOM.KE"

exchangestringRequired

Example: "NSE"

conditionabove | belowRequired
thresholdnumberRequired

Positive, in the instrument's local trading currency.

Example: 20

repeatboolean

When false (default) the alert fires once and disarms. When true it re-arms, but only after the price crosses back through the threshold, so a price hovering at the threshold cannot spam your endpoint.

clientAlertIdstring

Your own reference, echoed back on the alert and in the webhook payload.

Response fields

alertIdstring

Example: "alr_abc123"

clientAlertIdstring

Your own reference, supplied at creation.

symbolstring

Example: "SCOM.KE"

exchangestring

Example: "NSE"

conditionabove | below
thresholdnumber

In the instrument's local trading currency.

Example: 20

currencystring

The local trading currency the threshold is denominated in.

Example: "KES"

repeatboolean
stateARMED | TRIGGERED | DISARMED

ARMED — waiting for the threshold to be crossed. TRIGGERED — a repeat alert that has fired and is waiting for the price to cross back before re-arming. DISARMED — a one-shot alert that has fired and is spent.

lastPriceLocalnumber
triggerCountinteger
createdAtstring
lastTriggeredAtstring

Response codes

201Alert created and armed.
400Invalid body, unknown symbol, or the active-alert cap was reached.
GET

/price-alerts

List price alerts

Full endpoint documentation
Production onlylistPriceAlerts

Returns the price alerts registered by this partner.

Parameters

stateARMED | TRIGGERED | DISARMEDquery

Filter by alert state.

limitintegerquery

Example: 100

Headers

AuthorizationstringRequired

Bearer sk_sandbox_<your_key> for sandbox requests.

Response fields

dataobject[]
data[].alertIdstring

Example: "alr_abc123"

data[].clientAlertIdstring

Your own reference, supplied at creation.

data[].symbolstring

Example: "SCOM.KE"

data[].exchangestring

Example: "NSE"

data[].conditionabove | below
data[].thresholdnumber

In the instrument's local trading currency.

Example: 20

data[].currencystring

The local trading currency the threshold is denominated in.

Example: "KES"

data[].repeatboolean
data[].stateARMED | TRIGGERED | DISARMED

ARMED — waiting for the threshold to be crossed. TRIGGERED — a repeat alert that has fired and is waiting for the price to cross back before re-arming. DISARMED — a one-shot alert that has fired and is spent.

data[].lastPriceLocalnumber
data[].triggerCountinteger
data[].createdAtstring
data[].lastTriggeredAtstring

Response codes

200The partner's price alerts.
GET

/price-alerts/{alertId}

Get a price alert

Full endpoint documentation
Production onlygetPriceAlert

Parameters

alertIdstringpathRequired

Example: alr_abc123

Headers

AuthorizationstringRequired

Bearer sk_sandbox_<your_key> for sandbox requests.

Response fields

alertIdstring

Example: "alr_abc123"

clientAlertIdstring

Your own reference, supplied at creation.

symbolstring

Example: "SCOM.KE"

exchangestring

Example: "NSE"

conditionabove | below
thresholdnumber

In the instrument's local trading currency.

Example: 20

currencystring

The local trading currency the threshold is denominated in.

Example: "KES"

repeatboolean
stateARMED | TRIGGERED | DISARMED

ARMED — waiting for the threshold to be crossed. TRIGGERED — a repeat alert that has fired and is waiting for the price to cross back before re-arming. DISARMED — a one-shot alert that has fired and is spent.

lastPriceLocalnumber
triggerCountinteger
createdAtstring
lastTriggeredAtstring

Response codes

200The price alert.
404Price alert not found.
DELETE

/price-alerts/{alertId}

Delete a price alert

Full endpoint documentation
Production onlydeletePriceAlert

Removes a price alert. Requires a full key (pk_live_).

Parameters

alertIdstringpathRequired

Example: alr_abc123

Headers

AuthorizationstringRequired

Bearer sk_sandbox_<your_key> for sandbox requests.

Idempotency-KeystringheaderRequired

Unique string that makes write calls safe to retry on network failure. Required for money movement, trading, KYC, subscriptions, and webhook creation. Deduplicated for 24 hours -- duplicate keys return the cached response. HTTP 409 if a concurrent request with the same key is still in progress.

Example: dep_user42_1743152580

Response fields

alertIdstring
deletedboolean
messagestring

Response codes

200Alert deleted.
404Price alert not found.

Delivery envelope

Every delivery wraps the event-specific payload in a standard three-field envelope. Sandbox deliveries add isSandbox: true at the top level.

{ "event": "order.filled", "timestamp": "2026-06-06T10:32:00Z", "data": { "...": "event-specific fields" } }

Event payloads

// order.pending
{ "event": "order.pending", "timestamp": "2026-06-06T10:00:00Z", "data": { "orderId": "ord_abc123", "subAccountId": "usr_abc123", "externalId": "user_42", "type": "BUY", "symbol": "SCOM.KE", "quantity": 1000, "totalCost": 12.67, "status": "PENDING" } }
 
// order.filled (+ alias trade.settled)
{ "event": "order.filled", "timestamp": "2026-06-06T10:32:00Z", "data": { "orderId": "ord_abc123", "subAccountId": "usr_abc123", "type": "BUY", "symbol": "SCOM.KE", "exchange": "NSE", "quantity": 1000, "feeAmount": 0.09, "status": "FILLED", "settledAt": "2026-06-06T10:32:00Z", "settlementUsdPrice": 0.01261, "totalCost": 12.70 } }
 
// order.rejected (+ alias trade.rejected) — BUY escrow refunded before this fires
{ "event": "order.rejected", "timestamp": "2026-06-06T10:05:00Z", "data": { "orderId": "ord_abc123", "subAccountId": "usr_abc123", "type": "BUY", "symbol": "SCOM.KE", "quantity": 1000, "status": "REJECTED", "rejectionCode": "LIQUIDITY_UNAVAILABLE", "rejectionReason": "Insufficient market liquidity for this order size." } }
 
// order.cancelled — BUY includes refunded; SELL has no wallet impact
{ "event": "order.cancelled", "timestamp": "2026-06-06T09:15:00Z", "data": { "orderId": "ord_abc123", "subAccountId": "usr_abc123", "type": "BUY", "symbol": "SCOM.KE", "quantity": 1000, "status": "CANCELLED", "refunded": 12.67, "currency": "USD" } }
 
// deposit.confirmed — local-currency fields appear when provided in the deposit
{ "event": "deposit.confirmed", "timestamp": "2026-06-06T08:30:00Z", "data": { "subAccountId": "usr_abc123", "externalId": "user_42", "amount": 50.0, "currency": "USD", "newBalance": 150.0, "localAmount": 6500, "localCurrency": "KES", "fxRate": 130.0 } }
 
// withdraw.confirmed
{ "event": "withdraw.confirmed", "timestamp": "2026-06-06T08:45:00Z", "data": { "subAccountId": "usr_abc123", "amount": 25.0, "currency": "USD", "newBalance": 125.0 } }
 
// wallet.credited — master wallet top-up fulfilled
{ "event": "wallet.credited", "timestamp": "2026-06-06T09:00:00Z", "data": { "amount": 500.0, "newBalance": 1500.0, "currency": "USD" } }
 
// corporateaction.split (also sent as corporateaction.declared)
{ "event": "corporateaction.split", "timestamp": "2026-06-06T09:00:00Z", "data": { "corporateActionId": "ca_abc123", "type": "SPLIT", "symbol": "SCOM.KE", "exchange": "NSE", "ratio": "2:1", "effectiveDate": "2026-07-01", "affectedSubAccounts": [{ "subAccountId": "usr_abc123", "externalId": "user_42", "units": 1000 }] } }
 
// kyc.updated — overriddenByAdmin: true appears only on admin overrides
{ "event": "kyc.updated", "timestamp": "2026-06-06T11:00:00Z", "data": { "subAccountId": "usr_abc123", "externalId": "user_42", "kycStatus": "VERIFIED", "kycLevel": "FULL", "reference": "sumsub_ref_abc123" } }
 
// kyc.updated rejection — gate the trading UI and show partner-approved remediation text
{ "event": "kyc.updated", "timestamp": "2026-06-06T11:05:00Z", "data": { "subAccountId": "usr_abc123", "externalId": "user_42", "kycStatus": "REJECTED", "kycLevel": "NONE", "reasonCode": "DOCUMENT_EXPIRED", "reason": "Submit a current government-issued identity document.", "reviewedAt": "2026-06-06T11:05:00Z" } }
 
// account.frozen — frozen boolean distinguishes freeze/unfreeze
{ "event": "account.frozen", "timestamp": "2026-06-06T12:00:00Z", "data": { "subAccountId": "usr_abc123", "frozen": true, "frozenBy": "platform_admin" } }
 
// account.closed
{ "event": "account.closed", "timestamp": "2026-06-06T12:05:00Z", "data": { "subAccountId": "usr_abc123", "externalId": "user_42", "status": "closed", "residualTransferUsd": 0, "currency": "USD" } }
 
// dividend.paid — one per distribution batch, grouped by symbol
{ "event": "dividend.paid", "timestamp": "2026-06-06T09:00:00Z", "data": { "symbol": "SCOM.KE", "dividendPerShare": 0.00065, "currency": "KES", "totalUsdPaid": 0.65, "distributions": [{ "subAccountId": "usr_abc123", "units": 1000, "usdYield": 0.50 }] } }
 
// incident.declared — severity P0 (critical) → P3 (minor)
{ "event": "incident.declared", "timestamp": "2026-06-06T14:00:00Z", "data": { "id": "inc_abc123", "title": "NGX market data delay", "severity": "P2", "affectedServices": ["market-data"], "status": "investigating" } }
 
// incident.resolved — duration is a human-readable string
{ "event": "incident.resolved", "timestamp": "2026-06-06T16:30:00Z", "data": { "id": "inc_abc123", "severity": "P2", "status": "resolved", "resolvedAt": "2026-06-06T16:30:00Z", "duration": "2h 30m" } }

Signature verification

Every delivery includes an x-mystocks-signature header containing an HMAC-SHA256 hex digest of the raw request body signed with your webhook secret. Always verify using a constant-time comparison.

const crypto = require('crypto');
 
app.post('/webhooks/mystocks', express.raw({ type: '*/*' }), (req, res) => {
  const sig = req.headers['x-mystocks-signature'].replace(/^sha256=/i, '');
  const expected = crypto
    .createHmac('sha256', process.env.MYSTOCKS_WEBHOOK_SECRET)
    .update(req.body) // raw Buffer — do NOT parse JSON first
    .digest('hex');
 
  const verified = crypto.timingSafeEqual(
    Buffer.from(sig, 'utf8'),
    Buffer.from(expected, 'utf8'),
  );
  if (!verified) return res.status(401).send('Invalid signature');
 
  const event = JSON.parse(req.body);
  res.status(200).send('OK');
});

Delivery semantics — at-least-once, not exactly-once. The same event can arrive more than once; events are not guaranteed to arrive in order (a retried order.pending can land after order.filled); and a delivery can fail all retries. The robust pattern: dedupe by eventId, treat handlers as idempotent, and derive state from the event payload rather than arrival order. Respond 2xx quickly and process asynchronously — you have 8 seconds before the attempt fails.

Manage & inspect

List registered webhooks, delete one, fire a test event, or inspect delivery history. POST /webhooks/{id}/test sends a test.event payload immediately and logs the delivery — useful for verifying reachability and signature verification before going live. /deliveries returns each attempt with HTTP status, response snippet, duration (ms), and the retry schedule if it failed. Retry schedule: immediate → 5 s → 30 s → 5 min → 30 min → 2 h (6 attempts max).

GET

/webhooks

List webhooks

Full endpoint documentation
Sandbox availablelistWebhooks

Returns all registered webhook endpoints for your partner account.

Parameters

This endpoint has no path or query parameters.

Headers

AuthorizationstringRequired

Bearer sk_sandbox_<your_key> for sandbox requests.

Response fields

webhooksobject[]
webhooks[].idstring

Unique webhook ID.

Example: "wh_abc123"

webhooks[].urlstring

Example: "https://yourapp.com/webhooks/mystocks"

webhooks[].eventsstring[]

Subscribed event types.

webhooks[].statusactive | disabled

Response codes

200Webhook list.
DELETE

/webhooks/{id}

Delete webhook

Full endpoint documentation
Sandbox availabledeleteWebhooks

Permanently removes a webhook registration. In-flight deliveries will still complete.

Parameters

idstringpathRequired

Example: wh_abc123

Headers

AuthorizationstringRequired

Bearer sk_sandbox_<your_key> for sandbox requests.

Idempotency-KeystringheaderRequired

Unique string that makes write calls safe to retry on network failure. Required for money movement, trading, KYC, subscriptions, and webhook creation. Deduplicated for 24 hours -- duplicate keys return the cached response. HTTP 409 if a concurrent request with the same key is still in progress.

Example: dep_user42_1743152580

Response fields

messagestring

Example: "Webhook deleted."

Response codes

200Webhook deleted.
404Webhook not found.
POST

/webhooks/{id}/test

Test webhook

Full endpoint documentation
Sandbox availablecreateTest

Sends a signed synthetic test.event to the registered webhook URL. Use this to verify connectivity and HMAC signature verification before going live. This operation does not simulate business events such as order fills.

Parameters

idstringpathRequired

Example: wh_abc123

Headers

AuthorizationstringRequired

Bearer sk_sandbox_<your_key> for sandbox requests.

Idempotency-KeystringheaderRequired

Unique string that makes write calls safe to retry on network failure. Required for money movement, trading, KYC, subscriptions, and webhook creation. Deduplicated for 24 hours -- duplicate keys return the cached response. HTTP 409 if a concurrent request with the same key is still in progress.

Example: dep_user42_1743152580

Response fields

successboolean
eventIdstring
messagestring
webhookIdstring
urlstring
httpStatusinteger | null

HTTP status your endpoint returned.

Response codes

200Test delivery result.
502The registered endpoint could not be reached or returned a non-2xx response.
GET

/webhooks/{id}/deliveries

Webhook deliveries

Full endpoint documentation
Sandbox availablelistDeliveries

Returns the delivery log for a webhook endpoint — each attempt, response code, and body.

Parameters

idstringpathRequired

Example: wh_abc123

limitintegerquery

Example: 50

Headers

AuthorizationstringRequired

Bearer sk_sandbox_<your_key> for sandbox requests.

Response fields

deliveriesobject[]
deliveries[].deliveryIdstring
deliveries[].eventstring
deliveries[].statusCodeinteger | null
deliveries[].successboolean
deliveries[].latencyMsinteger | null
deliveries[].attemptsinteger
deliveries[].deliveredAtstring

Response codes

200Delivery log.

Was this page useful?

Your signal helps us tighten partner onboarding docs.

Send note

Last updated on

On this page