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.

Register a webhook

POST /webhooks

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.
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","deposit.confirmed","kyc.updated"],
    "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.
dividend.paidA dividend was received and credited to a sub-account.
quote.expiredA tradeable quote reached its 60-second TTL without being used.
market.statusAn exchange changed phase (OPEN/CLOSED/HOLIDAY).
incident.declaredA platform incident affecting your integration was opened.
incident.resolvedA previously declared incident was closed.

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" } }
 
// 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" } }
 
// 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'];
  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

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).

# 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 }

On this page