Skip to main content
The Embedded Player renders Edpire’s complete assessment UI inside a container on your page. The SDK fetches the assessment, handles every interaction, grades the submission and shows per-question feedback, all from one call. The learner never leaves your app. This is the Exam Hall mode from Two ways to ship. For the question-by-question alternative, see Custom Flow.
Why a server endpoint? You cannot call Edpire directly from the browser: your API key would sit in the JavaScript bundle for anyone to read. The token endpoint lets your server verify the learner from your own session, mint a short-lived single-use token, and send only that to the browser.
Fastest start: scaffold a ready-to-run example instead of wiring this by hand.
Both ship inside the @edpire/sdk npm package. Each has a README with next steps.

Step 1. Mint tokens on your server

Every integration needs this, whatever your frontend is.
Never trust the client for the learner ID. Resolve the learner from your server session. If you read a user ID from the request body, anyone can mint tokens as anyone else. createEdpireTokenHandler enforces this through resolveLearner.If some of your content is paid or restricted, check entitlement here too, not only on the page that renders the player. This endpoint is reachable on its own.
The token expires after 2 hours and is consumed when the learner submits. Mint a fresh one each time the learner opens the assessment. Minting does not invalidate a token already in use, so a learner with the assessment open in another tab is unaffected.

Step 2. Mount the player

<EdpireAssessmentPlayer> handles token fetching, mounting, unmounting and StrictMode double-invoke cleanup for you.
That is the entire integration.

React version support


Container sizing

The player fills its container, so you are responsible for sizing it. A container with no explicit height, or one constrained by centering utilities, collapses to zero and hides the player.
If your CSS framework applies global centering or max-width to #root or body (common in Vite and CRA templates), override those rules for the player container. A width: 1126px; margin: auto wrapper visually cuts off the player and can break RTL layout.

The column inside the box

Sizing the container sizes the player’s outer box. Inside it, the questions sit in a centred column capped at 48rem (768px), so on a wide container you will see empty space on both sides. That cap is a reading measure, not an oversight. At the runtime’s base size it already yields roughly 70 to 90 characters per line; past about 60rem the eye starts losing its place on the return sweep, which costs a learner real comprehension on a long passage. Keep it when the player owns the page. Override it with layout when the player is embedded inside your own chrome and your layout already supplies the measure, otherwise learners see a column inside a column:
maxWidth takes any CSS length, so "60rem" widens the column without removing the measure entirely. The value also drives the post-submit score banner, so the two always line up.
When an assessment has an annex (a reference passage or figure), wide viewports use a two-column layout that already fills the container. maxWidth then applies only to the narrow, single-column breakpoint.From SDK 0.9.0 the same panel also holds every long exercise text (over 80 words, more than one picture or video, or a table over 6 rows): the exercise shows a line linking to it, and on a wide viewport the panel follows the learner to it. An assessment whose only long text is an exercise’s gets the panel too.

Allowed origins

The embed player is origin-scoped: it only loads from domains you explicitly allow. A blank player, or ORIGIN_NOT_ALLOWED in onError, almost always means this list.
  1. Go to edpire.com → Integrations → Security → Allowed Embed Origins
  2. Add every origin your app runs on, including staging and local development:
    • http://localhost:3000 (Next.js dev)
    • http://localhost:5173 (Vite dev)
    • https://yourapp.com (production)
  3. Save and reload
Origins are matched exactly: scheme, host and port. https://app.example.com does not cover https://www.app.example.com.There are two separate lists. Allowed Embed Origins governs the browser SDK. Allowed API & Redirect Origins governs return_url and REST CORS. An origin present in the second but missing from the first looks registered and still fails.
Native WebViews send no origin, or an opaque one like capacitor://localhost. Both are handled, so there is nothing to register for mobile.

Reviewing a past attempt

EdpireAssessment.review() reopens a finished attempt as the corrected paper: the learner’s answers, locked, every mark, the expected answer on anything missed, and the teacher’s comments on open responses. It is the same screen the player shows the moment the learner submits, available again afterwards. Since 0.8.0. Your server fetches the attempt with GET /submissions/{id}/review after checking it belongs to the signed-in learner. The browser passes the response through unchanged:
There is no token and no request to Edpire from the browser, so the origin allow-list does not apply. The container needs a real height, exactly as for mount(). To page through the attempt one question per screen instead, Arcade-style, see flattenReview(). For the full picture (what to store, the cards, retry, and when to call this) see Attempt History.

Reference

EdpireAssessmentPlayerProps (React)

MountOptions (imperative)

Everything above applies, minus the React-only props. mount() takes the token directly rather than a tokenEndpoint.

mediaHandler

Required only if your assessment contains OpenResponse questions with file-upload or audio/video recording. Without it, those question types silently fail to submit.
upload receives (file, type) and must resolve to { url, mimeType? }. There is no progress callback. If you want a progress bar, track it in your own upload code and render it outside the player.

EmbedInstance

Safe to call from inside onComplete: the SDK defers the actual React unmount so it never fires mid-render.

EmbedResult

Passed to onComplete. Note the fields are snake_case, mirroring the API response.
If awaiting_manual_grading is true, the visible score is provisional: open-response answers still need a teacher. Listen for the submission.grading.completed webhook for the final score.

EmbedError


Advanced: bring your own data with AssessmentShell

EdpireAssessmentPlayer fetches the assessment client-side after mount and handles everything. Drop down to AssessmentShell only when you need to control data loading yourself: server-rendering the assessment for a faster first paint, pre-fetching before the learner navigates, or plugging into your own caching layer. AssessmentShell expects data in the runtime shape (AssessmentContent), not the raw REST Assessment. That conversion is yours.
Your onSubmit grades the attempt and returns a SubmitResult. Note that client.submit() returns GradeResult in snake_case while the shell expects SubmitResult in camelCase, so map between them on the server:
client.submit() returns per-question scores (exercise_results) but not the per-node visual feedback (exerciseFeedback) the shell uses to highlight answers inline. If you need both, use the Embedded Player above, which gets the full response automatically.

AssessmentShellProps

exerciseFeedback drives the inline per-question feedback. See the type catalog for ExerciseFeedbackData, QuestionFeedback and NodeFeedback.

Next steps

Custom Flow

Question-by-question instead, with your own UI around it.

Webhooks

Receive results server-side, which you want as the primary path.

Mobile and WebView

The same player in React Native, Flutter or native.

Troubleshooting

Blank player, token errors, styles behaving oddly.