EUREEL

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.

HeaderWhen
X-Eureel-KeyAlwayspk_live_…
X-Eureel-App-IdMobile appsYour bundle id / applicationId. Required if the project restricts Allowed apps.
X-Eureel-SdkOptionale.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

Request
curl https://api.eureel.com/v1/config \
  -H "X-Eureel-Key: pk_live_…" \
  -H "X-Eureel-App-Id: com.your.app"
Response 200
{
  "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
        }
      ]
    }
  ]
}
StatusMeaning
200Config JSON. Store it with the ETag response header.
304Unchanged — send the stored ETag as If-None-Match and keep using your copy.
401Missing / invalid / revoked key.
403This origin or app id isn't allowed (dashboard → Settings).
429Too 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:

typeMatches when
exactpath equals web_path (trailing slash ignored)
prefixpath equals web_path or starts with web_path + "/" (/docs matches /docs/intro, not /docsx)
regexweb_path is a regular expression searched in the path

Playing videos

FieldUse
mp4.small480p MP4 — small bubble previews
mp4.large720p MP4 — the opened player (fall back to small)
hls_urlAdaptive HLS stream, if your player prefers it
thumbnail_urlBubble image (may be an animated WebP if chosen in the dashboard)
preview_urlShort 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

Request
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 }
typeSend whenExtra
bubble_shownThe bubble appears on a screen
bubble_openedThe user opens the player
video_startedPlayback startsvideo_id
video_progress25 / 50 / 75 % reached (once each)video_id, value: 25 | 50 | 75
video_completed≥ 95 % watchedvideo_id
dismissedThe user closes the bubble for good
help_clickedYour “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:

eureel.js (by hand)
// 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)

Eureel.kt
// 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)

Eureel.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.