Skip to content
Partner API Docs

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

Try in API Tester
FieldTypeRequiredDescription
urlstringYesHTTPS endpoint. Must respond 2xx within 8 s.
eventsstring[]YesArray of event type strings to subscribe to.
secretstringNoHMAC signing secret (min 16 chars). Generated automatically if omitted.
bash
curl -X POST "https://mystocks.africa/api/v1/partner/webhooks" \
-H "Authorization: Bearer pk_live_<key>" \
-H "Idempotency-Key: webhook_primary_001" \
-H "Content-Type: application/json" \
-d '{"url":"https://yourapp.com/webhooks/mystocks","events":["order.filled","order.rejected"],"secret":"my-signing-secret-min-16-chars"}'

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/{id}.
order.replacedA resting order was modified via PATCH /users/{id}/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.
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.

curl -X POST https://mystocks.africa/api/v1/partner/price-alerts \
  -H "Authorization: Bearer pk_live_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "symbol": "SCOM.KE", "exchange": "NSE", "condition": "above", "threshold": 20.00 }'
FieldTypeRequiredDescription
symbolstringNoRequired. Must exist and trade on the given exchange.
exchangestringNoRequired. Exchange code, e.g. NSE, NGX, JSE.
conditionstringNoRequired. "above" or "below".
thresholdnumberNoRequired. Positive, in the local trading currency.
repeatbooleanNoDefault false. When false 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.
clientAlertIdstringNoOptional. Your own reference, echoed back on the alert and in the webhook payload.

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.

// price.alert
{ "event": "price.alert", "timestamp": "2026-07-13T11:45:02Z", "data": { "alertId": "alr_abc123", "clientAlertId": null, "symbol": "SCOM.KE", "exchange": "NSE", "condition": "above", "threshold": 20, "price": 20.15, "currency": "KES", "repeat": false, "state": "DISARMED", "triggeredAt": "2026-07-13T11:45:02.000Z" } }

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" } }
 
// 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

GET /webhooks · DELETE /webhooks/{id} · POST /webhooks/{id}/test · GET /webhooks/{id}/deliveries

Try delivery log

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 → 8 h → 24 h (8 attempts max).

# Fire a test.event delivery immediately
curl -X POST "https://mystocks.africa/api/v1/partner/webhooks/wh_abc123/test" \
  -H "Authorization: Bearer pk_live_<key>"
 
# Inspect delivery log
curl "https://mystocks.africa/api/v1/partner/webhooks/wh_abc123/deliveries?limit=10" \
  -H "Authorization: Bearer pk_live_<key>"
{ "deliveries": [{ "id": "del_abc", "eventId": "evt_abc123", "event": "order.filled", "status": 200, "duration": 142, "attemptedAt": "2026-06-04T11:30:01Z", "success": true }, { "id": "del_def", "eventId": "evt_def456", "event": "deposit.confirmed", "status": 503, "duration": 8000, "success": false, "nextRetryAt": "2026-06-04T10:05:00Z" }], "count": 2, "hasMore": false, "nextCursor": null }

Was this page useful?

Your signal helps us tighten partner onboarding docs.

Last updated on

On this page