← 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
| Step | Endpoint / Event | What it does |
|---|---|---|
| 1 | POST /api-keys/data-key | Create a read-only pk_data_ key for widget use (no trading, funds, or PII access). |
| 2 | GET /market/quotes?symbols=A,B,C | Delayed prices for up to 50 symbols. Poll no faster than the approximately 15-minute feed cadence. |
| 3 | GET /stocks/{symbol}/chart?period=1M | Pre-shaped chart data (labels, prices, volumes) per period. |
| 4 | GET /stocks/{symbol}/history?period=1M | Raw OHLCV candles for custom chart rendering. |
| 5 | GET /market/movers?exchange={exchange}&direction=gainers | Top gainers/losers by exchange. Refresh on the same approximately 15-minute cadence. |
| 6 | GET /market/status | Show open/closed state (and a countdown via nextOpen) before displaying prices. |
| 7 | GET /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.