The whole design in two sentences
Store the numbers. Fetch the paper.Keep one small table of attempts (who, which assessment, the score) in your own database, and build every card from it. When a learner opens a past attempt, ask Edpire for that one attempt and hand it to the SDK.
Who keeps what
The rule behind the table: copy what you list, fetch what you open. A catalogue page lists many assessments at once, so it must not depend on another service. A corrected paper is opened one at a time, by a learner who has just clicked, so one call is fine.
The one table you add
submission_id, so writing the same attempt twice is harmless. That matters, because it will be written more than once.
Keeping it up to date
Write the table from one function, and call it from three places.
Each caller passes only a submission ID, and
recordAttempt reads the facts from Edpire with your API key. The browser path also passes the signed-in learner, so nobody can claim someone else’s attempt by posting its ID.
The cards
A card’s state is plain code over your own rows:
Compute the state on your server and send it to every client, so your web app, your mobile app and a weekly email never disagree.
Retry
There is no retry endpoint. A retry is a new attempt: mint a token and mount the player exactly as the first time. Edpire numbers the attempt and enforces the assessment’s limit, returningMAX_ATTEMPTS_REACHED if none are left.
Review a past attempt
Your server, after checking ownership:Or one question at a time
For a flow that shows one question per screen, give the review the same shape. The same response, turned into steps:score, maxScore, a status for your verdict line, and the exercise’s reading passage. Previous, Next and the progress dots are yours to draw. Both views work for any attempt, whichever way it was taken. Details in Custom Flow.
Your API key needs
read:results on top of the scopes you already use.
Two product decisions that are yours
Which score goes on the card. The latest attempt shows where the learner is now. The best attempt rewards effort. The example shows the latest as the headline and the best beside it when it differs. Both are one line incardFor.
Retrying after seeing the corrections. Review shows the expected answers. A learner can read them and retry for full marks. That is fine for practice. For anything that counts, cap attempts in Edpire, or show and record the first attempt’s score.
Good to know
- Review needs SDK 0.8.0 or later, for
EdpireAssessment.review(),flattenReview()andreadOnly. - A question worth 0 points (usually one published with nothing to answer) has status
unscored. Show it as neutral, not wrong. In a one-question-at-a-time flow, let the learner move past it: a Check button that waits for an answer will never enable. - The paper uses the current version of the assessment. Answers and marks are keyed by question, so a republish keeps them attached. A question added since shows as unanswered.
- An unpublished assessment cannot be reviewed. The endpoint returns 409. The scores in your table are unaffected.
- Attempts from before September 2026 made through the SDK or the REST submit were stored without per-question detail and return 409. Their scores are intact. Attempts made from edpire.com links are not affected.
Checklist
1
Create the table
edpire_attempts, keyed on submission_id.2
Write recordAttempt
One function. It reads from Edpire and upserts.
3
Call it three ways
From the player’s
onComplete (via your backend), from the webhook, and once as a backfill.4
Build the cards from your table
new, done, grading. No Edpire call per card.5
Add the review route
Check ownership, call
getSubmissionReview with learnerRef, and pass the result to EdpireAssessment.review() for the whole paper, or flattenReview() + renderQuestion({ readOnly: true }) for one question at a time.6
Add read:results to your API key
Review and backfill both need it.