@edpire/sdk 0.6.4 โ 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +445 -443
- package/bin/create.js +0 -0
- package/dist/chunk-76CZ6Z7T.mjs +1 -0
- package/dist/client.d.mts +44 -3
- package/dist/client.mjs +15 -5
- package/dist/core.d.mts +149 -0
- package/dist/core.mjs +1 -0
- package/dist/dist-UCCG7PHU.mjs +11 -0
- package/dist/esm-RQP7AIMT.mjs +136 -0
- package/dist/index.d.mts +8 -1
- package/dist/index.mjs +52 -7
- package/dist/react.d.mts +9 -1
- package/dist/react.mjs +52 -7
- package/dist/sql-wasm-browser-NICPVFVD.mjs +2 -0
- package/dist/umd/index.global.js +454 -251
- package/examples/nextjs/README.md +66 -66
- package/examples/vite-express/README.md +69 -69
- package/examples/vite-express/server.ts +96 -90
- package/examples/vite-express/src/App.tsx +56 -56
- package/package.json +17 -11
- package/src/styles/runtime-utilities.css +1 -1
- package/src/styles/shell.css +1 -1
package/README.md
CHANGED
|
@@ -1,443 +1,445 @@
|
|
|
1
|
-
# @edpire/sdk
|
|
2
|
-
|
|
3
|
-
Embeddable assessment SDK for Edpire. Deliver, render, and grade assessments from your own application.
|
|
4
|
-
|
|
5
|
-
> ๐ **Full documentation** lives in the Edpire docs site under [**SDK Reference**](https://docs.edpire.com/developer/sdk/overview) โ complete option/prop/type tables for the Embedded Player, React components, Custom Flow, server client, and branding. This README is a condensed quick-start.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Which approach fits you?
|
|
10
|
-
|
|
11
|
-
| Approach | Learner stays on your page? | You build the UI? | Entry point |
|
|
12
|
-
|---|---|---|---|
|
|
13
|
-
| **[Hosted Redirect](#hosted-redirect)** | No | No | Share URL only |
|
|
14
|
-
| **[Embedded Player](#embedded-player)** | Yes | No | `EdpireAssessment.mount()` |
|
|
15
|
-
| **[Custom Flow](#custom-flow)** | Yes | Yes | `renderQuestion` + `flattenAssessment` |
|
|
16
|
-
|
|
17
|
-
> **Server Client** (`EdpireClient`) is a backend utility used alongside any of the above โ not a separate approach. [Jump to Server Client โ](#server-client)
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
## Installation
|
|
22
|
-
|
|
23
|
-
Published to the public npm registry โ no auth token required.
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
npm install @edpire/sdk
|
|
27
|
-
# or
|
|
28
|
-
pnpm add @edpire/sdk
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
> **License:** 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.
|
|
32
|
-
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
## Hosted Redirect
|
|
36
|
-
|
|
37
|
-
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.
|
|
38
|
-
|
|
39
|
-
```
|
|
40
|
-
https://{your-slug}.edpire.com/take/{shareCode}?learner_ref={userId}&return_url={yourUrl}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
| Parameter | Required | Description |
|
|
44
|
-
|-----------|----------|-------------|
|
|
45
|
-
| `shareCode` | Yes | Assessment share code from the dashboard |
|
|
46
|
-
| `learner_ref` | Recommended | Your stable internal user ID |
|
|
47
|
-
| `return_url` | Recommended | Where to send the learner after completion |
|
|
48
|
-
|
|
49
|
-
Edpire appends `?submission_id=...&score=...&max_score=...` to the return URL.
|
|
50
|
-
|
|
51
|
-
**When to use:** Fast adoption, no custom UI needed, comfortable with a browser redirect.
|
|
52
|
-
|
|
53
|
-
---
|
|
54
|
-
|
|
55
|
-
## Embedded Player
|
|
56
|
-
|
|
57
|
-
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.
|
|
58
|
-
|
|
59
|
-
**Server (Node.js / API route):**
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
import { EdpireClient } from "@edpire/sdk/client"
|
|
63
|
-
|
|
64
|
-
const client = new EdpireClient({ apiKey: process.env.EDPIRE_API_KEY! })
|
|
65
|
-
|
|
66
|
-
// Mint a short-lived single-use token for this learner + assessment
|
|
67
|
-
const { token } = await client.mintEmbedToken(assessmentId, userId)
|
|
68
|
-
// Pass `token` to the browser (e.g. via server-rendered props or an API response)
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
**Browser:**
|
|
72
|
-
|
|
73
|
-
```typescript
|
|
74
|
-
import { EdpireAssessment } from "@edpire/sdk"
|
|
75
|
-
|
|
76
|
-
const embed = EdpireAssessment.mount({
|
|
77
|
-
token, // from your server
|
|
78
|
-
container: "#assessment-root", // CSS selector or HTMLElement
|
|
79
|
-
onComplete: (result) => {
|
|
80
|
-
console.log(`Score: ${result.score}/${result.max_score}`)
|
|
81
|
-
console.log(`Passed: ${result.passed}`)
|
|
82
|
-
},
|
|
83
|
-
onError: (err) => console.error(err.message),
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
// Later, when done:
|
|
87
|
-
embed.unmount()
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
**CDN (no bundler):**
|
|
91
|
-
|
|
92
|
-
```html
|
|
93
|
-
<script src="https://cdn.jsdelivr.net/npm/@edpire/sdk@0.
|
|
94
|
-
<script>
|
|
95
|
-
EdpireSDK.EdpireAssessment.mount({ token, container: "#root", onComplete: handleDone })
|
|
96
|
-
</script>
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
> The UMD build is served straight from npm by [jsDelivr](https://www.jsdelivr.com/) (and [unpkg](https://unpkg.com/)) โ no CDN to run yourself. Pin a version for production; `@latest` works for prototyping.
|
|
100
|
-
|
|
101
|
-
**When to use:** You want the full assessment experience inside your page with a single function call. Zero UI code required.
|
|
102
|
-
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
## Custom Flow
|
|
106
|
-
|
|
107
|
-
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.
|
|
108
|
-
|
|
109
|
-
**How it works:**
|
|
110
|
-
1. Your server fetches the assessment (keeping your API key server-side)
|
|
111
|
-
2. `flattenAssessment()` converts it into a flat array of question steps
|
|
112
|
-
3. `renderQuestion()` (or `<EdpireQuestion>` in React) renders each step into a container you control
|
|
113
|
-
4. Your server grades each answer via the `/check` endpoint
|
|
114
|
-
5. After all questions, your server submits the full attempt
|
|
115
|
-
|
|
116
|
-
**Server โ fetch the assessment:**
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
119
|
-
// GET /api/edpire/assessment/[id]/route.ts
|
|
120
|
-
import { EdpireClient } from "@edpire/sdk/client"
|
|
121
|
-
|
|
122
|
-
const client = new EdpireClient({ apiKey: process.env.EDPIRE_API_KEY! })
|
|
123
|
-
const assessment = await client.getAssessment(assessmentId)
|
|
124
|
-
return NextResponse.json({ data: assessment })
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
**Browser โ flatten and render:**
|
|
128
|
-
|
|
129
|
-
```tsx
|
|
130
|
-
// React
|
|
131
|
-
import { flattenAssessment } from "@edpire/sdk"
|
|
132
|
-
import { EdpireQuestion } from "@edpire/sdk/react"
|
|
133
|
-
import type { RuntimeAnswer } from "@edpire/sdk"
|
|
134
|
-
|
|
135
|
-
const steps = flattenAssessment(assessment)
|
|
136
|
-
// steps[i] = { exerciseId, questionId, content, points, sequenceNumber, index }
|
|
137
|
-
|
|
138
|
-
const [currentIndex, setCurrentIndex] = useState(0)
|
|
139
|
-
const [answers, setAnswers] = useState<RuntimeAnswer[]>([])
|
|
140
|
-
const [feedback, setFeedback] = useState(null)
|
|
141
|
-
const step = steps[currentIndex]
|
|
142
|
-
|
|
143
|
-
<EdpireQuestion
|
|
144
|
-
content={step.content}
|
|
145
|
-
onAnswersChange={setAnswers}
|
|
146
|
-
feedback={feedback} // null = no feedback shown; pass result of /check to show it
|
|
147
|
-
dir="ltr" // or "rtl" for Arabic
|
|
148
|
-
/>
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
```typescript
|
|
152
|
-
// Vanilla JS
|
|
153
|
-
import { flattenAssessment, renderQuestion } from "@edpire/sdk"
|
|
154
|
-
|
|
155
|
-
const steps = flattenAssessment(assessment)
|
|
156
|
-
const step = steps[currentIndex]
|
|
157
|
-
|
|
158
|
-
const question = renderQuestion({
|
|
159
|
-
container: document.getElementById("question-root"),
|
|
160
|
-
content: step.content,
|
|
161
|
-
onAnswersChange: (answers) => { /* store answers */ },
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
// Later, update feedback:
|
|
165
|
-
question.setFeedback(feedbackFromCheckEndpoint)
|
|
166
|
-
|
|
167
|
-
// Move to next question:
|
|
168
|
-
question.setContent(steps[nextIndex].content)
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
**Server โ grade each question via your proxy:**
|
|
172
|
-
|
|
173
|
-
```typescript
|
|
174
|
-
// POST /api/edpire/check/[id]/route.ts (recommended โ type-safe, handles URL construction)
|
|
175
|
-
import { EdpireClient } from "@edpire/sdk/client"
|
|
176
|
-
|
|
177
|
-
const client = new EdpireClient({ apiKey: process.env.EDPIRE_API_KEY! })
|
|
178
|
-
|
|
179
|
-
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
180
|
-
const { id } = await params
|
|
181
|
-
const body = await req.json()
|
|
182
|
-
// Always set include_correct_answers server-side โ never trust the client to send it
|
|
183
|
-
const result = await client.checkQuestion(id, { ...body, include_correct_answers: true })
|
|
184
|
-
return Response.json(result) // result: CheckResult = { correct, score, max_score, feedback }
|
|
185
|
-
}
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
<details>
|
|
189
|
-
<summary>Alternative โ raw fetch without EdpireClient</summary>
|
|
190
|
-
|
|
191
|
-
```typescript
|
|
192
|
-
// Note: assessmentId goes in the URL path, not the request body
|
|
193
|
-
const result = await fetch(`https://edpire.com/api/v1/assessments/${assessmentId}/check`, {
|
|
194
|
-
method: "POST",
|
|
195
|
-
headers: {
|
|
196
|
-
Authorization: `Bearer ${process.env.EDPIRE_API_KEY}`,
|
|
197
|
-
"Content-Type": "application/json",
|
|
198
|
-
},
|
|
199
|
-
body: JSON.stringify({
|
|
200
|
-
exercise_id: exerciseId,
|
|
201
|
-
question_id: questionId,
|
|
202
|
-
answers,
|
|
203
|
-
learner_ref: userId,
|
|
204
|
-
session_id: sessionId,
|
|
205
|
-
include_correct_answers: true,
|
|
206
|
-
}),
|
|
207
|
-
}).then((r) => r.json())
|
|
208
|
-
// Response envelope: { data: CheckResult, error: null, meta: null }
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
</details>
|
|
212
|
-
|
|
213
|
-
**Server โ submit the full attempt when done:**
|
|
214
|
-
|
|
215
|
-
```typescript
|
|
216
|
-
import { EdpireClient } from "@edpire/sdk/client"
|
|
217
|
-
import type { StoredAnswer } from "@edpire/sdk"
|
|
218
|
-
|
|
219
|
-
const stored: StoredAnswer[] = []
|
|
220
|
-
// push { exerciseId, questionId, answers } for each step as the learner answers
|
|
221
|
-
|
|
222
|
-
const client = new EdpireClient({ apiKey: process.env.EDPIRE_API_KEY! })
|
|
223
|
-
const result = await client.submit(assessmentId, {
|
|
224
|
-
learner_ref: userId,
|
|
225
|
-
answers: stored, // StoredAnswer[] โ the client handles nesting automatically
|
|
226
|
-
})
|
|
227
|
-
console.log(`Final score: ${result.score}/${result.max_score}`)
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
<details>
|
|
231
|
-
<summary>Rate limiting on /check</summary>
|
|
232
|
-
|
|
233
|
-
Each `(session_id, question_id)` pair is limited to 3 checks per hour (configurable per org).
|
|
234
|
-
|
|
235
|
-
**Always pass a `session_id`** โ generate a UUID once at the start of each attempt:
|
|
236
|
-
```typescript
|
|
237
|
-
const sessionId = crypto.randomUUID() // generate once per attempt, not per question
|
|
238
|
-
```
|
|
239
|
-
If 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.
|
|
240
|
-
|
|
241
|
-
</details>
|
|
242
|
-
|
|
243
|
-
**Required API key scopes:** `read:assessments` (for fetching and checking) + `write:submissions` (for the final submit).
|
|
244
|
-
|
|
245
|
-
**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.
|
|
246
|
-
|
|
247
|
-
---
|
|
248
|
-
|
|
249
|
-
## Server Client
|
|
250
|
-
|
|
251
|
-
`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.
|
|
252
|
-
|
|
253
|
-
```typescript
|
|
254
|
-
import { EdpireClient } from "@edpire/sdk/client"
|
|
255
|
-
|
|
256
|
-
const client = new EdpireClient({
|
|
257
|
-
apiKey: process.env.EDPIRE_API_KEY!,
|
|
258
|
-
baseUrl: "https://edpire.com", // optional, defaults to edpire.com
|
|
259
|
-
})
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
**Common operations:**
|
|
263
|
-
|
|
264
|
-
```typescript
|
|
265
|
-
// List published assessments
|
|
266
|
-
const { items } = await client.getAssessments({ status: "published" })
|
|
267
|
-
|
|
268
|
-
// Fetch a single assessment with full content
|
|
269
|
-
const assessment = await client.getAssessment(assessmentId)
|
|
270
|
-
|
|
271
|
-
// Mint an embed token (for Embedded Player)
|
|
272
|
-
const { token } = await client.mintEmbedToken(assessmentId, learnerRef)
|
|
273
|
-
|
|
274
|
-
// Submit a full attempt (for Custom Flow)
|
|
275
|
-
const result = await client.submit(assessmentId, { learner_ref: userId, answers: stored })
|
|
276
|
-
|
|
277
|
-
// Grade a single question without recording (for Custom Flow /check)
|
|
278
|
-
const check = await client.checkQuestion(assessmentId, {
|
|
279
|
-
exercise_id, question_id, answers, learner_ref, session_id,
|
|
280
|
-
include_correct_answers: true,
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
// Register a webhook
|
|
284
|
-
const webhook = await client.registerWebhook("https://yourapp.com/webhooks/edpire", [
|
|
285
|
-
"submission.graded",
|
|
286
|
-
])
|
|
287
|
-
// Store webhook.secret securely โ shown only once
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
---
|
|
291
|
-
|
|
292
|
-
## Mobile & WebView integration
|
|
293
|
-
|
|
294
|
-
Flutter, React Native, and native iOS/Android apps cannot import this npm package directly.
|
|
295
|
-
The integration uses two parts:
|
|
296
|
-
|
|
297
|
-
1. **Your backend** mints an embed token via `client.mintEmbedToken()` (the API key stays server-side).
|
|
298
|
-
2. **A WebView** in your mobile app loads a minimal HTML page that calls the CDN UMD build with the token.
|
|
299
|
-
|
|
300
|
-
> The UMD build is served from npm via [jsDelivr](https://www.jsdelivr.com/) โ no CDN to host yourself.
|
|
301
|
-
|
|
302
|
-
**Universal WebView HTML template:**
|
|
303
|
-
|
|
304
|
-
```html
|
|
305
|
-
<!DOCTYPE html>
|
|
306
|
-
<html>
|
|
307
|
-
<head>
|
|
308
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
309
|
-
<style>* { margin: 0; padding: 0; box-sizing: border-box; } #root { min-height: 100vh; }</style>
|
|
310
|
-
</head>
|
|
311
|
-
<body>
|
|
312
|
-
<div id="root"></div>
|
|
313
|
-
<script src="https://cdn.jsdelivr.net/npm/@edpire/sdk@0.
|
|
314
|
-
<script>
|
|
315
|
-
EdpireSDK.EdpireAssessment.mount({
|
|
316
|
-
token: "{{EMBED_TOKEN}}", // replace server-side before loading
|
|
317
|
-
container: "#root",
|
|
318
|
-
onComplete: function(r) {
|
|
319
|
-
if (window.ReactNativeWebView)
|
|
320
|
-
window.ReactNativeWebView.postMessage(JSON.stringify({ type: "complete", result: r }))
|
|
321
|
-
if (window.flutter_inappwebview)
|
|
322
|
-
window.flutter_inappwebview.callHandler("onComplete", r)
|
|
323
|
-
},
|
|
324
|
-
onError: function(e) {
|
|
325
|
-
if (window.ReactNativeWebView)
|
|
326
|
-
window.ReactNativeWebView.postMessage(JSON.stringify({ type: "error", error: e }))
|
|
327
|
-
if (window.flutter_inappwebview)
|
|
328
|
-
window.flutter_inappwebview.callHandler("onError", e)
|
|
329
|
-
}
|
|
330
|
-
})
|
|
331
|
-
</script>
|
|
332
|
-
</body>
|
|
333
|
-
</html>
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
> **Ionic / Capacitor** apps run in a WebView โ you can use the full npm SDK directly. No HTML template needed.
|
|
337
|
-
|
|
338
|
-
For React Native, Flutter, and native iOS/Android code examples, see the [Mobile Integration guide](https://docs.edpire.com/developer/mobile).
|
|
339
|
-
|
|
340
|
-
---
|
|
341
|
-
|
|
342
|
-
## CSS
|
|
343
|
-
|
|
344
|
-
**Embedded Player** (`EdpireAssessment.mount()`) โ all CSS is injected automatically. No imports needed.
|
|
345
|
-
|
|
346
|
-
**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:
|
|
347
|
-
|
|
348
|
-
```typescript
|
|
349
|
-
import "@edpire/sdk/styles/runtime-utilities.css"
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
---
|
|
353
|
-
|
|
354
|
-
## TypeScript types
|
|
355
|
-
|
|
356
|
-
All commonly needed types are available from the main entry:
|
|
357
|
-
|
|
358
|
-
```typescript
|
|
359
|
-
import type {
|
|
360
|
-
// Answer types
|
|
361
|
-
RuntimeAnswer,
|
|
362
|
-
ChoiceSetAnswer,
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
1
|
+
# @edpire/sdk
|
|
2
|
+
|
|
3
|
+
Embeddable assessment SDK for Edpire. Deliver, render, and grade assessments from your own application.
|
|
4
|
+
|
|
5
|
+
> ๐ **Full documentation** lives in the Edpire docs site under [**SDK Reference**](https://docs.edpire.com/developer/sdk/overview) โ complete option/prop/type tables for the Embedded Player, React components, Custom Flow, server client, and branding. This README is a condensed quick-start.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Which approach fits you?
|
|
10
|
+
|
|
11
|
+
| Approach | Learner stays on your page? | You build the UI? | Entry point |
|
|
12
|
+
|---|---|---|---|
|
|
13
|
+
| **[Hosted Redirect](#hosted-redirect)** | No | No | Share URL only |
|
|
14
|
+
| **[Embedded Player](#embedded-player)** | Yes | No | `EdpireAssessment.mount()` |
|
|
15
|
+
| **[Custom Flow](#custom-flow)** | Yes | Yes | `renderQuestion` + `flattenAssessment` |
|
|
16
|
+
|
|
17
|
+
> **Server Client** (`EdpireClient`) is a backend utility used alongside any of the above โ not a separate approach. [Jump to Server Client โ](#server-client)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
Published to the public npm registry โ no auth token required.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @edpire/sdk
|
|
27
|
+
# or
|
|
28
|
+
pnpm add @edpire/sdk
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
> **License:** 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.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Hosted Redirect
|
|
36
|
+
|
|
37
|
+
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.
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
https://{your-slug}.edpire.com/take/{shareCode}?learner_ref={userId}&return_url={yourUrl}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| Parameter | Required | Description |
|
|
44
|
+
|-----------|----------|-------------|
|
|
45
|
+
| `shareCode` | Yes | Assessment share code from the dashboard |
|
|
46
|
+
| `learner_ref` | Recommended | Your stable internal user ID |
|
|
47
|
+
| `return_url` | Recommended | Where to send the learner after completion |
|
|
48
|
+
|
|
49
|
+
Edpire appends `?submission_id=...&score=...&max_score=...` to the return URL.
|
|
50
|
+
|
|
51
|
+
**When to use:** Fast adoption, no custom UI needed, comfortable with a browser redirect.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Embedded Player
|
|
56
|
+
|
|
57
|
+
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.
|
|
58
|
+
|
|
59
|
+
**Server (Node.js / API route):**
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { EdpireClient } from "@edpire/sdk/client"
|
|
63
|
+
|
|
64
|
+
const client = new EdpireClient({ apiKey: process.env.EDPIRE_API_KEY! })
|
|
65
|
+
|
|
66
|
+
// Mint a short-lived single-use token for this learner + assessment
|
|
67
|
+
const { token } = await client.mintEmbedToken(assessmentId, userId)
|
|
68
|
+
// Pass `token` to the browser (e.g. via server-rendered props or an API response)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Browser:**
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { EdpireAssessment } from "@edpire/sdk"
|
|
75
|
+
|
|
76
|
+
const embed = EdpireAssessment.mount({
|
|
77
|
+
token, // from your server
|
|
78
|
+
container: "#assessment-root", // CSS selector or HTMLElement
|
|
79
|
+
onComplete: (result) => {
|
|
80
|
+
console.log(`Score: ${result.score}/${result.max_score}`)
|
|
81
|
+
console.log(`Passed: ${result.passed}`)
|
|
82
|
+
},
|
|
83
|
+
onError: (err) => console.error(err.message),
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// Later, when done:
|
|
87
|
+
embed.unmount()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**CDN (no bundler):**
|
|
91
|
+
|
|
92
|
+
```html
|
|
93
|
+
<script src="https://cdn.jsdelivr.net/npm/@edpire/sdk@0.6.7/dist/umd/index.global.js"></script>
|
|
94
|
+
<script>
|
|
95
|
+
EdpireSDK.EdpireAssessment.mount({ token, container: "#root", onComplete: handleDone })
|
|
96
|
+
</script>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
> The UMD build is served straight from npm by [jsDelivr](https://www.jsdelivr.com/) (and [unpkg](https://unpkg.com/)) โ no CDN to run yourself. Pin a version for production; `@latest` works for prototyping.
|
|
100
|
+
|
|
101
|
+
**When to use:** You want the full assessment experience inside your page with a single function call. Zero UI code required.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Custom Flow
|
|
106
|
+
|
|
107
|
+
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.
|
|
108
|
+
|
|
109
|
+
**How it works:**
|
|
110
|
+
1. Your server fetches the assessment (keeping your API key server-side)
|
|
111
|
+
2. `flattenAssessment()` converts it into a flat array of question steps
|
|
112
|
+
3. `renderQuestion()` (or `<EdpireQuestion>` in React) renders each step into a container you control
|
|
113
|
+
4. Your server grades each answer via the `/check` endpoint
|
|
114
|
+
5. After all questions, your server submits the full attempt
|
|
115
|
+
|
|
116
|
+
**Server โ fetch the assessment:**
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// GET /api/edpire/assessment/[id]/route.ts
|
|
120
|
+
import { EdpireClient } from "@edpire/sdk/client"
|
|
121
|
+
|
|
122
|
+
const client = new EdpireClient({ apiKey: process.env.EDPIRE_API_KEY! })
|
|
123
|
+
const assessment = await client.getAssessment(assessmentId)
|
|
124
|
+
return NextResponse.json({ data: assessment })
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Browser โ flatten and render:**
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
// React
|
|
131
|
+
import { flattenAssessment } from "@edpire/sdk"
|
|
132
|
+
import { EdpireQuestion } from "@edpire/sdk/react"
|
|
133
|
+
import type { RuntimeAnswer } from "@edpire/sdk"
|
|
134
|
+
|
|
135
|
+
const steps = flattenAssessment(assessment)
|
|
136
|
+
// steps[i] = { exerciseId, questionId, content, points, sequenceNumber, index }
|
|
137
|
+
|
|
138
|
+
const [currentIndex, setCurrentIndex] = useState(0)
|
|
139
|
+
const [answers, setAnswers] = useState<RuntimeAnswer[]>([])
|
|
140
|
+
const [feedback, setFeedback] = useState(null)
|
|
141
|
+
const step = steps[currentIndex]
|
|
142
|
+
|
|
143
|
+
<EdpireQuestion
|
|
144
|
+
content={step.content}
|
|
145
|
+
onAnswersChange={setAnswers}
|
|
146
|
+
feedback={feedback} // null = no feedback shown; pass result of /check to show it
|
|
147
|
+
dir="ltr" // or "rtl" for Arabic
|
|
148
|
+
/>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// Vanilla JS
|
|
153
|
+
import { flattenAssessment, renderQuestion } from "@edpire/sdk"
|
|
154
|
+
|
|
155
|
+
const steps = flattenAssessment(assessment)
|
|
156
|
+
const step = steps[currentIndex]
|
|
157
|
+
|
|
158
|
+
const question = renderQuestion({
|
|
159
|
+
container: document.getElementById("question-root"),
|
|
160
|
+
content: step.content,
|
|
161
|
+
onAnswersChange: (answers) => { /* store answers */ },
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// Later, update feedback:
|
|
165
|
+
question.setFeedback(feedbackFromCheckEndpoint)
|
|
166
|
+
|
|
167
|
+
// Move to next question:
|
|
168
|
+
question.setContent(steps[nextIndex].content)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Server โ grade each question via your proxy:**
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// POST /api/edpire/check/[id]/route.ts (recommended โ type-safe, handles URL construction)
|
|
175
|
+
import { EdpireClient } from "@edpire/sdk/client"
|
|
176
|
+
|
|
177
|
+
const client = new EdpireClient({ apiKey: process.env.EDPIRE_API_KEY! })
|
|
178
|
+
|
|
179
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
180
|
+
const { id } = await params
|
|
181
|
+
const body = await req.json()
|
|
182
|
+
// Always set include_correct_answers server-side โ never trust the client to send it
|
|
183
|
+
const result = await client.checkQuestion(id, { ...body, include_correct_answers: true })
|
|
184
|
+
return Response.json(result) // result: CheckResult = { correct, score, max_score, feedback }
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
<details>
|
|
189
|
+
<summary>Alternative โ raw fetch without EdpireClient</summary>
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// Note: assessmentId goes in the URL path, not the request body
|
|
193
|
+
const result = await fetch(`https://edpire.com/api/v1/assessments/${assessmentId}/check`, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: {
|
|
196
|
+
Authorization: `Bearer ${process.env.EDPIRE_API_KEY}`,
|
|
197
|
+
"Content-Type": "application/json",
|
|
198
|
+
},
|
|
199
|
+
body: JSON.stringify({
|
|
200
|
+
exercise_id: exerciseId,
|
|
201
|
+
question_id: questionId,
|
|
202
|
+
answers,
|
|
203
|
+
learner_ref: userId,
|
|
204
|
+
session_id: sessionId,
|
|
205
|
+
include_correct_answers: true,
|
|
206
|
+
}),
|
|
207
|
+
}).then((r) => r.json())
|
|
208
|
+
// Response envelope: { data: CheckResult, error: null, meta: null }
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
</details>
|
|
212
|
+
|
|
213
|
+
**Server โ submit the full attempt when done:**
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { EdpireClient } from "@edpire/sdk/client"
|
|
217
|
+
import type { StoredAnswer } from "@edpire/sdk"
|
|
218
|
+
|
|
219
|
+
const stored: StoredAnswer[] = []
|
|
220
|
+
// push { exerciseId, questionId, answers } for each step as the learner answers
|
|
221
|
+
|
|
222
|
+
const client = new EdpireClient({ apiKey: process.env.EDPIRE_API_KEY! })
|
|
223
|
+
const result = await client.submit(assessmentId, {
|
|
224
|
+
learner_ref: userId,
|
|
225
|
+
answers: stored, // StoredAnswer[] โ the client handles nesting automatically
|
|
226
|
+
})
|
|
227
|
+
console.log(`Final score: ${result.score}/${result.max_score}`)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
<details>
|
|
231
|
+
<summary>Rate limiting on /check</summary>
|
|
232
|
+
|
|
233
|
+
Each `(session_id, question_id)` pair is limited to 3 checks per hour (configurable per org).
|
|
234
|
+
|
|
235
|
+
**Always pass a `session_id`** โ generate a UUID once at the start of each attempt:
|
|
236
|
+
```typescript
|
|
237
|
+
const sessionId = crypto.randomUUID() // generate once per attempt, not per question
|
|
238
|
+
```
|
|
239
|
+
If 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.
|
|
240
|
+
|
|
241
|
+
</details>
|
|
242
|
+
|
|
243
|
+
**Required API key scopes:** `read:assessments` (for fetching and checking) + `write:submissions` (for the final submit).
|
|
244
|
+
|
|
245
|
+
**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.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Server Client
|
|
250
|
+
|
|
251
|
+
`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.
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import { EdpireClient } from "@edpire/sdk/client"
|
|
255
|
+
|
|
256
|
+
const client = new EdpireClient({
|
|
257
|
+
apiKey: process.env.EDPIRE_API_KEY!,
|
|
258
|
+
baseUrl: "https://edpire.com", // optional, defaults to edpire.com
|
|
259
|
+
})
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Common operations:**
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// List published assessments
|
|
266
|
+
const { items } = await client.getAssessments({ status: "published" })
|
|
267
|
+
|
|
268
|
+
// Fetch a single assessment with full content
|
|
269
|
+
const assessment = await client.getAssessment(assessmentId)
|
|
270
|
+
|
|
271
|
+
// Mint an embed token (for Embedded Player)
|
|
272
|
+
const { token } = await client.mintEmbedToken(assessmentId, learnerRef)
|
|
273
|
+
|
|
274
|
+
// Submit a full attempt (for Custom Flow)
|
|
275
|
+
const result = await client.submit(assessmentId, { learner_ref: userId, answers: stored })
|
|
276
|
+
|
|
277
|
+
// Grade a single question without recording (for Custom Flow /check)
|
|
278
|
+
const check = await client.checkQuestion(assessmentId, {
|
|
279
|
+
exercise_id, question_id, answers, learner_ref, session_id,
|
|
280
|
+
include_correct_answers: true,
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
// Register a webhook
|
|
284
|
+
const webhook = await client.registerWebhook("https://yourapp.com/webhooks/edpire", [
|
|
285
|
+
"submission.graded",
|
|
286
|
+
])
|
|
287
|
+
// Store webhook.secret securely โ shown only once
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Mobile & WebView integration
|
|
293
|
+
|
|
294
|
+
Flutter, React Native, and native iOS/Android apps cannot import this npm package directly.
|
|
295
|
+
The integration uses two parts:
|
|
296
|
+
|
|
297
|
+
1. **Your backend** mints an embed token via `client.mintEmbedToken()` (the API key stays server-side).
|
|
298
|
+
2. **A WebView** in your mobile app loads a minimal HTML page that calls the CDN UMD build with the token.
|
|
299
|
+
|
|
300
|
+
> The UMD build is served from npm via [jsDelivr](https://www.jsdelivr.com/) โ no CDN to host yourself.
|
|
301
|
+
|
|
302
|
+
**Universal WebView HTML template:**
|
|
303
|
+
|
|
304
|
+
```html
|
|
305
|
+
<!DOCTYPE html>
|
|
306
|
+
<html>
|
|
307
|
+
<head>
|
|
308
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
309
|
+
<style>* { margin: 0; padding: 0; box-sizing: border-box; } #root { min-height: 100vh; }</style>
|
|
310
|
+
</head>
|
|
311
|
+
<body>
|
|
312
|
+
<div id="root"></div>
|
|
313
|
+
<script src="https://cdn.jsdelivr.net/npm/@edpire/sdk@0.6.7/dist/umd/index.global.js"></script>
|
|
314
|
+
<script>
|
|
315
|
+
EdpireSDK.EdpireAssessment.mount({
|
|
316
|
+
token: "{{EMBED_TOKEN}}", // replace server-side before loading
|
|
317
|
+
container: "#root",
|
|
318
|
+
onComplete: function(r) {
|
|
319
|
+
if (window.ReactNativeWebView)
|
|
320
|
+
window.ReactNativeWebView.postMessage(JSON.stringify({ type: "complete", result: r }))
|
|
321
|
+
if (window.flutter_inappwebview)
|
|
322
|
+
window.flutter_inappwebview.callHandler("onComplete", r)
|
|
323
|
+
},
|
|
324
|
+
onError: function(e) {
|
|
325
|
+
if (window.ReactNativeWebView)
|
|
326
|
+
window.ReactNativeWebView.postMessage(JSON.stringify({ type: "error", error: e }))
|
|
327
|
+
if (window.flutter_inappwebview)
|
|
328
|
+
window.flutter_inappwebview.callHandler("onError", e)
|
|
329
|
+
}
|
|
330
|
+
})
|
|
331
|
+
</script>
|
|
332
|
+
</body>
|
|
333
|
+
</html>
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
> **Ionic / Capacitor** apps run in a WebView โ you can use the full npm SDK directly. No HTML template needed.
|
|
337
|
+
|
|
338
|
+
For React Native, Flutter, and native iOS/Android code examples, see the [Mobile Integration guide](https://docs.edpire.com/developer/mobile).
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## CSS
|
|
343
|
+
|
|
344
|
+
**Embedded Player** (`EdpireAssessment.mount()`) โ all CSS is injected automatically. No imports needed.
|
|
345
|
+
|
|
346
|
+
**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:
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
import "@edpire/sdk/styles/runtime-utilities.css"
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## TypeScript types
|
|
355
|
+
|
|
356
|
+
All commonly needed types are available from the main entry:
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
import type {
|
|
360
|
+
// Answer types
|
|
361
|
+
RuntimeAnswer,
|
|
362
|
+
ChoiceSetAnswer,
|
|
363
|
+
TypedBlankAnswer,
|
|
364
|
+
BlankChoiceAnswer,
|
|
365
|
+
MatchingSetAnswer,
|
|
366
|
+
MatchingConnection,
|
|
367
|
+
OpenResponseAnswer,
|
|
368
|
+
|
|
369
|
+
// Custom Flow helpers
|
|
370
|
+
StoredAnswer, // { exerciseId, questionId, answers: RuntimeAnswer[] }
|
|
371
|
+
FlatStep, // one step from flattenAssessment()
|
|
372
|
+
|
|
373
|
+
// Embedded Player
|
|
374
|
+
MountOptions,
|
|
375
|
+
EmbedInstance,
|
|
376
|
+
EmbedResult,
|
|
377
|
+
EmbedError,
|
|
378
|
+
} from "@edpire/sdk"
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Types specific to the Server Client come from `@edpire/sdk/client`:
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
import type { Assessment, GradeResult, CheckResult, EdpireClientOptions } from "@edpire/sdk/client"
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## Webhooks
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
const webhook = await client.registerWebhook("https://yourapp.com/api/webhooks/edpire", [
|
|
393
|
+
"submission.graded",
|
|
394
|
+
"assessment.published",
|
|
395
|
+
])
|
|
396
|
+
// Store webhook.secret securely โ it is only shown once.
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
**Verify incoming signatures:**
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
import { createHmac, timingSafeEqual } from "crypto"
|
|
403
|
+
|
|
404
|
+
export async function POST(req: Request) {
|
|
405
|
+
const rawBody = await req.text()
|
|
406
|
+
const signature = req.headers.get("x-edpire-signature") ?? ""
|
|
407
|
+
|
|
408
|
+
const expected = "sha256=" + createHmac("sha256", process.env.EDPIRE_WEBHOOK_SECRET!)
|
|
409
|
+
.update(rawBody)
|
|
410
|
+
.digest("hex")
|
|
411
|
+
|
|
412
|
+
// Length check first: timingSafeEqual throws RangeError on unequal lengths,
|
|
413
|
+
// so a truncated or missing header would crash the route instead of 401ing.
|
|
414
|
+
const sigBuf = Buffer.from(signature)
|
|
415
|
+
const expBuf = Buffer.from(expected)
|
|
416
|
+
if (sigBuf.length !== expBuf.length || !timingSafeEqual(sigBuf, expBuf)) {
|
|
417
|
+
return new Response("Unauthorized", { status: 401 })
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const event = JSON.parse(rawBody)
|
|
421
|
+
// event.event, event.submission_id, event.score, event.passed, ...
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**Available events:** `submission.graded`, `submission.grading.completed`, `assessment.published`, `assessment.updated`, `assessment.content_updated`, `assessment.archived`, and the `collection.*` events. Full list and payloads: [Webhooks](https://docs.edpire.com/developer/webhooks).
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## Error handling
|
|
430
|
+
|
|
431
|
+
`EdpireClient` throws `EdpireError` on non-2xx responses:
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
import { EdpireError } from "@edpire/sdk/client"
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
await client.submit(assessmentId, options)
|
|
438
|
+
} catch (err) {
|
|
439
|
+
if (err instanceof EdpireError) {
|
|
440
|
+
console.error(err.status, err.message)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Common status codes: `400` bad request ยท `401` invalid API key ยท `404` not found ยท `409` max attempts reached ยท `422` grading failed ยท `429` rate limit exceeded
|