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 function call. The learner never leaves your app.
Your server ──mintEmbedToken()──▶ Edpire (API key stays server-side)
│
▼ token
Your browser ──EdpireAssessment.mount({ token })──▶ player renders in your <div>
│
▼ onComplete(result)
Your app handles the score
Why a server endpoint? You can’t call Edpire directly from the browser — your API key would be visible in the JavaScript bundle for anyone to read. The token endpoint lets your server verify the learner (from your session), mint a short-lived single-use token, and send only that to the browser. The browser never touches your API key.
Fastest start: scaffold a ready-to-run example instead of wiring this by hand.# Next.js App Router
npx --package=@edpire/sdk create-edpire-app nextjs my-app
# Vite + Express (works in dev and production)
npx --package=@edpire/sdk create-edpire-app vite-express my-app
Both examples ship inside the @edpire/sdk npm package — no separate repo needed. Each has a README with next steps.
Quickstart — React (recommended)
If you’re using React, use <EdpireAssessmentPlayer> from @edpire/sdk/react. It handles token fetching, mounting, and cleanup automatically.
Server — create the token endpoint:
// app/api/edpire/token/route.ts (Next.js App Router)
import { createEdpireTokenHandler } from "@edpire/sdk/client"
export const POST = createEdpireTokenHandler({
apiKey: process.env.EDPIRE_API_KEY!, // server-side only — never NEXT_PUBLIC_
resolveLearner: async (req) => {
// Read the learner from YOUR session — never from the request body.
const session = await auth.api.getSession({ headers: req.headers })
return session?.user.id ?? null // null → 401 Unauthorized
},
})
Browser — render the player:
import { EdpireAssessmentPlayer } from "@edpire/sdk/react"
<EdpireAssessmentPlayer
tokenEndpoint="/api/edpire/token"
assessmentId="your-assessment-uuid"
onComplete={(r) => console.log("Score:", r.score, "/", r.max_score)}
onError={(e) => console.error(e.code, e.message)}
style={{ width: "100%", height: "100vh" }}
/>
That’s the entire integration. The component handles token fetching, useRef / useEffect lifecycle, and StrictMode double-invoke cleanup for you. See EdpireAssessmentPlayerProps for the full prop list.
Quickstart — vanilla JS / other frameworks
For non-React setups (Vue, Svelte, Solid, plain JS):
1. Mint a token (server endpoint):
// app/api/edpire/token/route.ts
import { createEdpireTokenHandler } from "@edpire/sdk/client"
export const POST = createEdpireTokenHandler({
apiKey: process.env.EDPIRE_API_KEY!,
resolveLearner: async (req) => {
const session = await auth.api.getSession({ headers: req.headers })
return session?.user.id ?? null
},
})
import { createEdpireTokenHandler, toNodeHandler } from "@edpire/sdk/client"
const handler = toNodeHandler(createEdpireTokenHandler({
apiKey: process.env.EDPIRE_API_KEY!,
resolveLearner: (req) => req.session?.userId ?? null,
}))
// Express:
app.post("/api/edpire/token", handler)
// Vite configureServer (dev only — use Express for production):
server.middlewares.use("/api/edpire/token", handler)
import { EdpireClient } from "@edpire/sdk/client"
const client = new EdpireClient({ apiKey: process.env.EDPIRE_API_KEY! })
// Inside your authenticated server route handler:
const learnerRef = session.userId // from YOUR session — never from the request body
const { token } = await client.mintEmbedToken(assessmentId, learnerRef)
return Response.json({ token })
Security — never trust the client for the learner ID. The token endpoint should
resolve the learner from your server session (req.session.userId, getServerSession(),
etc.). If you read userId from the request body, anyone can mint tokens for other learners.
createEdpireTokenHandler enforces this via the resolveLearner callback.
The token expires in 1 hour and is single-use. Mint a fresh one each time the learner opens the assessment.
2. Mount the player (browser):
import { EdpireAssessment } from "@edpire/sdk"
const { token } = await fetch("/api/edpire/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ assessmentId: "your-uuid" }),
}).then((r) => r.json())
const embed = EdpireAssessment.mount({
token,
container: "#assessment-root", // CSS selector or HTMLElement
onComplete: (result) => {
console.log(`Score: ${result.score}/${result.max_score} (${result.percentage}%)`)
console.log(`Passed: ${result.passed}`)
},
onError: (err) => console.error(err.code, err.message),
})
// When you're done (e.g. closing a modal):
embed.unmount()
Container sizing
The player fills its container. You are responsible for sizing the container. A container with no explicit height (or one constrained by centering utilities) collapses to zero and hides the player.
/* Minimal example — full viewport */
#assessment-root {
width: 100%;
height: 100vh;
}
/* React — use className or style prop */
<EdpireAssessmentPlayer style={{ width: "100%", height: "100vh" }} ... />
If you’re using a CSS framework that applies global centering or max-width constraints to #root or body (common in Vite and CRA templates), remove or override those rules for the player container. A width: 1126px; margin: auto wrapper will visually cut off the player and may break layout in RTL mode.
Environment variables
# .env
EDPIRE_API_KEY=edp_live_... # server-side only — never NEXT_PUBLIC_ or VITE_
Allowed Origins
The embed player is origin-scoped — it only loads from domains you explicitly allow. If you see a blank player or receive ORIGIN_NOT_ALLOWED in onError:
- Go to edpire.com → Settings → Integrations → Allowed Origins
- Add every origin your app runs on:
http://localhost:3000 (Next.js dev)
http://localhost:5173 (Vite dev)
https://yourapp.com (production)
- Save and reload — the player should appear immediately.
Not sure if it’s an origin error? Add a temporary onError handler to log the code:onError: (e) => console.error("[edpire]", e.code, e.message)
ORIGIN_NOT_ALLOWED means the allow-list is missing your domain.
MountOptions
| Option | Type | Required | Description |
|---|
token | string | Yes | Embed token from client.mintEmbedToken(). Pass from your server — never hardcode. |
container | string | HTMLElement | Yes | CSS selector or DOM element to mount into. |
baseUrl | string | No | Edpire instance URL. Defaults to https://edpire.com. Override for staging/self-hosted. |
locale | "en" | "fr" | "ar" | No | UI language. Defaults to "en". RTL layout is applied automatically for "ar". |
branding | TakeBranding | No | Override the org’s logo/colors/fonts (white-labelling). By default the assessment’s org branding loads automatically. See Branding. |
returnUrl | string | No | When set, the post-submit “View Full Report” button redirects here with ?submission_id=…&score=…&max_score=… appended. Omit to hide the button. |
reportLabel | string | No | Custom label for the report/redirect button. |
onBack | () => void | No | Called when the learner clicks the Back button in the top bar (visible only before submission). Use it to navigate back in your app or close a modal. |
backLabel | string | No | Custom label for the back button. |
overlayConfig | OverlayConfig | No | Customise the grading overlay (spinner theme, colors, animation). See Branding. |
mediaHandler | QuestionRuntimeProps["mediaHandler"] | No | Required if the assessment has OpenResponse questions with file-upload or recording modes. See mediaHandler below. |
onComplete | (result: EmbedResult) => void | No | Called when the learner successfully submits. |
onError | (error: EmbedError) => void | No | Called on a non-recoverable error. |
Required only if your assessment contains OpenResponse questions with file-upload or audio/video recording. Without it those question types silently fail to submit.
EdpireAssessment.mount({
token,
container: "#root",
mediaHandler: {
/**
* Called when the learner selects a file (image, audio, video, document).
* Upload it to your storage and return the permanent URL.
*/
upload: async (file, { onProgress }) => {
const form = new FormData()
form.append("file", file)
// Example: your own upload endpoint backed by S3, R2, etc.
const xhr = new XMLHttpRequest()
return new Promise((resolve, reject) => {
xhr.upload.onprogress = (e) => onProgress?.(e.loaded / e.total)
xhr.onload = () => {
const { url } = JSON.parse(xhr.responseText) as { url: string }
resolve({ url })
}
xhr.onerror = reject
xhr.open("POST", "/api/upload")
xhr.send(form)
})
},
},
})
## Return value — `EmbedInstance`
```typescript
interface EmbedInstance {
unmount: () => void
}
Call unmount() to tear down the player and clean up. It’s safe to call from inside onComplete — the SDK defers the actual React unmount so it never fires mid-render.
EmbedResult
Passed to onComplete after a successful submission.
interface EmbedResult {
submission_id: string
score: number
max_score: number
percentage: number
passed: boolean
passing_score_percent: number
attempt_number: number
/** True when open-response nodes are still pending teacher review. */
awaiting_manual_grading?: boolean
/**
* Per-question scores — use this for scorecards, gradebooks, or
* recording results. Has scores but not the visual feedback state.
*/
exercise_results: Array<{
exercise_id: string
total_score: number
max_score: number
question_results: Array<{
question_id: string
score: number
correct: boolean
points: number
}>
}>
/**
* Per-node visual feedback — use this to show learners what they got
* right/wrong inline. Includes status, display_answer, and correctChoiceIds.
* The player renders this automatically; access it if you're building a
* custom results screen.
*/
exercise_feedback: Array<{
exercise_id: string
questions: Array<{
question_id: string
node_results: Array<{
node_id: string
status: "correct" | "partial" | "incorrect" | "awaiting_review"
score: number
max_score: number
feedback?: string
display_answer?: string
detail?:
| { type: "choiceSet"; correctChoiceIds: string[] }
| {
type: "matchingSet"
pairFeedback: Array<{ leftId: string; rightId: string; correct: boolean }>
correctPairings: Array<{ leftId: string; rightId: string }>
}
}>
}>
}>
}
If awaiting_manual_grading is true, the visible score is provisional — open-response answers still need a teacher. Listen for the submission.graded webhook to learn the final score.
EmbedError
Passed to onError.
interface EmbedError {
code:
| "TOKEN_INVALID" | "TOKEN_EXPIRED" | "TOKEN_USED"
| "ORIGIN_NOT_ALLOWED" | "ASSESSMENT_NOT_FOUND"
| "MAX_ATTEMPTS_REACHED" | "NETWORK_ERROR" | "UNKNOWN"
message: string
}
| Code | Meaning | How to recover |
|---|
TOKEN_INVALID | Token is malformed or unrecognized. | Mint a new token server-side. |
TOKEN_EXPIRED | Token is older than 1 hour. | Mint a new token. |
TOKEN_USED | Token was already redeemed (single-use). | Mint a new token per open. |
ORIGIN_NOT_ALLOWED | The page origin isn’t in your Allowed Origins. | Add the origin in org/integrations. |
ASSESSMENT_NOT_FOUND | Assessment is missing, unpublished, or not in your org. | Verify the ID and that it’s published. |
MAX_ATTEMPTS_REACHED | Learner has used all allowed attempts. | Show your own “no attempts left” UI. |
NETWORK_ERROR | Couldn’t reach Edpire. | Retry; check connectivity. |
UNKNOWN | Anything else (e.g. container not found). | Inspect message. |
CDN / no-bundler
For a plain <script> tag (no bundler), use the UMD build’s EdpireSDK global:
<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/@edpire/sdk@0.6.4/dist/umd/index.global.js"></script>
<script>
EdpireSDK.EdpireAssessment.mount({ token: "…", container: "#root", onComplete: handleDone })
</script>
The UMD build is served straight from npm by
jsDelivr (and
unpkg) — no CDN of your own to run. Pin a version (
@0.6.1) for production;
@latest is fine for prototyping.
This is the same pattern used for Mobile & WebView integration.