> ## 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.

# Two ways to ship

> Exam Hall or Arcade. Pick one, follow the steps, ship. Most platforms are live in a day.

Every Edpire integration is one of two shapes. The difference is not what the assessment contains, it is **who draws the screen**.

<CardGroup cols={2}>
  <Card title="Exam Hall" icon="file-lines">
    We draw the screen. You mount one component and the learner sees the whole paper, navigates it, submits it, and gets graded feedback.

    **Ships in a day. Difficulty 3/10.**
  </Card>

  <Card title="Arcade" icon="gamepad">
    You draw the screen. We hand you one question at a time and grade it on demand. Hearts, streaks, timers, pacing, all yours.

    **Ships in a week. Difficulty 6/10.**
  </Card>
</CardGroup>

The same assessment can be delivered either way. Nothing about the content, the authoring, the results or the webhooks changes between them. You can also ship both, in the same app, against the same catalogue. That is what Edulylo does: exams and competitive entrance tests run in Exam Hall, practice exercises run in Arcade.

***

## Which one

Answer one question honestly:

> **Is the shape of the learner experience part of your product, or just the container for someone else's content?**

If a learner would describe your practice mode as a feature of *your* app ("the one with the streaks"), that is Arcade. If they would describe it as "the test", that is Exam Hall.

|                               | Exam Hall                                           | Arcade                             |
| ----------------------------- | --------------------------------------------------- | ---------------------------------- |
| Who builds the UI             | Edpire                                              | You                                |
| Learner stays in your product | Yes                                                 | Yes                                |
| Question types supported      | All, including open response, audio and file upload | All, but you handle media yourself |
| Per question feedback         | Built in                                            | You call `/check` and place it     |
| Navigation, progress, timer   | Built in                                            | You build it                       |
| Grading                       | One call, automatic                                 | One call, automatic                |
| Realistic first ship          | Half a day to a day                                 | Four to eight days                 |

<Note>
  Start with Exam Hall even if Arcade is your end goal. It proves the token flow, the origin
  allow-list and the webhook, which are the parts most likely to surprise you. Arcade then reuses
  all three unchanged.
</Note>

***

## Before either mode

Three things, once. This is the part you cannot do alone, so start it first.

<Steps>
  <Step title="Create your organisation and key">
    Sign up at [edpire.com](https://edpire.com), create your organisation, then open
    **Integrations** and create an API key.

    On the same screen, add every origin your player will be embedded on, including staging
    and local development:

    ```
    https://app.yourplatform.com
    https://staging.yourplatform.com
    http://localhost:3000
    ```

    <Warning>
      Origins are matched **exactly**, scheme and host and port. `https://app.yourplatform.com`
      does not cover `https://www.app.yourplatform.com`. A missing origin is the single most
      common cause of a blank player on launch day, so send the full list up front.
    </Warning>

    Mobile apps in a WebView send no origin, or an opaque one like `capacitor://localhost`.
    Both are handled. You do not need to register anything for native.
  </Step>

  <Step title="Install, or do not">
    On React 18 or 19, install the package:

    ```bash theme={null}
    npm install @edpire/sdk
    ```

    **On anything else, skip this step entirely.** Angular, Vue, Svelte, Rails, Django, plain
    HTML, or React 15, 16 and 17 all use a single script tag instead, with no npm and no build
    step. See [Not on React](#not-on-react) below. The rest of this guide applies unchanged,
    only the mounting syntax differs.

    Styles are bundled and scoped, so the SDK cannot leak CSS into your app and your Tailwind
    version does not matter.
  </Step>

  <Step title="Store one ID per item">
    Add a nullable `edpire_assessment_id` column to whatever table holds your lessons, tests
    or exercises. That UUID is the entire contract between your catalogue and ours.

    Your content team authors in Edpire. List assessments by title in your own admin and store
    the ID against your lesson. See [Find an assessment ID](/developer/find-assessment-id), and
    [catalogue sync](/developer/catalog-sync) for keeping the list current.
  </Step>
</Steps>

***

## Exam Hall

The learner opens your page and sees the complete assessment: every question, a progress
indicator, reading passages in a side panel where the assessment has them, and a submit button.
When they submit, it grades and shows per question feedback without a page load.

### Step 1. Mint tokens on your server

Your API key never touches the browser. Instead your backend issues a short lived token scoped
to one learner and one assessment.

```typescript app/api/edpire/token/route.ts theme={null}
import { createEdpireTokenHandler } from "@edpire/sdk/client"

export const POST = createEdpireTokenHandler({
  apiKey: process.env.EDPIRE_API_KEY!,

  // Resolve the learner from YOUR session, never from the request body.
  resolveLearner: async () => {
    const session = await getSession()
    return session?.user?.id ?? null   // null returns 401
  },
})
```

That is the whole endpoint. It works on any framework via `toNodeHandler()`.

<Warning>
  This endpoint is reachable on its own, so if some of your content is paid or restricted,
  check entitlement **here**, not only on the page that renders the player. Add a
  `resolveAssessmentId` callback that looks the ID up in your own catalogue and throws if the
  learner is not entitled to it. A page level check is UX. This one is the boundary.
</Warning>

### Step 2. Mount the player

```tsx theme={null}
import { EdpireAssessmentPlayer } from "@edpire/sdk/react"

<EdpireAssessmentPlayer
  tokenEndpoint="/api/edpire/token"
  assessmentId={lesson.edpireAssessmentId}
  className="h-screen w-full"
  onComplete={(r) => router.push(`/results/${attemptId}`)}
/>
```

<Warning>
  The player fills its container, so a container with no height collapses to nothing. Always set
  `className` or `style` with a real height. `h-screen`, `h-[600px]`, or a flex parent all work.
</Warning>

Useful props:

| Prop           | Why you want it                                                                                                                                                                                 |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `locale`       | `"en"`, `"fr"` or `"ar"`. RTL layout applies automatically. Set it from the assessment's own language, not your UI language, so a French exercise reads left to right inside an Arabic product. |
| `returnUrl`    | Adds a "View full report" button that navigates here with `?submission_id=` appended. Omit it to hide the button.                                                                               |
| `branding`     | White labelling. Org branding loads automatically, this overrides it.                                                                                                                           |
| `mediaHandler` | **Required** if any question asks for a file upload or an audio or video recording. Without it those questions will not work.                                                                   |
| `onBack`       | Renders a back button before submission.                                                                                                                                                        |

### Step 3. Give the learner somewhere to go

The player grades in place and then stops. Without a next step the learner is stranded on a
finished exam. Two options, and you should use both:

1. **Your own result screen**, navigated to from `onComplete`. This is the primary path, because
   the useful next action belongs to you: retry, next lesson, back to the class.
2. **`returnUrl`** as a fallback, so the built in report button also lands somewhere sensible.

### Step 4. Record the result

See [Results, for both modes](#results-for-both-modes) below.

**Done.** That is Exam Hall in full.

***

## Arcade

The learner sees one question, answers it, gets told immediately whether they were right, and
moves on. You own everything around the question: the progress bar, the lives, the streak
counter, the celebration animation, the pacing.

Edpire gives you two things here. A renderer that turns question content into a working
interactive question, and a grader you can call per question.

### Step 1. Fetch the content on your server

```typescript theme={null}
const res = await fetch(`https://edpire.com/api/v1/assessments/${id}`, {
  headers: { Authorization: `Bearer ${process.env.EDPIRE_API_KEY}` },
  next: { revalidate: 3600 },
})
const { data: assessment } = await res.json()
```

Server side, because the key stays there. Cache it, question content rarely changes.

### Step 2. Flatten it into steps

An assessment nests questions inside exercises. Arcade wants a flat list.

```typescript theme={null}
import { flattenAssessment } from "@edpire/sdk/core"

const steps = flattenAssessment(assessment)
// [{ exerciseId, questionId, content, ... }, ...]
```

<Note>
  Import pure helpers from `@edpire/sdk/core`, not `@edpire/sdk`. The main entry pulls in React
  and the full player tree, which a server file cannot import. `/core` is the same functions with
  none of that.
</Note>

### Step 3. Render one question

```tsx theme={null}
import { EdpireQuestion } from "@edpire/sdk/react"

<EdpireQuestion
  content={steps[i].content}
  onAnswersChange={setAnswers}
  feedback={feedback}          // from /check, pass through as is
  dir="ltr"
/>
```

`EdpireQuestion` is deliberately unstyled beyond what makes the interaction work. It inherits
your fonts and colours, and your CSS reaches into it on purpose. Style it to match your product.

### Step 4. Grade the question

```typescript theme={null}
const res = await fetch(`/api/edpire/check`, {           // your proxy
  method: "POST",
  body: JSON.stringify({
    exercise_id: steps[i].exerciseId,
    question_id: steps[i].questionId,
    answers,                        // straight from onAnswersChange
    session_id: attemptSessionId,   // crypto.randomUUID() once per attempt
    include_correct_answers: true,  // reveal after they commit
  }),
})
const { correct, score, max_score, feedback } = await res.json()
```

Pass `feedback` straight back into `EdpireQuestion` and the right and wrong states appear on the
correct blanks, choices and pairs. You do not have to interpret it.

<Warning>
  `/check` allows **3 checks per question, per session, per rolling hour**. That is an anti brute
  force limit, since `include_correct_answers` would otherwise let a learner guess their way to
  the answer key. Generate a fresh `session_id` per attempt, and design the loop so one check per
  question is the normal case.
</Warning>

Proxy this through your own backend rather than calling Edpire from the browser, and validate
that the assessment is one you actually publish before forwarding. Otherwise any signed in user
can grade against any assessment ID they can guess.

### Step 5. Submit the attempt

Per question checks do not create a submission. When the learner finishes, send the whole
attempt once so it lands in reporting and fires your webhook.

```typescript theme={null}
import { buildSubmitPayload } from "@edpire/sdk/core"

const payload = buildSubmitPayload(assessment, storedAnswers)
await client.submit(assessmentId, { ...payload, learner_ref: userId })
```

### Step 6. Record the result

Same as Exam Hall. Read on.

***

## Results, for both modes

A result reaches you two ways, and you want both.

<Steps>
  <Step title="Webhook, the primary path">
    Register an endpoint and we POST to it when a submission completes. Verify the signature with
    `EDPIRE_WEBHOOK_SECRET`. See [Webhooks](/developer/webhooks).

    Handle it idempotently. Retries are real, and a delivery can arrive twice.
  </Step>

  <Step title="Client reconciliation, the safety net">
    `onComplete` hands you the submission ID the moment grading finishes. Take it, re-fetch the
    submission **server to server**, confirm the `learner_ref` matches the session, and write the
    score.

    This is what saves you when a webhook is delayed or lost. Without it a dropped delivery leaves
    the attempt stuck as pending forever, and the learner sees a blank result screen.
  </Step>
</Steps>

Both paths should funnel into one function, guarded by a check on whether that submission is
already recorded. Then it does not matter which one wins the race.

<Warning>
  Match the attempt by its own ID. Do not "find the most recent pending row for this learner",
  which quietly completes the wrong attempt the moment someone has two open in different tabs.
  We have shipped that bug. It is not fun to find.
</Warning>

If an assessment contains open response questions, the first result is **provisional**. Check
`is_fully_graded` and wait for `submission.grading.completed` before showing a score as final.

***

## Not on React

Both modes work without React, without npm and without a build step. The CDN bundle carries its
own React inside a closure, so your framework and your React version are irrelevant to it. It
does not touch `window.React`, so it cannot collide with a React app that already exists on the
page.

```html theme={null}
<div id="assessment" style="height: 100vh"></div>

<script src="https://cdn.jsdelivr.net/npm/@edpire/sdk@latest/dist/umd/index.global.js"></script>
<script>
  fetch("/api/edpire/token", {                  // your endpoint, same as everyone else's
    method: "POST",
    body: JSON.stringify({ assessmentId: "..." }),
  })
    .then((r) => r.json())
    .then(({ token }) => {
      EdpireSDK.EdpireAssessment.mount({
        token: token,
        container: "#assessment",
        onComplete: function (result) { window.location = "/results"; },
        onError: function (err) { console.error(err.code, err.message); },
      });
    });
</script>
```

`mount()` returns an object with `unmount()`, which is what you call from your framework's
teardown hook: `ngOnDestroy`, `onUnmounted`, `onDestroy`, or a router leave guard.

For Arcade without React, `EdpireSDK.renderQuestion()` is the imperative equivalent of
`<EdpireQuestion>`. It returns an instance with `setContent()`, `setFeedback()` and `unmount()`,
so you drive it from your own loop:

```javascript theme={null}
var q = EdpireSDK.renderQuestion({
  container: "#question",
  content: steps[i].content,
  onAnswersChange: function (answers) { current = answers; },
});

q.setFeedback(checkResult.feedback);   // after POST /check
q.setContent(steps[i + 1].content);    // next question, clears feedback
q.unmount();                           // teardown
```

`EdpireSDK.flattenAssessment` and `EdpireSDK.buildSubmitPayload` are on the same global.

<Warning>
  The bundle is roughly 700 KB gzipped, because it contains React, the maths renderer and the
  rich text engine. That is the price of needing no build step. Load it on the page that runs the
  assessment, not in your global layout, and prefer a pinned version over `@latest` in production
  so a release cannot change under you.
</Warning>

### React version support

| Your React   | What to use                                                                                                                                                 |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 19           | `npm install @edpire/sdk`, import from `@edpire/sdk/react`                                                                                                  |
| 18           | Same. Supported, and the peer range allows it                                                                                                               |
| 17 and older | The script tag above. `createRoot` does not exist before 18, so the npm path cannot work. The bundled React is isolated from yours, so there is no conflict |
| None at all  | The script tag above                                                                                                                                        |

***

## How hard is this, honestly

**Exam Hall: 3 out of 10.**

One server endpoint you can copy verbatim, one component, one webhook handler. A competent
React developer does it in an afternoon. The only genuinely fiddly parts are remembering to size
the container and getting your origins registered before you test.

**Arcade: 6 out of 10.**

The Edpire specific parts are still easy. `flattenAssessment`, `EdpireQuestion` and `/check` are
maybe a day. The other four to seven days are you building a game: state machine, progress,
lives, animations, resume behaviour, what happens when they close the tab mid run. That work is
real, but it is your product, not our API.

What actually consumes the time, in order:

|                                | Typical cost      | Notes                                               |
| ------------------------------ | ----------------- | --------------------------------------------------- |
| Authoring the content          | **Days to weeks** | Almost always the long pole. Start before the code. |
| Arcade UX, if you chose it     | 4 to 7 days       | Your game, your call                                |
| Linking your catalogue to ours | Half a day        | One column, one admin field                         |
| Webhook plus reconciliation    | Half a day        | Do not skip the reconciliation                      |
| Token endpoint                 | 30 minutes        | Copy the snippet                                    |
| Mounting the player            | 30 minutes        | One component                                       |

Being on Angular, Vue or anything else does **not** push the number up. The script tag path is
about as much work as the React one, and arguably less, since there is no install and no build
config. What does push it up: you need file or audio answers, so you must supply a
`mediaHandler` and somewhere to store the files, or you have no public HTTPS endpoint for
webhooks yet, or your backend is not Node, in which case `@edpire/sdk/client` will not run for
you and you call the REST API directly instead. The token endpoint is a single POST, so this is
a small amount of work in any language.

Things that push it **down**: you are on Next.js, your learners already have stable user IDs,
and your content is authored before you start writing code.

***

## Before you go live

* [ ] Every origin registered, including staging and localhost
* [ ] Entitlement checked inside the token endpoint, not only on the page
* [ ] Player container has a real height
* [ ] `mediaHandler` supplied, if any question takes an upload or a recording
* [ ] `locale` set from the assessment's language, not your UI language
* [ ] Webhook signature verified, handler idempotent
* [ ] Client reconciliation matches the attempt by ID
* [ ] Learner has a next step after submitting
* [ ] Open response assessments show provisional scores as provisional
* [ ] Tested with a real learner account, on mobile, on a slow connection
* [ ] If using the script tag, pinned to an exact SDK version rather than `@latest`

***

## Where to go next

<CardGroup cols={2}>
  <Card title="Integration Runbook" href="/developer/platform-runbook" icon="list-check">
    Who does what, in what order, with dates.
  </Card>

  <Card title="Embedded Player" href="/developer/sdk/embedded-player" icon="play">
    Every Exam Hall prop and callback.
  </Card>

  <Card title="Custom Flow" href="/developer/sdk/custom-flow" icon="wand-magic-sparkles">
    Arcade in full detail, with a complete worked example.
  </Card>

  <Card title="Troubleshooting" href="/developer/sdk/troubleshooting" icon="bug">
    Blank player, token errors, styles behaving oddly.
  </Card>
</CardGroup>
