Skip to content
Partner API Docs
← All recipes

Embed a Market Data Widget

≈ 2 hours

Embed a delayed stock ticker, price charts, or a top-movers board — no sub-accounts required. Prices refresh approximately every 15 minutes during market hours. Prefer a backend proxy; embed a read-only pk_data_ key only for low-stakes public surfaces after accepting extraction and shared-quota risk.

The flow

StepEndpoint / EventWhat it does
1POST /api-keys/data-keyCreate a read-only pk_data_ key for widget use (no trading, funds, or PII access).
2GET /market/quotes?symbols=A,B,CDelayed prices for up to 50 symbols. Poll no faster than the approximately 15-minute feed cadence.
3GET /stocks/{symbol}/chart?period=1MPre-shaped chart data (labels, prices, volumes) per period.
4GET /stocks/{symbol}/history?period=1MRaw OHLCV candles for custom chart rendering.
5GET /market/movers?exchange={exchange}&direction=gainersTop gainers/losers by exchange. Refresh on the same approximately 15-minute cadence.
6GET /market/statusShow open/closed state (and a countdown via nextOpen) before displaying prices.
7GET /companies/{symbol}Company profile, fundamentals, logo URL, recent corporate actions.

Implementation

javascript
const BASE = process.env.MYSTOCKS_BASE_URL ?? 'https://mystocks.africa/api/v1/partner';
const API_KEY = BASE.includes('/sandbox/') ? process.env.MYSTOCKS_API_KEY : (process.env.MYSTOCKS_DATA_KEY ?? process.env.MYSTOCKS_API_KEY);
if (!API_KEY) throw new Error('Set MYSTOCKS_DATA_KEY for production or MYSTOCKS_API_KEY for sandbox');
const IS_SANDBOX = BASE.includes('/sandbox/');
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

function logicalKey(...parts) {
  return parts.map(part => String(part).trim().replace(/[^a-zA-Z0-9_-]/g, '-')).join('_');
}

async function api(path, { method = 'GET', body, idempotencyKey, headers = {}, maxAttempts = 4 } = {}) {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const res = await fetch(BASE + path, {
      method,
      headers: {
        'x-api-key': API_KEY,
        ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
        ...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
        ...headers,
      },
      ...(body === undefined ? {} : { body: JSON.stringify(body) }),
    });

    const contentType = res.headers.get('content-type') || '';
    const payload = contentType.includes('json') ? await res.json() : await res.text();
    if (res.ok) return payload;

    const envelope = payload && typeof payload === 'object' ? payload.error : null;
    const code = envelope && typeof envelope === 'object'
      ? envelope.code
      : payload && typeof payload === 'object' && payload.code || 'HTTP_' + res.status;
    const message = envelope && typeof envelope === 'object'
      ? envelope.message
      : typeof envelope === 'string' ? envelope
      : payload && typeof payload === 'object' && payload.message || res.statusText;
    const retryable = (res.status === 429 || res.status >= 500) && (method === 'GET' || idempotencyKey);
    if (!retryable || attempt === maxAttempts - 1) {
      throw Object.assign(new Error(code + ': ' + message), {
        status: res.status, code, details: envelope?.details ?? payload?.details ?? null, payload,
        retryAfterSeconds: Number(res.headers.get('Retry-After')) || null,
      });
    }

    const retryAfter = Number(res.headers.get('Retry-After'));
    const delayMs = Number.isFinite(retryAfter) && retryAfter > 0
      ? retryAfter * 1000
      : Math.min(1000 * (2 ** attempt), 30_000);
    await sleep(delayMs + Math.floor(Math.random() * 250));
  }
}
// Server-side example. Prefer a backend proxy for production clients.
// Embedded pk_data_ keys are extractable and share the parent key's rate-limit bucket.

// Delayed ticker — schedule no faster than once every ~15 minutes during market hours.
async function refreshTicker(symbols) {
  const query = encodeURIComponent(symbols.join(','));
  const { data } = await api('/market/quotes?symbols=' + query);
  return data.map(q => ({ symbol: q.symbol, price: q.usdPrice, changePct: q.changePct }));
}

// Movers are derived from the same delayed feed; use the same cadence.
async function topMovers(exchange) {
  const path = '/market/movers?exchange=' + encodeURIComponent(exchange) + '&direction=gainers&limit=10';
  const { data, meta } = await api(path);
  return { rows: data, total: meta.totalCount };
}

Common mistakes

  • Embedding a pk_data_ key in a production mobile or web bundle. The key is extractable, shares your full key’s rate-limit bucket, and rotation breaks shipped clients — proxy through your backend or mint short-lived /oauth/token tokens.
  • Polling faster than the data refreshes. Prices update roughly every 15 minutes during market hours — sub-30-second polling burns rate limit for nothing.
  • Ignoring 429, Retry-After, and transient 5xx responses. Back off with jitter instead of synchronizing a retry storm across widget instances.
  • Ignoring market status. Show CLOSED (with nextOpen countdown) and the observation timestamp instead of a frozen price that looks broken.
  • Calling a write endpoint with the data key — it returns 403 FORBIDDEN; data keys are market-data GETs only.

Prove it in sandbox first

  • 1.Data keys exist in production only; for sandbox experiments just use your sk_sandbox_ key against the same market-data endpoints.
  • 2.Sandbox reads through the same delayed production feed, so the values and approximately 15-minute freshness model match production market data.