Skip to main content
A learner opens your app and sees their assessments as cards. A card they have never opened says Start. A card they have finished shows their score, and offers Retry and Review. Review reopens any past attempt exactly as they saw it on submitting: their answers, locked, each one marked, with the expected answer on anything they missed, the AI’s comment on any AI-graded answer, and their teacher’s comments. This page is the recommended way to build that. A complete, runnable version is in the Angular example: the cards, the history, the review, retry and the webhook, in about 300 lines of server code.

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.
Everything below is detail.

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

Every write is an upsert on 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.
Re-read the attempt in the webhook rather than storing the payload’s numbers. Deliveries can arrive late and out of order, and a fresh read is always current.

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.
Never show a score while is_fully_graded is false. An assessment with open responses is only partly marked at submit time, and its score covers only the automatic questions. Show it as final and you will be taking it back when the teacher marks.

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, returning MAX_ATTEMPTS_REACHED if none are left.

Review a past attempt

Your server, after checking ownership:
Your page:
Pass the response through unchanged. The paper is read-only, with no submit button. There is no token, and the browser makes no call to Edpire, so the origin allow-list does not apply to it. The history list (Attempt 1, Attempt 2 and so on) comes from your own table. Only the paper being looked at is fetched.

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:
Each step also carries its 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.
If your flow shuffles questions, keep the order you showed them in. Steps come back in the assessment’s order, and Edpire does not record the order an attempt was presented in.
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 in cardFor. 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() and readOnly.
  • 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.