Mobile Integration

Add audio to your mobile app

Integrate Everlit audio players into iOS, Android, React Native, and Flutter apps using WebView components.

Overview

This guide shows you how to add Everlit audio to native mobile apps using WebView components. Whether you're using React Native, native iOS, native Android, or Flutter, the approach is the same: load a WebView with the Everlit widget HTML.

Building a website instead? See the Web Integration docs for adding audio to websites and CMS platforms.

How It Works

Mobile integration uses a WebView to display the Everlit audio player:

  1. Create a WebView in your native app
  2. Load HTML containing your article content and the Everlit widget
  3. The widget automatically converts content to audio and displays the player
Note: The audio player is always rendered as an iframe inside the WebView. There's no native audio component—all platforms use the same web-based player for consistency.

Platform Examples

Here's how to create a WebView and load the HTML snippet in different platforms:

Installation

npm install react-native-webview
# or
yarn add react-native-webview

Basic Implementation

import React, { useRef } from 'react';
import { WebView } from 'react-native-webview';

const ArticleWithAudio = ({ articleUrl, publicationId }) => {
  const webViewRef = useRef(null);

  const htmlContent = `
    <!DOCTYPE html>
    <html>
      <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <style>
          body {
            margin: 0;
            padding: 16px;
            font-family: -apple-system, sans-serif;
          }
          #everlit-audio-embed-container iframe {
            width: 100%;
            border: none;
          }
        </style>
      </head>
      <body>
        <div id="article-content">
          <h1>Article Title</h1>
          <div class="articleBody">
            <p>Article content...</p>
          </div>
        </div>

        <!-- Everlit Audio Widget -->
        <div
          id="everlit-auto-audio-widget"
          data-publication-id="${publicationId}"
          style="height: 136px; width: 100%;"
          hidden
        ></div>

        <script
          defer
          src="https://cdn.everlit.audio/libs/everlitAutoAudio.js"
          type="text/javascript"
        ></script>
      </body>
    </html>
  `;

  return (
    <WebView
      ref={webViewRef}
      source={{ html: htmlContent }}
      style={{ flex: 1 }}
      javaScriptEnabled={true}
      domStorageEnabled={true}
      thirdPartyCookiesEnabled={true}
      sharedCookiesEnabled={true}
      originWhitelist={['*']}
      allowsInlineMediaPlayback={true}
      mediaPlaybackRequiresUserAction={false}
      mixedContentMode="always"
    />
  );
};

export default ArticleWithAudio;

Swift Implementation

import WebKit

class ArticleViewController: UIViewController, WKNavigationDelegate {
    var webView: WKWebView!

    override func loadView() {
        let webConfiguration = WKWebViewConfiguration()
        webView = WKWebView(frame: .zero, configuration: webConfiguration)
        webView.navigationDelegate = self
        view = webView
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let htmlString = """
        <!DOCTYPE html>
        <html>
          <head>
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <style>
              body { margin: 0; padding: 16px; font-family: -apple-system; }
              #everlit-audio-embed-container iframe { width: 100%; border: none; }
            </style>
          </head>
          <body>
            <h1>Article Title</h1>
            <div class="articleBody">
              <p>Article content...</p>
            </div>

            <div
              id="everlit-auto-audio-widget"
              data-publication-id="YOUR-PUBLICATION-ID"
              style="height: 136px; width: 100%;"
              hidden
            ></div>

            <script defer src="https://cdn.everlit.audio/libs/everlitAutoAudio.js"></script>
          </body>
        </html>
        """

        webView.loadHTMLString(htmlString, baseURL: nil)
    }
}
Note: WKWebView handles JavaScript and media playback automatically. No additional configuration needed!

Kotlin Implementation

import android.os.Bundle
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity

class ArticleActivity : AppCompatActivity() {
    private lateinit var webView: WebView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        webView = WebView(this)
        setContentView(webView)

        // Enable JavaScript
        webView.settings.javaScriptEnabled = true
        webView.settings.domStorageEnabled = true
        webView.settings.mediaPlaybackRequiresUserGesture = false

        // Set WebView client
        webView.webViewClient = WebViewClient()

        val htmlContent = """
            <!DOCTYPE html>
            <html>
              <head>
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <style>
                  body { margin: 0; padding: 16px; font-family: sans-serif; }
                  #everlit-audio-embed-container iframe { width: 100%; border: none; }
                </style>
              </head>
              <body>
                <h1>Article Title</h1>
                <div class="articleBody">
                  <p>Article content...</p>
                </div>

                <div
                  id="everlit-auto-audio-widget"
                  data-publication-id="YOUR-PUBLICATION-ID"
                  style="height: 136px; width: 100%;"
                  hidden
                ></div>

                <script defer src="https://cdn.everlit.audio/libs/everlitAutoAudio.js"></script>
              </body>
            </html>
        """.trimIndent()

        webView.loadDataWithBaseURL(null, htmlContent, "text/html", "UTF-8", null)
    }
}

AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET" />

pubspec.yaml

dependencies:
  webview_flutter: ^4.4.0

Dart Implementation

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class ArticleScreen extends StatefulWidget {
  @override
  _ArticleScreenState createState() => _ArticleScreenState();
}

class _ArticleScreenState extends State<ArticleScreen> {
  late final WebViewController controller;

  @override
  void initState() {
    super.initState();

    controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..loadHtmlString('''
        <!DOCTYPE html>
        <html>
          <head>
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <style>
              body { margin: 0; padding: 16px; font-family: sans-serif; }
              #everlit-audio-embed-container iframe { width: 100%; border: none; }
            </style>
          </head>
          <body>
            <h1>Article Title</h1>
            <div class="articleBody">
              <p>Article content...</p>
            </div>

            <div
              id="everlit-auto-audio-widget"
              data-publication-id="YOUR-PUBLICATION-ID"
              style="height: 136px; width: 100%;"
              hidden
            ></div>

            <script defer src="https://cdn.everlit.audio/libs/everlitAutoAudio.js"></script>
          </body>
        </html>
      ''');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Article')),
      body: WebViewWidget(controller: controller),
    );
  }
}

Advanced Options

The basic WebView approach above works out of the box. For more control—custom loading states, error handling, or reliable EUT (analytics token) persistence across app sessions—call the Auto Audio API directly from your app and render the returned iframe player in a WebView.

EUT persistence: Inside a WebView the widget stores its EUT (analytics token) in a cookie, which mobile WebViews don't always persist across sessions. To keep a stable EUT, use the Direct API approach below and store the token yourself (for example with AsyncStorage), passing it on each request.

Direct API Integration

Advanced Option: For maximum control over the conversion lifecycle, you can call the Auto Audio API directly from your app. You'll still render the iframe player in a WebView, but you control when and how the API is called.

This approach gives you programmatic control over API calls, error handling, and conversion status, while still rendering the returned iframe player.

Authentication: The check and create calls shown below don't require a token. A bearer token (Authorization: Bearer api_…) is only needed for server-to-server content ingestion (sending article text directly). API tokens are scoped to your publication(s)—request one from Everlit. Avoid shipping a token inside an app binary you distribute to users.

Installation

npm install @react-native-async-storage/async-storage
# or
yarn add @react-native-async-storage/async-storage

API Client Implementation

// everlitAudioClient.js
import AsyncStorage from '@react-native-async-storage/async-storage';

class EverlitAudioClient {
  constructor(publicationId, apiToken = null) {
    this.publicationId = publicationId;
    this.apiToken = apiToken;
    this.baseUrl = 'https://creator.everlit.audio';
  }

  async generateEUT() {
    const now = Date.now();
    const timeHex = now.toString(16).padStart(12, '0');
    const randomBytes = new Uint8Array(10);

    for (let i = 0; i < 10; i++) {
      randomBytes[i] = Math.floor(Math.random() * 256);
    }

    const randomHex = Array.from(randomBytes)
      .map(byte => byte.toString(16).padStart(2, '0'))
      .join('');

    return `${timeHex.slice(0, 8)}${timeHex.slice(8)}7${randomHex.slice(0, 3)}${randomHex.slice(3, 5)}${randomHex.slice(5, 15)}`;
  }

  async ensureEUT() {
    let eut = await AsyncStorage.getItem('everlit_eut');
    if (!eut) {
      eut = await this.generateEUT();
      await AsyncStorage.setItem('everlit_eut', eut);
    }
    return eut;
  }

  encodeEVP(params) {
    const json = JSON.stringify(params);
    return btoa(json)
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=/g, '');
  }

  async checkStatus(url, contentData = null) {
    const eut = await this.ensureEUT();
    const params = { url, publication_id: this.publicationId, eut };

    if (contentData) Object.assign(params, contentData);

    const evp = this.encodeEVP(params);
    const response = await fetch(
      `${this.baseUrl}/audio/auto/check?evp=${evp}`
    );

    return await response.json();
  }

  async createAudio(url, options = {}) {
    const eut = await this.ensureEUT();
    const headers = { 'Content-Type': 'application/json' };

    if (this.apiToken) {
      headers['Authorization'] = `Bearer ${this.apiToken}`;
    }

    const response = await fetch(`${this.baseUrl}/audio/auto`, {
      method: 'POST',
      headers,
      body: JSON.stringify({
        url,
        publication_id: this.publicationId,
        eut,
        ...options,
      }),
    });

    return await response.json();
  }

  async getOrCreateAudio(url, options = {}) {
    const checkResult = await this.checkStatus(url);

    if (checkResult.embed) return checkResult;

    if (checkResult.create || checkResult.click_to_create) {
      const createResult = await this.createAudio(url, options);
      if (createResult.embed) return createResult;
      if (createResult.waiting) {
        return await this.pollForCompletion(url);
      }
      return createResult;
    }

    if (checkResult.waiting) {
      return await this.pollForCompletion(url);
    }

    return checkResult;
  }

  async pollForCompletion(url, maxAttempts = 60, delay = 5000) {
    for (let attempt = 0; attempt < maxAttempts; attempt++) {
      await new Promise(resolve => setTimeout(resolve, delay));
      const status = await this.checkStatus(url);
      if (status.embed) return status;
      if (!status.successful && !status.waiting) {
        throw new Error(status.reason || 'Conversion failed');
      }
    }
    throw new Error('Conversion timeout');
  }

  extractIframeUrl(embedHtml) {
    const match = embedHtml.match(/src="([^"]+)"/);
    return match ? match[1] : null;
  }
}

export default EverlitAudioClient;
// ArticleAudioPlayer.js
import React, { useState, useEffect } from 'react';
import { View, ActivityIndicator, Text, StyleSheet } from 'react-native';
import { WebView } from 'react-native-webview';
import EverlitAudioClient from './everlitAudioClient';

const ArticleAudioPlayer = ({
  articleUrl,
  publicationId,
  apiToken = null,
  onStatusChange = () => {},
}) => {
  const [status, setStatus] = useState('loading');
  const [embedUrl, setEmbedUrl] = useState(null);
  const [error, setError] = useState(null);
  const [client] = useState(() => new EverlitAudioClient(publicationId, apiToken));

  useEffect(() => {
    loadAudio();
  }, [articleUrl]);

  const loadAudio = async () => {
    try {
      setStatus('loading');
      setError(null);
      onStatusChange('loading');

      const result = await client.getOrCreateAudio(articleUrl);

      if (result.embed) {
        const url = client.extractIframeUrl(result.embed);
        setEmbedUrl(url);
        setStatus('ready');
        onStatusChange('ready', result);
      } else if (result.click_to_create) {
        setStatus('click_to_create');
        onStatusChange('click_to_create', result);
      } else if (!result.successful) {
        throw new Error(result.reason || 'Unknown error');
      }
    } catch (err) {
      setError(err.message);
      setStatus('error');
      onStatusChange('error', err);
    }
  };

  if (status === 'loading') {
    return (
      <View style={styles.container}>
        <ActivityIndicator size="large" color="#0066CC" />
        <Text style={styles.statusText}>Checking for audio...</Text>
      </View>
    );
  }

  if (status === 'ready' && embedUrl) {
    return (
      <View style={styles.playerContainer}>
        <WebView
          source={{ uri: embedUrl }}
          style={styles.webview}
          scrollEnabled={false}
          allowsInlineMediaPlayback={true}
          mediaPlaybackRequiresUserAction={false}
        />
      </View>
    );
  }

  return null;
};

const styles = StyleSheet.create({
  container: {
    padding: 20,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#F5F5F5',
    borderRadius: 8,
    minHeight: 130,
  },
  playerContainer: {
    height: 130,
    backgroundColor: '#F5F5F5',
    borderRadius: 8,
    overflow: 'hidden',
  },
  webview: {
    flex: 1,
    backgroundColor: 'transparent',
  },
  statusText: {
    marginTop: 12,
    fontSize: 14,
    color: '#666',
  },
});

export default ArticleAudioPlayer;
// App.js or your article screen
import React from 'react';
import { ScrollView, Text } from 'react-native';
import ArticleAudioPlayer from './ArticleAudioPlayer';

const ArticleScreen = () => {
  return (
    <ScrollView style={{ flex: 1, padding: 16 }}>
      <Text style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 16 }}>
        Article Title
      </Text>

      {/* Audio Player */}
      <ArticleAudioPlayer
        articleUrl="https://example.com/article-slug"
        publicationId="pblc_your_publication_id"
        onStatusChange={(status, data) => {
          console.log('Audio status:', status, data);
        }}
      />

      {/* Article Content */}
      <Text style={{ marginTop: 16, fontSize: 16, lineHeight: 24 }}>
        Your article content here...
      </Text>
    </ScrollView>
  );
};

export default ArticleScreen;

Mobile Considerations

Cookie Storage

Problem: Mobile WebViews may not persist cookies reliably.

Solution: Use the Direct API approach and manage the EUT yourself (e.g. AsyncStorage), passing it on each request.

Iframe Rendering

Problem: Android WebView versions have issues with nested iframes.

Solution: Set allowsInlineMediaPlayback and mixedContentMode="always".

CORS and Security

Problem: WebView may block cross-origin requests.

Solution: Set originWhitelist={['*']} and enable third-party cookies.

Content Extraction

Problem: Widget expects specific DOM structures.

Solution: Use native API and pass extracted content directly.

Performance

Problem: Full WebView + iframe can be slow.

Solution: Use native API integration or lazy load the player.

Audio Playback

Problem: iOS requires user interaction for audio.

Solution: Set mediaPlaybackRequiresUserAction={false}.

WebView Configuration Reference

<WebView
  javaScriptEnabled={true}
  domStorageEnabled={true}
  thirdPartyCookiesEnabled={true}
  sharedCookiesEnabled={true}
  originWhitelist={['*']}
  allowsInlineMediaPlayback={true}
  mediaPlaybackRequiresUserAction={false}
  mixedContentMode="always" // Android
/>

Troubleshooting

Common Issues

Issue Possible Cause Solution
Audio player not appearing Incorrect publication ID or JavaScript disabled Verify data-publication-id and javaScriptEnabled={true}
Cookies not persisting WebView cookie restrictions Use the Direct API approach and pass a stored EUT on each request
Iframe not rendering WebView configuration Set allowsInlineMediaPlayback={true} and originWhitelist={['*']}
Audio not playing iOS autoplay restrictions Set mediaPlaybackRequiresUserAction={false}
Content not extracted Missing DOM structure Use native API and pass extractedText explicitly

Debug WebView

Enable remote debugging to see console errors:

<WebView
  onError={(syntheticEvent) => {
    const { nativeEvent } = syntheticEvent;
    console.error('WebView error:', nativeEvent);
  }}
  onHttpError={(syntheticEvent) => {
    const { nativeEvent } = syntheticEvent;
    console.error('HTTP error:', nativeEvent);
  }}
  onMessage={(event) => {
    console.log('WebView message:', event.nativeEvent.data);
  }}
/>
Remote Debugging:
iOS: Safari > Develop > [Your Device] > [Your App]
Android: chrome://inspect/#devices

Next Steps