Skip to main content
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:
PartWho does itHow
API calls (list assessments, submit answers, fetch results)Your backendREST API with Bearer token
Assessment playerMobile WebViewUMD 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 comparison

PlatformServer-side APIPlayer delivery
React NativeEdpireClient (npm) or plain fetchreact-native-webview
Flutterhttp / dio (Dart)flutter_inappwebview
iOS nativeURLSession (Swift)WKWebView
Android nativeOkHttp (Kotlin)WebView
Ionic / CapacitorEdpireClient (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.