> ## Documentation Index
> Fetch the complete documentation index at: https://docs.edpire.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Embedded Player

> Mount Edpire's full assessment player inside your page with EdpireAssessment.mount().

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
```

<Note>
  **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.
</Note>

<Tip>
  **Fastest start:** scaffold a ready-to-run example instead of wiring this by hand.

  ```bash theme={null}
  # 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.
</Tip>

## 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:**

```typescript theme={null}
// 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:**

```tsx theme={null}
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`](/developer/sdk/react-components#edpireassessmentplayer) 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):**

<Tabs>
  <Tab title="Next.js App Router">
    ```typescript theme={null}
    // 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
      },
    })
    ```
  </Tab>

  <Tab title="Express / Vite dev server">
    ```typescript theme={null}
    import { createEdpireTokenHandler, toNodeHandler } from "@edpire/sdk/client"

    const handler = toNodeHandler(createEdpireTokenHandler({
      apiKey: process.env.EDPIRE_API_KEY!,
      // `req` is a Web-standard Request rebuilt from the Node request, so Express
      // extras like `req.session` and `req.cookies` are NOT available here. Read
      // from headers and do your own session lookup.
      resolveLearner: async (req) => {
        const cookie = req.headers.get("cookie") ?? ""
        return await getUserIdFromCookie(cookie)   // your own session lookup
      },
    }))

    // Express — mount BEFORE any body parser (express.json() etc.). The handler
    // reads the request stream itself; a parser that already drained it makes this hang.
    app.post("/api/edpire/token", handler)

    // Vite configureServer (dev only — use Express for production):
    server.middlewares.use("/api/edpire/token", handler)
    ```
  </Tab>

  <Tab title="Manual (any runtime)">
    ```typescript theme={null}
    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 })
    ```
  </Tab>
</Tabs>

<Warning>
  **Security — never trust the client for the learner ID.** The token endpoint should
  resolve the learner from your server session (`getServerSession()`, a cookie lookup, a verified
  JWT). If you read `userId` from the request body, anyone can mint tokens for other learners.
  `createEdpireTokenHandler` enforces this via the `resolveLearner` callback.
</Warning>

<Info>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.</Info>

**2. Mount the player (browser):**

```typescript theme={null}
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.

```css theme={null}
/* Minimal example — full viewport */
#assessment-root {
  width: 100%;
  height: 100vh;
}
```

```tsx theme={null}
/* React — use className or style prop */
<EdpireAssessmentPlayer style={{ width: "100%", height: "100vh" }} ... />
```

<Warning>
  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.
</Warning>

***

## Environment variables

```bash theme={null}
# .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 → Integrations → Security tab → Allowed Embed 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.

<Tip>
  Not sure if it's an origin error? Add a temporary `onError` handler to log the code:

  ```typescript theme={null}
  onError: (e) => console.error("[edpire]", e.code, e.message)
  ```

  `ORIGIN_NOT_ALLOWED` means the allow-list is missing your domain.
</Tip>

***

## `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](/developer/sdk/branding-theming). |
| `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](/developer/sdk/branding-theming#grading-overlay).                                     |
| `mediaHandler`  | `QuestionRuntimeProps["mediaHandler"]` | No       | **Required** if the assessment has OpenResponse questions with file-upload or recording modes. See [mediaHandler](#mediahandler) below.                                |
| `onComplete`    | `(result: EmbedResult) => void`        | No       | Called when the learner successfully submits.                                                                                                                          |
| `onError`       | `(error: EmbedError) => void`          | No       | Called 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.

```typescript theme={null}
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.
     *
     * @param file  The selected File
     * @param type  Media kind, e.g. "image" | "audio" | "video" | "file"
     */
    upload: async (file: File, type: string) => {
      const form = new FormData()
      form.append("file", file)
      form.append("type", type)

      // Your own upload endpoint, backed by S3, R2, etc.
      const res = await fetch("/api/upload", { method: "POST", body: form })
      if (!res.ok) throw new Error("Upload failed")

      const { url, mimeType } = await res.json()
      return { url, mimeType }
    },
  },
})
```

<Note>
  `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.
</Note>

## Return value — `EmbedInstance`

```typescript theme={null}
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.

```typescript theme={null}
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 }>
            }
      }>
    }>
  }>
}
```

<Note>
  If `awaiting_manual_grading` is `true`, the visible score is provisional — open-response answers still need a teacher. Listen for the `submission.graded` [webhook](/developer/webhooks) to learn the final score.
</Note>

## `EmbedError`

Passed to `onError`.

```typescript theme={null}
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 2 hours.                             | Mint a new token.                                                                                        |
| `TOKEN_USED`           | Token was already consumed by a submission.              | Mint a new token per open.                                                                               |
| `ORIGIN_NOT_ALLOWED`   | The page origin isn't in your **Allowed Embed Origins**. | Add it under Integrations → Security. Note this is a different list from Allowed API & Redirect Origins. |
| `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:

```html theme={null}
<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/@edpire/sdk@0.6.5/dist/umd/index.global.js"></script>
<script>
  EdpireSDK.EdpireAssessment.mount({ token: "…", container: "#root", onComplete: handleDone })
</script>
```

<Tip>The UMD build is served straight from npm by [jsDelivr](https://www.jsdelivr.com/) (and [unpkg](https://unpkg.com/)) — no CDN of your own to run. Pin an exact version (`@0.6.5`) for production; `@latest` is fine for prototyping. The SDK is pre-1.0, so minor releases can contain breaking changes.</Tip>

This is the same pattern used for [Mobile & WebView](/developer/mobile) integration.
