Unicorn Academy for Business gives your users knowledge tests and lessons under your brand. You can embed the test with a script tag, send users to a signed link, or drive everything from your backend with the REST API. Base URL: https://business.unicornprivate.com.
POST /v1/sessions).attempt.completed) or read it with GET /v1/attempts.Categories: us_stocks, options, futures, forex, crypto, private_markets, art. Languages: en it fr de es pt ru zh ja hi ko tr id ar vi pl.
This is a knowledge assessment tool that you use within your own processes. The responsibility for any appropriateness or suitability assessment stays with you. The service does not by itself ensure compliance with MiFID or any other regulation.
Each account has two environments with separate credentials and data:
| Credential | Test | Live | Use |
|---|---|---|---|
| Secret key | sk_test_... | sk_live_... | REST API from your server only: Authorization: Bearer sk_... |
| Public key | pk_test_... | pk_live_... | Embed script and signed links, safe in the browser |
| Signing secret | whsec_... (one per environment) | Verify webhooks; sign user ids and links | |
Test data is never billed and never mixed with live data. Live keys work once a plan is active. The Start plan (0 USD per month, no card, business email) covers live tests through signed links and the embeddable script only: one test category, up to 20 verified users per month, the Unicorn Academy for Business brand; the live REST API, webhooks and the register export answer 403 or 402 plan_upgrade_required until you move to a paid plan. A verified user is a distinct user_id that completes at least one live test in a calendar month (UTC).
The user_id is your own identifier for the end user (1 to 128 characters: letters, digits and _ . : @ + -). Use a pseudonymous id rather than an email address.
Add the script where the test should appear. It creates an iframe that resizes itself.
<div id="unicorn-academy"></div>
<script src="https://business.unicornprivate.com/embed.js"
data-key="pk_live_..."
data-user="user_8231"
data-user-signature="SIGNATURE"
data-category="options"
data-lang="en"
data-user-name="Jane Doe"
async></script>
data-user-signature is hex(HMAC_SHA256(signing_secret, user_id)) computed on your server. It is required in live mode, optional in test mode. In live mode the page that embeds the script must be listed in Websites allowed to embed the test in the dashboard.
curl
printf '%s' 'user_8231' | openssl dgst -sha256 -hmac "$SIGNING_SECRET" | sed 's/^.* //'JavaScript (Node 18+)
import crypto from 'node:crypto';
const signature = crypto.createHmac('sha256', process.env.SIGNING_SECRET).update('user_8231').digest('hex');Python
import hmac, hashlib, os
signature = hmac.new(os.environ["SIGNING_SECRET"].encode(), b"user_8231", hashlib.sha256).hexdigest()When the user finishes, the iframe sends a message to your page:
window.addEventListener('message', (e) => {
if (e.origin !== 'https://business.unicornprivate.com' || e.data?.type !== 'unicorn-academy:completed') return;
console.log(e.data.attempt_id, e.data.passed, e.data.score);
});
Always confirm the result on your server (webhook or API) before acting on it.
Send a user to a test without an API call. Build the link on your server:
https://business.unicornprivate.com/l?key=pk_live_...&user=user_8231&category=crypto&lang=it&exp=1767225600&sig=SIGNATURE
exp is a Unix time in seconds (at most 7 days ahead). sig is hex(HMAC_SHA256(signing_secret, "user|category|lang|exp")), for example user_8231|crypto|it|1767225600. Optional: name (display name for the certificate, not signed) and return (an https URL, signed when present: append |return to the string).
curl
EXP=$(( $(date +%s) + 86400 ))
SIG=$(printf '%s' "user_8231|crypto|it|$EXP" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" | sed 's/^.* //')
echo "https://business.unicornprivate.com/l?key=$PK&user=user_8231&category=crypto&lang=it&exp=$EXP&sig=$SIG"JavaScript (Node 18+)
const exp = Math.floor(Date.now() / 1000) + 86400;
const sig = crypto.createHmac('sha256', SIGNING_SECRET).update(`user_8231|crypto|it|${exp}`).digest('hex');
const url = `https://business.unicornprivate.com/l?key=${PK}&user=user_8231&category=crypto&lang=it&exp=${exp}&sig=${sig}`;Python
import time
exp = int(time.time()) + 86400
sig = hmac.new(SIGNING_SECRET.encode(), f"user_8231|crypto|it|{exp}".encode(), hashlib.sha256).hexdigest()
url = f"https://business.unicornprivate.com/l?key={PK}&user=user_8231&category=crypto&lang=it&exp={exp}&sig={sig}"POST /v1/sessions returns a one time url to open in a browser, a webview or an iframe.
curl
curl https://business.unicornprivate.com/v1/sessions \
-H "Authorization: Bearer $SECRET_KEY" -H "Content-Type: application/json" \
-d '{"user_id":"user_8231","category":"futures","lang":"de","user_name":"Jane Doe","return_url":"https://broker.example/onboarding/next","metadata":{"account":"A-1009"}}'JavaScript (Node 18+)
const res = await fetch('https://business.unicornprivate.com/v1/sessions', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.SECRET_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: 'user_8231', category: 'futures', lang: 'de', return_url: 'https://broker.example/onboarding/next' }),
});
const session = await res.json(); // session.urlPython
import requests, os
r = requests.post("https://business.unicornprivate.com/v1/sessions",
headers={"Authorization": f"Bearer {os.environ['SECRET_KEY']}"},
json={"user_id": "user_8231", "category": "futures", "lang": "de"})
session = r.json() # session["url"]{
"id": "ses_6f1c...",
"object": "session",
"mode": "live",
"user_id": "user_8231",
"category": "futures",
"lang": "de",
"status": "open",
"url": "https://business.unicornprivate.com/s/ses_6f1c...",
"questions": 15,
"pass_mark": 70,
"test_version": "04fe7435e9b8d6cb.3a91c0d2",
"attempt_id": null,
"expires_at": "2026-10-30T10:00:00.000Z"
}
Fields: user_id and category are required. Optional: lang (else your default), user_name (printed on the certificate, Latin characters), return_url (https; the user returns there with session_id, attempt_id and passed in the query), metadata (up to 20 string pairs, returned in results and webhooks), expires_in (seconds, 300 to 604800, default 86400).
Questions are drawn at random from the category bank and options are shuffled for every session. If the user has no attempts left the API returns 409 attempts_exhausted; during a cooldown 409 cooldown_active with retry_after.
curl
curl "https://business.unicornprivate.com/v1/attempts?user_id=user_8231&limit=10" -H "Authorization: Bearer $SECRET_KEY"
curl "https://business.unicornprivate.com/v1/attempts/att_9b2e...?include=items" -H "Authorization: Bearer $SECRET_KEY"
curl "https://business.unicornprivate.com/v1/users/user_8231/status" -H "Authorization: Bearer $SECRET_KEY"JavaScript (Node 18+)
const r = await fetch('https://business.unicornprivate.com/v1/users/user_8231/status', { headers: { Authorization: `Bearer ${SECRET_KEY}` } });
const status = await r.json(); // status.categories[].passedPython
r = requests.get("https://business.unicornprivate.com/v1/attempts", params={"user_id": "user_8231"},
headers={"Authorization": f"Bearer {SECRET_KEY}"})
for a in r.json()["data"]:
print(a["category"], a["score"], a["passed"]){
"id": "att_9b2e...",
"object": "attempt",
"mode": "live",
"session_id": "ses_6f1c...",
"user_id": "user_8231",
"category": "futures",
"lang": "de",
"test_version": "04fe7435e9b8d6cb.3a91c0d2",
"started_at": "2026-10-29T09:58:02.114Z",
"completed_at": "2026-10-29T10:06:41.870Z",
"score": 80, "correct": 12, "total": 15, "pass_mark": 70, "passed": true,
"study_path": [{"course": "futures", "course_title": "...", "module": 1, "lesson": 3, "title": "...", "missed": 2}],
"certificate_url": "https://business.unicornprivate.com/v1/attempts/att_9b2e.../certificate.pdf",
"metadata": {"account": "A-1009"},
"seq": 418, "hash": "5d0c..."
}
List endpoints return {"object":"list","data":[...],"has_more":bool}. Paginate with before_seq (the seq of the last item). Filters: user_id, category, passed, from, to (ISO dates), limit (up to 100).
Passed attempts have a one page PDF certificate with your brand, the score, the date, the attempt id and a verification link. Download it with your secret key:
curl -o certificate.pdf "https://business.unicornprivate.com/v1/attempts/att_9b2e.../certificate.pdf" -H "Authorization: Bearer $SECRET_KEY"
The user also gets a personal download link on the result page.
Set a webhook URL (https) in the dashboard. Each completed attempt sends POST with a JSON event:
{
"id": "evt_1a2b...",
"type": "attempt.completed",
"created": 1761732401,
"mode": "live",
"data": { "id": "att_9b2e...", "object": "attempt", "user_id": "user_8231", "category": "futures", "score": 80, "passed": true, "...": "..." }
}
The header Unicorn-Signature: t=1761732401,v1=HEX carries HEX = HMAC_SHA256(signing_secret, t + "." + raw_body). Verify it with the raw body and reject events older than 5 minutes. Answer with any 2xx status within 10 seconds; otherwise we retry up to 8 times over about two days (1, 5, 30, 120, 360, 720, 1440 minutes). Events can arrive more than once: use id to ignore duplicates. POST /v1/webhooks/test sends a webhook.test event.
curl
# Compute the expected signature for a saved body
printf '%s' "$T.$(cat body.json)" | openssl dgst -sha256 -hmac "$SIGNING_SECRET"JavaScript (Node 18+)
import crypto from 'node:crypto';
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ''));
}Python
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
expected = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Every attempt is stored with date, the questions shown in the order shown, the options, the answer given, the correct answer, the score, the test version and the language. Records are append only and linked in a hash chain: hash = SHA256(prev_hash + "|" + canonical_json(record)), where canonical JSON has sorted keys. Records older than your retention period are deleted; their hashes stay in the chain without personal data.
curl
curl -o attempts.csv "https://business.unicornprivate.com/v1/exports/attempts?format=csv&from=2026-10-01&to=2026-10-31" -H "Authorization: Bearer $SECRET_KEY"
curl "https://business.unicornprivate.com/v1/audit/verify" -H "Authorization: Bearer $SECRET_KEY"JavaScript (Node 18+)
const csv = await (await fetch('https://business.unicornprivate.com/v1/exports/attempts?format=csv', { headers: { Authorization: `Bearer ${SECRET_KEY}` } })).text();Python
r = requests.get("https://business.unicornprivate.com/v1/exports/attempts", params={"format": "json"},
headers={"Authorization": f"Bearer {SECRET_KEY}"})
open("attempts.json", "wb").write(r.content)The test version combines the catalog version and a fingerprint of the category settings (number of questions and pass mark), so you can tell which rules applied to each attempt.
| Method and path | Description |
|---|---|
GET /v1/categories | Categories, number of questions in each bank, your settings |
POST /v1/sessions | Create a test session for a user |
GET /v1/sessions/{id} | Session status and attempt id |
GET /v1/attempts | List attempts (filters above) |
GET /v1/attempts/{id} | One attempt; ?include=items adds questions and answers |
GET /v1/attempts/{id}/certificate.pdf | PDF certificate (passed attempts) |
GET /v1/users/{user_id}/status | Per category: attempts, attempts left, passed, last attempt |
GET /v1/exports/attempts?format=csv|json | Full export for audits (optional from, to) |
GET /v1/audit/verify | Recompute the hash chain and report any break |
POST /v1/webhooks/test | Send a test event to your webhook URL |
Errors return JSON: {"error":{"code":"invalid_category"}} with an HTTP status: 400 invalid input, 401 invalid key, 402 live mode without an active plan, 403 not allowed, 404 not found, 409 conflict (attempts exhausted, cooldown, session completed), 410 session expired, 429 too many requests, 5xx temporary problem (retry with backoff).
Rate limit: 600 API requests per minute per environment. Questions per test: 5 to 40. Session lifetime: 5 minutes to 7 days.