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 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.
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
  },
})
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:
  1. Go to edpire.com → Settings → Integrations → Allowed Origins
  2. Add every origin your app runs on:
    • http://localhost:3000 (Next.js dev)
    • http://localhost:5173 (Vite dev)
    • https://yourapp.com (production)
  3. 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

OptionTypeRequiredDescription
tokenstringYesEmbed token from client.mintEmbedToken(). Pass from your server — never hardcode.
containerstring | HTMLElementYesCSS selector or DOM element to mount into.
baseUrlstringNoEdpire instance URL. Defaults to https://edpire.com. Override for staging/self-hosted.
locale"en" | "fr" | "ar"NoUI language. Defaults to "en". RTL layout is applied automatically for "ar".
brandingTakeBrandingNoOverride the org’s logo/colors/fonts (white-labelling). By default the assessment’s org branding loads automatically. See Branding.
returnUrlstringNoWhen set, the post-submit “View Full Report” button redirects here with ?submission_id=…&score=…&max_score=… appended. Omit to hide the button.
reportLabelstringNoCustom label for the report/redirect button.
onBack() => voidNoCalled 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.
backLabelstringNoCustom label for the back button.
overlayConfigOverlayConfigNoCustomise the grading overlay (spinner theme, colors, animation). See Branding.
mediaHandlerQuestionRuntimeProps["mediaHandler"]NoRequired if the assessment has OpenResponse questions with file-upload or recording modes. See mediaHandler below.
onComplete(result: EmbedResult) => voidNoCalled when the learner successfully submits.
onError(error: EmbedError) => voidNoCalled on a non-recoverable error.

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.
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
}
CodeMeaningHow to recover
TOKEN_INVALIDToken is malformed or unrecognized.Mint a new token server-side.
TOKEN_EXPIREDToken is older than 1 hour.Mint a new token.
TOKEN_USEDToken was already redeemed (single-use).Mint a new token per open.
ORIGIN_NOT_ALLOWEDThe page origin isn’t in your Allowed Origins.Add the origin in org/integrations.
ASSESSMENT_NOT_FOUNDAssessment is missing, unpublished, or not in your org.Verify the ID and that it’s published.
MAX_ATTEMPTS_REACHEDLearner has used all allowed attempts.Show your own “no attempts left” UI.
NETWORK_ERRORCouldn’t reach Edpire.Retry; check connectivity.
UNKNOWNAnything 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.