Edpire’s REST API and embed token system are designed to work with any platform, including mobile apps that cannot run the JavaScript SDK directly.
How it works
Mobile integration follows a two-part architecture:
| Part | Who does it | How |
|---|
| API calls (list assessments, submit answers, fetch results) | Your backend | REST API with Bearer token |
| Assessment player | Mobile WebView | UMD SDK loaded in a minimal HTML page |
Your API key never leaves your server. The mobile app only ever sees a short-lived embed token.
Mobile app → YOUR backend → POST /api/v1/embed/token → Edpire
↓
Mobile app ← { token, expires_at } ← YOUR backend
↓
WebView loads player HTML with token
↓
onComplete message posted to native layer
The WebView HTML template
This is the universal mobile player. Load it in any WebView with the embed token injected:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: sans-serif; }
#root { min-height: 100vh; }
</style>
</head>
<body>
<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/@edpire/sdk@0.5.0/dist/umd/index.global.js"></script>
<script>
var TOKEN = "{{EMBED_TOKEN}}"; // inject server-side before loading in WebView
EdpireSDK.EdpireAssessment.mount({
token: TOKEN,
container: "#root",
onComplete: function(result) {
// Post result back to native layer
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(JSON.stringify({ type: "complete", result: result }));
}
if (window.flutter_inappwebview) {
window.flutter_inappwebview.callHandler("onComplete", result);
}
},
onError: function(err) {
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(JSON.stringify({ type: "error", error: err }));
}
if (window.flutter_inappwebview) {
window.flutter_inappwebview.callHandler("onError", err);
}
}
});
</script>
</body>
</html>
Serve this HTML from your own backend (or build it as a string in your app) with {{EMBED_TOKEN}} replaced server-side.
Never embed your API key in the WebView HTML. The embed token is short-lived (1 hour), single-use, and scoped to one learner + assessment — it is safe to put in client code.
React Native
React Native apps can use EdpireClient directly for server-side operations — it uses fetch, which is available natively in React Native. For the player, open a WebView with the HTML template.
Server-side (your Node.js backend):
import { EdpireClient } from "@edpire/sdk/client"
const client = new EdpireClient({ apiKey: process.env.EDPIRE_API_KEY! })
// Your API route that the React Native app calls:
export async function POST(req: Request) {
const { assessmentId, learnerRef } = await req.json()
const token = await client.mintEmbedToken(assessmentId, learnerRef)
return Response.json(token)
}
React Native app:
import { useState } from "react"
import { WebView } from "react-native-webview"
const PLAYER_HTML = `
<!DOCTYPE html>
<html>
<head><meta name="viewport" content="width=device-width, initial-scale=1" /></head>
<body>
<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/@edpire/sdk@0.5.0/dist/umd/index.global.js"></script>
<script>
EdpireSDK.EdpireAssessment.mount({
token: "EMBED_TOKEN_PLACEHOLDER",
container: "#root",
onComplete: function(r) {
window.ReactNativeWebView.postMessage(JSON.stringify({ type: "complete", result: r }));
},
onError: function(e) {
window.ReactNativeWebView.postMessage(JSON.stringify({ type: "error", error: e }));
}
});
</script>
</body>
</html>
`
export function AssessmentScreen({ assessmentId, learnerRef }) {
const [html, setHtml] = useState<string | null>(null)
async function launch() {
// Call YOUR backend — never call Edpire directly from the app
const res = await fetch("https://yourapp.com/api/embed-token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ assessmentId, learnerRef }),
})
const { token } = await res.json()
setHtml(PLAYER_HTML.replace("EMBED_TOKEN_PLACEHOLDER", token))
}
if (!html) return <Button onPress={launch} title="Start Assessment" />
return (
<WebView
source={{ html }}
onMessage={(event) => {
const msg = JSON.parse(event.nativeEvent.data)
if (msg.type === "complete") {
console.log("Score:", msg.result.score, "/", msg.result.max_score)
}
}}
/>
)
}
Flutter
Flutter apps call your backend via http or dio to fetch the embed token, then load the player HTML in a WebView.
Mint the token from your backend (Dart HTTP call):
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<String> mintEmbedToken(String assessmentId, String learnerRef) async {
// Call YOUR backend — the API key lives there, not in the app
final res = await http.post(
Uri.parse('https://yourapp.com/api/embed-token'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'assessmentId': assessmentId, 'learnerRef': learnerRef}),
);
final data = jsonDecode(res.body) as Map<String, dynamic>;
return data['token'] as String;
}
Load the player in a WebView (using flutter_inappwebview):
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
class AssessmentScreen extends StatefulWidget {
final String assessmentId;
final String learnerRef;
const AssessmentScreen({required this.assessmentId, required this.learnerRef, super.key});
@override
State<AssessmentScreen> createState() => _AssessmentScreenState();
}
class _AssessmentScreenState extends State<AssessmentScreen> {
String? _token;
InAppWebViewController? _webController;
@override
void initState() {
super.initState();
mintEmbedToken(widget.assessmentId, widget.learnerRef).then((token) {
setState(() => _token = token);
});
}
String _buildHtml(String token) => '''
<!DOCTYPE html>
<html>
<head><meta name="viewport" content="width=device-width, initial-scale=1" /></head>
<body>
<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/@edpire/sdk@0.5.0/dist/umd/index.global.js"></script>
<script>
EdpireSDK.EdpireAssessment.mount({
token: "$token",
container: "#root",
onComplete: function(r) {
window.flutter_inappwebview.callHandler("onComplete", r);
},
onError: function(e) {
window.flutter_inappwebview.callHandler("onError", e);
}
});
</script>
</body>
</html>
''';
@override
Widget build(BuildContext context) {
if (_token == null) return const Center(child: CircularProgressIndicator());
return InAppWebView(
initialData: InAppWebViewInitialData(data: _buildHtml(_token!)),
onWebViewCreated: (controller) {
_webController = controller;
controller.addJavaScriptHandler(
handlerName: 'onComplete',
callback: (args) {
final result = args.first as Map<String, dynamic>;
debugPrint('Score: ${result['score']} / ${result['max_score']}');
// Navigate to your results screen
},
);
controller.addJavaScriptHandler(
handlerName: 'onError',
callback: (args) {
debugPrint('Embed error: ${args.first}');
},
);
},
);
}
}
Native iOS / Android
The same HTML template works in any native WebView:
- iOS:
WKWebView — use loadHTMLString(_:baseURL:) and receive messages via WKScriptMessageHandler
- Android:
WebView — use loadDataWithBaseURL() and receive messages via addJavascriptInterface
The pattern is identical in both cases: your backend mints the token, the native code builds the HTML string with the token injected, and the result comes back via the WebView message bridge.
| Platform | Server-side API | Player delivery |
|---|
| React Native | EdpireClient (npm) or plain fetch | react-native-webview |
| Flutter | http / dio (Dart) | flutter_inappwebview |
| iOS native | URLSession (Swift) | WKWebView |
| Android native | OkHttp (Kotlin) | WebView |
| Ionic / Capacitor | EdpireClient (npm) | Full SDK in WebView |
Ionic and Capacitor apps run in a WebView already. You can use the full @edpire/sdk npm package directly — no need for the HTML template approach.