Anonymous Teaser API
The teaser is a three-step anonymous mini-diagnostic that lets a visitor experience one Arabic
literacy skill assessment without creating an account. It is intended for embedding in marketing
landing pages. All three routes carry security: [] in the OpenAPI spec; no Authorization header
is required or accepted.
The teaser writes nothing to the measurement, mastery, or evidence tables. It is analytically isolated from the production diagnostic pipeline.
Routes
| Method | Path | Description |
|---|---|---|
POST | /api/teaser/sessions | Start a session for a grade |
POST | /api/teaser/sessions/{id}/finish | Score the submitted responses |
POST | /api/teaser/leads | Capture an opt-in contact |
Step 1: Start a session
curl -X POST https://localhost:3000/api/teaser/sessions \
-H "Content-Type: application/json" \
-d '{ "grade": 2 }'| Field | Type | Notes |
|---|---|---|
grade | integer | 1 to 4 |
source | string | optional referral tag |
Response
{
"token": "<opaque-token>",
"sessionId": "...",
"subSkillId": "...",
"items": [
{ "id": "...", "itemType": "...", "prompt": "...", "options": [...] }
]
}Ten items are returned with answer keys stripped. The opaque token must be stored by the
caller and passed to both finish and leads; it is the only proof of session ownership.
Rate limit
3 sessions per IP per hour. The limit is enforced on a salted HMAC hash of the client IP; the
raw IP is never persisted. Exceeding the limit returns 429 with a retryAfter field (seconds).
Client IP is read from the cf-connecting-ip / x-forwarded-for header set by the trusted edge
proxy. The teaser shares the same trusted-proxy posture as the rest of the platform.
503: no active item set for grade
When no TeaserItemSet row with isActive = true exists for the requested grade, the server
returns 503 TEASER_UNAVAILABLE. Currently grades 1 and 3 return 503 because the seed corpus does
not yet have a sub-skill with 10 qualifying items at those grades. Client code should handle 503
gracefully and offer a fallback experience.
Step 2: Finish the session
curl -X POST https://localhost:3000/api/teaser/sessions/<sessionId>/finish \
-H "Content-Type: application/json" \
-d '{
"token": "<opaque-token>",
"responses": [
{ "itemId": "...", "selectedOptionIndex": 2 }
]
}'The server validates the token, scores the responses through the real Rasch 1PL EAP estimator (the same engine used in the production diagnostic), and maps the resulting ability estimate to one of four fixed, grade-neutral logit thresholds:
| Band | Min logit | Arabic label |
|---|---|---|
meets | 1.0 | ميزان: يُتقن |
approaching | 0.0 | ميزان: في الطريق |
below | -1.0 | ميزان: يحتاج دعم |
severe | -Infinity | ميزان: يحتاج دعم مكثف |
These cutoffs are fixed and grade-neutral. Production diagnostics use per-(country, skill) benchmark profiles; the teaser uses version-pinned defaults because an anonymous visitor has no country and the teaser is an explicitly disclaimed sample.
Response
{
"band": "approaching",
"bandLabelAr": "في الطريق",
"bandDescriptionAr": "...",
"disclaimerAr": "هذه تجربة — للحصول على تشخيص كامل اطلب من مدرستك تجربة أمل"
}The Arabic disclaimer string is always included in the response. It is not configurable.
Error codes
| Status | Code | When |
|---|---|---|
401 | TOKEN_INVALID | Token does not match the session |
404 | NOT_FOUND | Session ID does not exist |
409 | CONFLICT | Session was already completed |
503 | TEASER_UNAVAILABLE | Item set is misconfigured |
Step 3: Capture a lead
curl -X POST https://localhost:3000/api/teaser/leads \
-H "Content-Type: application/json" \
-d '{
"sessionId": "...",
"token": "<opaque-token>",
"email": "school@example.com",
"source": "hero-cta"
}'| Field | Type | Notes |
|---|---|---|
sessionId | string | the session ID from Step 1 |
token | string | the opaque token from Step 1 |
email | string | optional; at least one of email or whatsapp required |
whatsapp | string | optional E.164 phone number |
source | string | optional referral tag |
The lead row is upserted on (sessionId): a visitor who returns to provide additional contact
details (for example, adding WhatsApp after first providing only an email) simply updates their
existing row rather than creating a duplicate. Combined with the 3-per-hour session limit, this
closes the anonymous flood-insert vector.
A successful write returns 201 with no body. Both email and whatsapp errors return 422.
Data isolation
All three teaser tables (teaser_session, teaser_lead, teaser_item_set) carry
organizationId = null, the same global-namespace convention as the item bank. No write ever
touches diagnostic_session, mastery_event, xp_transaction, or any other measurement table.
Related guides
- Demo Tenant: a fully authenticated demo environment for sales and pilots
- Diagnostic & Practice: the production diagnostic that authenticated students use