Docs
REST API
Use Eureel from any platform — native Android or iOS, Ionic, .NET MAUI, Unity… Official SDKs exist for Flutter, React / Next.js, React Native and any website (script tag); everything else talks to the same two endpoints directly.
Overview
Base URL https://api.eureel.com. Your app does three things: 1. fetch the config (GET /v1/config) — which screens have videos and their playable links; 2. when the user is on one of those screens, show a bubble that opens a player; 3. report what happened (POST /v1/events) so the dashboard analytics work.
Authentication
Send your project's public key (dashboard → Install) on every request. It can only read this project's videos, so it's safe to ship inside apps.
| Header | When | |
|---|---|---|
X-Eureel-Key | Always | pk_live_… |
X-Eureel-App-Id | Mobile apps | Your bundle id / applicationId. Required if the project restricts Allowed apps. |
X-Eureel-Sdk | Optional | e.g. rn/1.0.0 — helps support. |
Browsers send Origin automatically; if the project restricts Allowed origins, add your domain in the dashboard.
GET /v1/config
curl https://api.eureel.com/v1/config \
-H "X-Eureel-Key: pk_live_…" \
-H "X-Eureel-App-Id: com.your.app"{
"schema_version": 1,
"project": { "id": "4f0c…", "name": "My App", "platforms": "both" },
"enabled": true, // false → hide everything ("reason" says why)
"reason": null, // e.g. "quota_exceeded"
"expires_at": "2026-10-14T18:00:00Z", // signed video links stop working after this
"theme": {
"primary_color": "#1828DC",
"position": "bottom-right", // bottom-right | bottom-left
"bubble_size": 150, // px on web; scale down on phones
"bubble_shape": "circle", // circle | rounded
"help_button_label": "How to use",
"autoplay_bubble": true,
"fullscreen_player": false
},
"bubble_video": null, // optional: one video for every bubble
"screens": [ // already sorted in match order
{
"key": "checkout",
"name": "Checkout",
"priority": 0,
"match": {
"web_path": "/checkout", // web
"type": "prefix", // exact | prefix | regex
"mobile_route": "/checkout" // mobile (or match by "key")
},
"videos": [
{
"id": "a1b2c3d4-…",
"title": "How checkout works",
"duration": 42.5, // seconds
"mp4": {
"small": "https://vz-….b-cdn.net/…/play_480p.mp4?token=…&expires=…",
"large": "https://vz-….b-cdn.net/…/play_720p.mp4?token=…&expires=…"
},
"hls_url": "https://vz-….b-cdn.net/bcdn_token=…/…/playlist.m3u8",
"thumbnail_url": "https://vz-….b-cdn.net/…/thumbnail.jpg?token=…",
"preview_url": "https://vz-….b-cdn.net/…/preview.webp?token=…" // animated
}
]
}
]
}| Status | Meaning |
|---|---|
| 200 | Config JSON. Store it with the ETag response header. |
| 304 | Unchanged — send the stored ETag as If-None-Match and keep using your copy. |
| 401 | Missing / invalid / revoked key. |
| 403 | This origin or app id isn't allowed (dashboard → Settings). |
| 429 | Too many requests — back off. |
If enabled is false, show nothing. Video links are signed and stop working at expires_at (a few hours): refresh the config before then — and if a video fails to load, refetch the config once and retry.
Finding the screen
Mobile: pick the screen whose key equals your screen name (e.g. checkout), or whose match.mobile_route equals your current route.
Web: walk screens in the order given and take the first whose match.web_path matches the URL path:
| type | Matches when |
|---|---|
exact | path equals web_path (trailing slash ignored) |
prefix | path equals web_path or starts with web_path + "/" (/docs matches /docs/intro, not /docsx) |
regex | web_path is a regular expression searched in the path |
Playing videos
| Field | Use |
|---|---|
mp4.small | 480p MP4 — small bubble previews |
mp4.large | 720p MP4 — the opened player (fall back to small) |
hls_url | Adaptive HLS stream, if your player prefers it |
thumbnail_url | Bubble image (may be an animated WebP if chosen in the dashboard) |
preview_url | Short animated WebP preview |
The MP4s are H.264 + AAC and play in every native player (ExoPlayer, AVPlayer, react-native-video, HTML video). Use "contain" scaling so nothing is cropped.
POST /v1/events
POST https://api.eureel.com/v1/events
X-Eureel-Key: pk_live_…
Content-Type: application/json
{
"session_id": "k3j2h1", // random id per app launch
"user_id": "user_123", // optional — your id, never emails/names
"platform": "android", // web | ios | android | other
"sdk_version": "rn/1.0.0", // your integration's name/version
"events": [
{ "type": "bubble_shown", "screen_key": "checkout", "ts": 1791000000000 },
{ "type": "bubble_opened", "screen_key": "checkout" },
{ "type": "video_started", "screen_key": "checkout", "video_id": "a1b2c3d4-…" },
{ "type": "video_progress", "screen_key": "checkout", "video_id": "a1b2c3d4-…", "value": 50 },
{ "type": "video_completed", "screen_key": "checkout", "video_id": "a1b2c3d4-…" }
]
}
→ 202 { "accepted": 5, "dropped": 0 }| type | Send when | Extra |
|---|---|---|
bubble_shown | The bubble appears on a screen | |
bubble_opened | The user opens the player | |
video_started | Playback starts | video_id |
video_progress | 25 / 50 / 75 % reached (once each) | video_id, value: 25 | 50 | 75 |
video_completed | ≥ 95 % watched | video_id |
dismissed | The user closes the bubble for good | |
help_clicked | Your “How to use” button is tapped |
Batch events (up to 100 per request), e.g. every few seconds and when the app goes to the background. ts is optional (epoch ms or ISO 8601). If you can't set headers (e.g. navigator.sendBeacon), put "key": "pk_live_…" in the body and send it as text/plain.
Recommended behaviour
- Cache the config on the device; refresh on launch, every ~10 minutes and when the app returns to the foreground (with
If-None-Match). - Show a round bubble in a bottom corner (
theme.position,theme.primary_color); tapping it opens the player. - A small ✕ on the bubble hides it for that screen — remember it on the device, and offer a “How to use” button to bring it back.
- Never block your own UI on Eureel: if anything fails, just don't show the bubble.
React Native
Use the official SDK — npm i @eureel/react-native react-native-video @react-native-async-storage/async-storage — then Eureel.init({ apiKey }) once and <EureelOverlay screenKey="home" /> in a screen. If you'd rather integrate by hand, here's the minimal version on top of the REST API:
// npm i react-native-video @react-native-async-storage/async-storage
import { useEffect, useState } from "react";
import { Modal, Pressable, Image, Platform } from "react-native";
import Video from "react-native-video";
const API = "https://api.eureel.com";
const KEY = "pk_live_…";
const APP_ID = "com.your.app"; // only needed if "Allowed apps" is set
const headers = { "X-Eureel-Key": KEY, "X-Eureel-App-Id": APP_ID, "X-Eureel-Sdk": "rn/1.0.0" };
export async function fetchEureelConfig() {
const res = await fetch(`${API}/v1/config`, { headers });
return res.ok ? res.json() : null;
}
export function track(events) {
fetch(`${API}/v1/events`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ platform: Platform.OS, session_id: "launch-1", events }),
}).catch(() => {});
}
// <EureelBubble config={config} screenKey="checkout" />
export function EureelBubble({ config, screenKey }) {
const [open, setOpen] = useState(false);
const screen = config?.enabled ? config.screens.find((s) => s.key === screenKey) : null;
const video = screen?.videos[0];
useEffect(() => { if (video) track([{ type: "bubble_shown", screen_key: screenKey }]); }, [video?.id]);
if (!video) return null;
return (
<>
<Pressable
onPress={() => { setOpen(true); track([{ type: "bubble_opened", screen_key: screenKey }]); }}
style={{ position: "absolute", right: 20, bottom: 30, width: 96, height: 96, borderRadius: 48, overflow: "hidden" }}
>
<Image source={{ uri: video.thumbnail_url }} style={{ flex: 1 }} />
</Pressable>
<Modal visible={open} animationType="fade" onRequestClose={() => setOpen(false)}>
<Video
source={{ uri: video.mp4.large ?? video.mp4.small }}
style={{ flex: 1, backgroundColor: "black" }}
resizeMode="contain"
controls
onLoad={() => track([{ type: "video_started", screen_key: screenKey, video_id: video.id }])}
onEnd={() => track([{ type: "video_completed", screen_key: screenKey, video_id: video.id }])}
/>
</Modal>
</>
);
}Android (Kotlin)
// build.gradle: implementation("androidx.media3:media3-exoplayer:1.4.1")
// implementation("androidx.media3:media3-ui:1.4.1")
// implementation("com.squareup.okhttp3:okhttp:4.12.0")
import okhttp3.*
import org.json.JSONObject
object Eureel {
private const val API = "https://api.eureel.com"
private const val KEY = "pk_live_…"
private val http = OkHttpClient()
private fun req(path: String) = Request.Builder().url("$API$path")
.header("X-Eureel-Key", KEY)
.header("X-Eureel-App-Id", BuildConfig.APPLICATION_ID)
.header("X-Eureel-Sdk", "android/1.0.0")
/** Call off the main thread. Returns the first video's 720p URL for a screen, or null. */
fun videoUrlFor(screenKey: String): String? {
http.newCall(req("/v1/config").build()).execute().use { res ->
if (!res.isSuccessful) return null
val cfg = JSONObject(res.body!!.string())
if (!cfg.optBoolean("enabled", true)) return null
val screens = cfg.getJSONArray("screens")
for (i in 0 until screens.length()) {
val s = screens.getJSONObject(i)
if (s.getString("key") != screenKey) continue
val mp4 = s.getJSONArray("videos").getJSONObject(0).getJSONObject("mp4")
return mp4.optString("large", mp4.optString("small"))
}
}
return null
}
}
// Play it: ExoPlayer.Builder(context).build().apply {
// setMediaItem(MediaItem.fromUri(url)); prepare(); play() }iOS (Swift)
import AVKit
enum Eureel {
static let api = "https://api.eureel.com"
static let key = "pk_live_…"
/// First video's 720p URL for a screen key.
static func videoURL(for screenKey: String) async throws -> URL? {
var req = URLRequest(url: URL(string: "\(api)/v1/config")!)
req.setValue(key, forHTTPHeaderField: "X-Eureel-Key")
req.setValue(Bundle.main.bundleIdentifier, forHTTPHeaderField: "X-Eureel-App-Id")
req.setValue("ios/1.0.0", forHTTPHeaderField: "X-Eureel-Sdk")
let (data, _) = try await URLSession.shared.data(for: req)
guard let cfg = try JSONSerialization.jsonObject(with: data) as? [String: Any],
cfg["enabled"] as? Bool != false,
let screens = cfg["screens"] as? [[String: Any]],
let screen = screens.first(where: { $0["key"] as? String == screenKey }),
let video = (screen["videos"] as? [[String: Any]])?.first,
let mp4 = video["mp4"] as? [String: String],
let link = mp4["large"] ?? mp4["small"] else { return nil }
return URL(string: link)
}
}
// Play it:
// let vc = AVPlayerViewController(); vc.player = AVPlayer(url: url)
// present(vc, animated: true) { vc.player?.play() }Need an official SDK for your platform? Tell us from the dashboard — React Native and native Android/iOS SDKs are next on our list.