Skip to content
MyStocks Developers
API v1

API v1

Current stable contract

Versioning policyRelease changelog
Sandbox console
← All recipes

Browse Published Pre-IPO Offerings

Sandbox workflow; production allocation is a separate reviewed process

Published offering information and synthetic demonstrations share Sandbox discovery but never share real customer reservations. Select a returned ID, read its terms and create a virtual commitment in your isolated test account.

The flow

StepEndpoint / EventWhat it does
1GET /opportunities?type=PRE_IPOInspect catalogueSource and simulationOnly; published information is not a real reservation.
2GET /opportunities/{id}Read minimumCommitmentUsd, subscriptionAvailable and restrictions before committing.
3POST /users/{userId}/subscribeSandbox only: assetType PRE_IPO, canonical assetId and a positive USD amount.
4GET /users/{userId}/portfolioCheck the virtual holding and debit; replay the same logical action with its original idempotency key.

Implementation

javascript
const BASE = process.env.MYSTOCKS_BASE_URL ?? 'https://mystocks.africa/api/sandbox/v1/partner';
const API_KEY = process.env.MYSTOCKS_API_KEY;
if (!API_KEY) throw new Error('Set MYSTOCKS_API_KEY before running this recipe');
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));
  }
}

if (!IS_SANDBOX) throw new Error('This recipe performs virtual Sandbox commitments only');
const userId = process.env.MYSTOCKS_SUB_ACCOUNT_ID;
const offeringId = process.env.MYSTOCKS_OFFERING_ID;
const amount = Number(process.env.MYSTOCKS_COMMITMENT_USD);
if (!userId || !offeringId || !Number.isFinite(amount) || amount <= 0) throw new Error('Set customer ID, returned offering ID and positive USD commitment');
const { data } = await api('/opportunities?type=PRE_IPO');
if (!data.some(row => row.id === offeringId)) throw new Error('Select an ID returned by the catalogue');
const offering = await api('/opportunities/' + encodeURIComponent(offeringId));
if (!offering.simulationOnly || !offering.subscriptionAvailable) throw new Error(offering.subscriptionRestriction || 'Virtual subscription unavailable');
if (offering.minimumCommitmentUsd != null && amount < offering.minimumCommitmentUsd) throw new Error('Commitment below published USD minimum');
const actionId = process.env.MYSTOCKS_COMMITMENT_ACTION_ID;
if (!actionId) throw new Error('Set a stable, unique action ID; reuse it only for retries of this commitment');
const result = await api('/users/' + encodeURIComponent(userId) + '/subscribe', {
  method: 'POST', idempotencyKey: logicalKey('virtual-preipo', userId, actionId),
  body: { assetType: 'PRE_IPO', assetId: offering.id, amount },
});
console.log(result, await api('/users/' + encodeURIComponent(userId) + '/portfolio'));

Common mistakes

  • —Do not use a browse slug as subscription assetId; use the canonical returned ID.
  • —Do not imply a real share allocation from a Sandbox fill. No production reservation or offering total changes.
  • —Never compare local-currency minimums directly with USD commitments. Respect returned availability and restrictions.

Prove it in sandbox first

  • 1.Fund the synthetic customer with virtual USD before committing. Published PUBLISHED, LISTED and ALLOCATED offerings may appear alongside labelled demos.
  • 2.Test below-minimum rejection, wrong asset type, duplicate replay and tenant isolation. Review virtual orders separately in admin; real offerings are managed in /admin/pre-ipo.