@edpire/sdk
v0.6.4
Published
Embeddable JS SDK for Edpire assessments
Readme
@edpire/sdk
Embeddable assessment SDK for Edpire. Deliver, render, and grade assessments from your own application.
📚 Full documentation lives in the Edpire docs site under SDK Reference — complete option/prop/type tables for the Embedded Player, React components, Custom Flow, server client, and branding. This README is a condensed quick-start.
Which approach fits you?
| Approach | Learner stays on your page? | You build the UI? | Entry point |
|---|---|---|---|
| Hosted Redirect | No | No | Share URL only |
| Embedded Player | Yes | No | EdpireAssessment.mount() |
| Custom Flow | Yes | Yes | renderQuestion + flattenAssessment |
Server Client (
EdpireClient) is a backend utility used alongside any of the above — not a separate approach. Jump to Server Client →
Installation
Published to the public npm registry — no auth token required.
npm install @edpire/sdk
# or
pnpm add @edpire/sdkLicense: the package is free to install but carries a proprietary, integrate-with-Edpire-only license (see
LICENSE). It is safe to make public because the bundle is only the question renderer — answer keys, grading, and API keys never ship in it; they stay on the server.
Hosted Redirect
The simplest integration — no SDK required on the frontend. Redirect learners to Edpire's hosted assessment UI. After completion, Edpire redirects back to your return_url with results.
https://{your-slug}.edpire.com/take/{shareCode}?learner_ref={userId}&return_url={yourUrl}| Parameter | Required | Description |
|-----------|----------|-------------|
| shareCode | Yes | Assessment share code from the dashboard |
| learner_ref | Recommended | Your stable internal user ID |
| return_url | Recommended | Where to send the learner after completion |
Edpire appends ?submission_id=...&score=...&max_score=... to the return URL.
When to use: Fast adoption, no custom UI needed, comfortable with a browser redirect.
Embedded Player
Mount Edpire's full assessment player inline inside your page. The SDK fetches the assessment, handles all interactions, grades the submission, and shows per-question feedback — all automatically. The learner never leaves your app.
Server (Node.js / API route):
import { EdpireClient } from "@edpire/sdk/client"
const client = new EdpireClient({ apiKey: process.env.EDPIRE_API_KEY! })
// Mint a short-lived single-use token for this learner + assessment
const { token } = await client.mintEmbedToken(assessmentId, userId)
// Pass `token` to the browser (e.g. via server-rendered props or an API response)Browser:
import { EdpireAssessment } from "@edpire/sdk"
const embed = EdpireAssessment.mount({
token, // from your server
container: "#assessment-root", // CSS selector or HTMLElement
onComplete: (result) => {
console.log(`Score: ${result.score}/${result.max_score}`)
console.log(`Passed: ${result.passed}`)
},
onError: (err) => console.error(err.message),
})
// Later, when done:
embed.unmount()CDN (no bundler):
<script src="https://cdn.jsdelivr.net/npm/@edpire/[email protected]/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 to run yourself. Pin a version for production;
@latestworks for prototyping.
When to use: You want the full assessment experience inside your page with a single function call. Zero UI code required.
Custom Flow
Build your own question-by-question UX — Duolingo-style flows, flashcard drills, practice modes, custom navigation. The SDK handles question rendering and answer collection; you own everything else.
How it works:
- Your server fetches the assessment (keeping your API key server-side)
flattenAssessment()converts it into a flat array of question stepsrenderQuestion()(or<EdpireQuestion>in React) renders each step into a container you control- Your server grades each answer via the
/checkendpoint - After all questions, your server submits the full attempt
Server — fetch the assessment:
// GET /api/edpire/assessment/[id]/route.ts
import { EdpireClient } from "@edpire/sdk/client"
const client = new EdpireClient({ apiKey: process.env.EDPIRE_API_KEY! })
const assessment = await client.getAssessment(assessmentId)
return NextResponse.json({ data: assessment })Browser — flatten and render:
// React
import { flattenAssessment } from "@edpire/sdk"
import { EdpireQuestion } from "@edpire/sdk/react"
import type { RuntimeAnswer } from "@edpire/sdk"
const steps = flattenAssessment(assessment)
// steps[i] = { exerciseId, questionId, content, points, sequenceNumber, index }
const [currentIndex, setCurrentIndex] = useState(0)
const [answers, setAnswers] = useState<RuntimeAnswer[]>([])
const [feedback, setFeedback] = useState(null)
const step = steps[currentIndex]
<EdpireQuestion
content={step.content}
onAnswersChange={setAnswers}
feedback={feedback} // null = no feedback shown; pass result of /check to show it
dir="ltr" // or "rtl" for Arabic
/>// Vanilla JS
import { flattenAssessment, renderQuestion } from "@edpire/sdk"
const steps = flattenAssessment(assessment)
const step = steps[currentIndex]
const question = renderQuestion({
container: document.getElementById("question-root"),
content: step.content,
onAnswersChange: (answers) => { /* store answers */ },
})
// Later, update feedback:
question.setFeedback(feedbackFromCheckEndpoint)
// Move to next question:
question.setContent(steps[nextIndex].content)Server — grade each question via your proxy:
// POST /api/edpire/check/[id]/route.ts (recommended — type-safe, handles URL construction)
import { EdpireClient } from "@edpire/sdk/client"
const client = new EdpireClient({ apiKey: process.env.EDPIRE_API_KEY! })
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const body = await req.json()
// Always set include_correct_answers server-side — never trust the client to send it
const result = await client.checkQuestion(id, { ...body, include_correct_answers: true })
return Response.json(result) // result: CheckResult = { correct, score, max_score, feedback }
}// Note: assessmentId goes in the URL path, not the request body
const result = await fetch(`https://edpire.com/api/v1/assessments/${assessmentId}/check`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.EDPIRE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
exercise_id: exerciseId,
question_id: questionId,
answers,
learner_ref: userId,
session_id: sessionId,
include_correct_answers: true,
}),
}).then((r) => r.json())
// Response envelope: { data: CheckResult, error: null, meta: null }Server — submit the full attempt when done:
import { EdpireClient } from "@edpire/sdk/client"
import type { StoredAnswer } from "@edpire/sdk"
const stored: StoredAnswer[] = []
// push { exerciseId, questionId, answers } for each step as the learner answers
const client = new EdpireClient({ apiKey: process.env.EDPIRE_API_KEY! })
const result = await client.submit(assessmentId, {
learner_ref: userId,
answers: stored, // StoredAnswer[] — the client handles nesting automatically
})
console.log(`Final score: ${result.score}/${result.max_score}`)Each (session_id, question_id) pair is limited to 3 checks per hour (configurable per org).
Always pass a session_id — generate a UUID once at the start of each attempt:
const sessionId = crypto.randomUUID() // generate once per attempt, not per questionIf omitted, rate limiting falls back to learner_ref, which accumulates across all of a learner's attempts and can cause unexpected 429s on repeat attempts.
Required API key scopes: read:assessments (for fetching and checking) + write:submissions (for the final submit).
When to use: You want full control over pacing, navigation, animations, or scoring UX. You're building a Duolingo-style flow, a timed drill, a practice mode, or any experience that goes beyond a linear "answer all → submit" form.
Server Client
EdpireClient is a backend-only utility for calling the Edpire REST API from your server. It is not a rendering pattern — it is used alongside Embedded Player and Custom Flow.
import { EdpireClient } from "@edpire/sdk/client"
const client = new EdpireClient({
apiKey: process.env.EDPIRE_API_KEY!,
baseUrl: "https://edpire.com", // optional, defaults to edpire.com
})Common operations:
// List published assessments
const { items } = await client.getAssessments({ status: "published" })
// Fetch a single assessment with full content
const assessment = await client.getAssessment(assessmentId)
// Mint an embed token (for Embedded Player)
const { token } = await client.mintEmbedToken(assessmentId, learnerRef)
// Submit a full attempt (for Custom Flow)
const result = await client.submit(assessmentId, { learner_ref: userId, answers: stored })
// Grade a single question without recording (for Custom Flow /check)
const check = await client.checkQuestion(assessmentId, {
exercise_id, question_id, answers, learner_ref, session_id,
include_correct_answers: true,
})
// Register a webhook
const webhook = await client.registerWebhook("https://yourapp.com/webhooks/edpire", [
"submission.graded",
])
// Store webhook.secret securely — shown only onceMobile & WebView integration
Flutter, React Native, and native iOS/Android apps cannot import this npm package directly. The integration uses two parts:
- Your backend mints an embed token via
client.mintEmbedToken()(the API key stays server-side). - A WebView in your mobile app loads a minimal HTML page that calls the CDN UMD build with the token.
The UMD build is served from npm via jsDelivr — no CDN to host yourself.
Universal WebView HTML template:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>* { margin: 0; padding: 0; box-sizing: border-box; } #root { min-height: 100vh; }</style>
</head>
<body>
<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/@edpire/[email protected]/dist/umd/index.global.js"></script>
<script>
EdpireSDK.EdpireAssessment.mount({
token: "{{EMBED_TOKEN}}", // replace server-side before loading
container: "#root",
onComplete: function(r) {
if (window.ReactNativeWebView)
window.ReactNativeWebView.postMessage(JSON.stringify({ type: "complete", result: r }))
if (window.flutter_inappwebview)
window.flutter_inappwebview.callHandler("onComplete", r)
},
onError: function(e) {
if (window.ReactNativeWebView)
window.ReactNativeWebView.postMessage(JSON.stringify({ type: "error", error: e }))
if (window.flutter_inappwebview)
window.flutter_inappwebview.callHandler("onError", e)
}
})
</script>
</body>
</html>Ionic / Capacitor apps run in a WebView — you can use the full npm SDK directly. No HTML template needed.
For React Native, Flutter, and native iOS/Android code examples, see the Mobile Integration guide.
CSS
Embedded Player (EdpireAssessment.mount()) — all CSS is injected automatically. No imports needed.
Custom Flow (EdpireQuestion / renderQuestion) — styles are bundled into the SDK's JavaScript and injected when the component first renders. In most setups this is also automatic. If styles appear missing (e.g. in a sandboxed iframe or a strict CSP environment that blocks inline styles), import the utility stylesheet explicitly:
import "@edpire/sdk/styles/runtime-utilities.css"TypeScript types
All commonly needed types are available from the main entry:
import type {
// Answer types
RuntimeAnswer,
ChoiceSetAnswer,
BlankAnswer,
TypedBlankAnswer,
BlankChoiceAnswer,
MatchingSetAnswer,
AiAnswer,
OpenResponseAnswer,
MathResponseAnswer,
// Custom Flow helpers
StoredAnswer, // { exerciseId, questionId, answers: RuntimeAnswer[] }
FlatStep, // one step from flattenAssessment()
// Embedded Player
MountOptions,
EmbedInstance,
EmbedResult,
EmbedError,
} from "@edpire/sdk"Types specific to the Server Client come from @edpire/sdk/client:
import type { Assessment, SubmitResult, CheckResult, EdpireClientOptions } from "@edpire/sdk/client"Webhooks
const webhook = await client.registerWebhook("https://yourapp.com/api/webhooks/edpire", [
"submission.graded",
"assessment.published",
])
// Store webhook.secret securely — it is only shown once.Verify incoming signatures:
import { createHmac, timingSafeEqual } from "crypto"
export async function POST(req: Request) {
const rawBody = await req.text()
const signature = req.headers.get("x-edpire-signature") ?? ""
const expected = "sha256=" + createHmac("sha256", process.env.EDPIRE_WEBHOOK_SECRET!)
.update(rawBody)
.digest("hex")
if (!timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return new Response("Unauthorized", { status: 401 })
}
const event = JSON.parse(rawBody)
// event.event, event.submission_id, event.score, event.passed, ...
}Available events: submission.graded, submission.pending_review, assessment.published, assessment.archived
Error handling
EdpireClient throws EdpireError on non-2xx responses:
import { EdpireError } from "@edpire/sdk/client"
try {
await client.submit(assessmentId, options)
} catch (err) {
if (err instanceof EdpireError) {
console.error(err.status, err.message)
}
}Common status codes: 400 bad request · 401 invalid API key · 404 not found · 409 max attempts reached · 422 grading failed · 429 rate limit exceeded
