Skip to Content

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

MethodPathDescription
POST/api/teaser/sessionsStart a session for a grade
POST/api/teaser/sessions/{id}/finishScore the submitted responses
POST/api/teaser/leadsCapture 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 }'
FieldTypeNotes
gradeinteger1 to 4
sourcestringoptional 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:

BandMin logitArabic label
meets1.0ميزان: يُتقن
approaching0.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

StatusCodeWhen
401TOKEN_INVALIDToken does not match the session
404NOT_FOUNDSession ID does not exist
409CONFLICTSession was already completed
503TEASER_UNAVAILABLEItem 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" }'
FieldTypeNotes
sessionIdstringthe session ID from Step 1
tokenstringthe opaque token from Step 1
emailstringoptional; at least one of email or whatsapp required
whatsappstringoptional E.164 phone number
sourcestringoptional 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.