← 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
| Step | Endpoint / Event | What it does |
|---|---|---|
| 1 | GET /opportunities?type=PRE_IPO | Inspect catalogueSource and simulationOnly; published information is not a real reservation. |
| 2 | GET /opportunities/{id} | Read minimumCommitmentUsd, subscriptionAvailable and restrictions before committing. |
| 3 | POST /users/{userId}/subscribe | Sandbox only: assetType PRE_IPO, canonical assetId and a positive USD amount. |
| 4 | GET /users/{userId}/portfolio | Check 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.