Skip to main content
Custom Flow gives you full control over pacing, navigation, and scoring UX. The SDK renders questions and collects answers; you own everything else — progress bars, hearts/lives, animations, “next” buttons. The shape of a Custom Flow:
1

Fetch the assessment (server)

Your server calls client.getAssessment() — the API key never reaches the browser.
2

Flatten into steps

flattenAssessment() turns the nested assessment into a flat array of questions.
3

Render each question

<EdpireQuestion> (React) or renderQuestion() (vanilla) renders one step at a time.
4

Check answers per question

Your server proxies POST /check for immediate per-question feedback. Stateless — no record created.
5

Submit the full attempt

When done, your server calls client.submit() once to record + grade the whole attempt.

1. Fetch (server)

Keep the API key server-side — never call Edpire directly from the browser. Expose a thin proxy your browser can call:

2. Flatten — flattenAssessment()

Returns an ordered FlatStep[] — one entry per question, in the order a learner would see them (exercise order, then question order).
exerciseId and questionId are required for the /check call — that’s why flattenAssessment preserves them on every step. Use index for progress (index + 1 of steps.length).

3. Render — <EdpireQuestion> or renderQuestion()

Generate your session_id now, before rendering the first question. You’ll need it for every /check call and it must be the same across all questions in one attempt. Create it once at attempt start:
If you wait until step 4 and generate one per question, rate limiting accumulates across retakes and causes surprise 429s.

EdpireQuestionProps

EdpireQuestion is deliberately unstyled beyond what makes the interaction work. It inherits your fonts and colors, and your CSS reaches into it on purpose. Style it to match your product.
feedback takes the raw JSON from /check, with no manual mapping. Internally the component converts it to the Map the runtime expects.

renderQuestion() reference

The imperative equivalent, for Vue, Svelte, Angular or plain JS. Also available as EdpireSDK.renderQuestion on the script-tag build.
RenderQuestionOptions takes the same fields as EdpireQuestionProps plus container. QuestionInstance:

4. Check per question — POST /check

Grade a single question for immediate feedback. Stateless — no submission is recorded. Proxy it through your server so the API key and include_correct_answers flag stay trusted:
The browser then passes result.feedback straight into EdpireQuestion’s feedback prop (or q.setFeedback()).
Rate limiting: 3 checks per question, per session, per rolling hour (configurable per org), keyed on (api key, assessment, question, session_id) and falling back to learner_ref if session_id is omitted. Use the session_id you generated in step 3 — one per attempt, not per question.

How feedback maps onto the question

The /check response’s feedback is keyed by node ID → feedback state (correct/partial/incorrect, score, message, and — when include_correct_answers is true — the correct answer fields). EdpireQuestion consumes that object directly and renders the right per-node visual state. Set feedback back to null (or call setContent) before showing the next question to reset it. When include_correct_answers is true, feedback also includes: correctChoiceIds (choice/drag-drop), correctPairings (matching), and displayAnswer (typed blanks).

5. Submit the full attempt: client.submit()

/check records nothing. After the last question, submit the whole attempt once to create the graded submission record (and fire the submission.graded webhook). Collect answers as a flat StoredAnswer[] during the session:
client.submit() accepts either a flat StoredAnswer[] or the nested { exerciseAnswers: [...] } format. If you need the nested form explicitly (e.g. constructing it elsewhere), convert with buildSubmitPayload(stored).
Required API key scopes: read:assessments (fetch + check) and write:submissions (the final submit).
If the learner abandons mid-flow: /check is stateless, so nothing is recorded. If they close the tab before step 5, their attempt is lost. Consider saving answers to sessionStorage after each question so you can restore them on reload, and warn users before they navigate away during an active attempt.

6. Review a past attempt, one question at a time

Once an attempt is submitted, the learner can page back through it the way they took it: one question per screen, their answer locked, marked, with the expected answer on anything missed. Since 0.8.0. Your server fetches the attempt with GET /submissions/{id}/review (client.getSubmissionReview(id, { learnerRef })) after checking it belongs to the learner. The browser turns it into steps and draws each one read-only:
ReviewStep
Steps come back in the assessment’s order. Edpire does not record the order an attempt was shown in, so if you shuffle, keep the question IDs in the order you used and sort the steps by them. The Angular example does this.
flattenReview is pure, so it is also exported from @edpire/sdk/core and from the script-tag build as EdpireSDK.flattenReview. To show the whole attempt at once instead, use EdpireAssessment.review(). The design around both, including what to store, is in Attempt History.

Full example

The steps above map directly to a complete implementation — see the Custom Flow example for a reference build that wires all five steps together end-to-end.