Skip to main content

Why use JSON for mobile?

Mobile apps render content with native UI components — there’s no HTML injection, no iframes, no webviews needed. The JSON API approach is the natural fit:

Native rendering

Receive structured data and render it with SwiftUI, Jetpack Compose, Flutter widgets, or React Native components.

Lightweight

A single HTTP call returns exactly the fields your app needs — no SDK to install, no JavaScript runtime overhead.

Offline-friendly

Cache JSON responses locally for offline display. Refresh when connectivity returns.

Cross-platform

Same endpoint works for iOS, Android, Flutter, React Native, KMP — any client that can make HTTP requests.

Runner endpoint

GET https://reelevant.run/{workflowId}/{entrypointId}?rlvt-u={userId}
ParameterTypeDescription
workflowIdpathThe workflow ID (visible in the workflow editor URL or integration modal)
entrypointIdpathThe entrypoint ID within the workflow
rlvt-uqueryUser identifier for personalisation — email, internal user ID, or device ID

Response format

The response body is the resolved template data directly — no wrapper envelope. Static fields are returned as-is and dependency fields are replaced with real values from datasources:
{
  "headline": "Recommended for you",
  "productName": "Running Shoes Pro",
  "productPrice": 129.99,
  "productImageUrl": "https://cdn.example.com/shoes-pro.jpg"
}

HTTP status codes

StatusMeaning
200Workflow executed successfully. Body contains the JSON payload.
204Workflow executed but no content was produced (all branches were empty).
404Workflow not found or not published.

Code examples

Swift (iOS)

import Foundation

struct Recommendation: Codable {
    let headline: String
    let productName: String
    let productPrice: Double
    let productImageUrl: String
    let productUrl: String
}

func fetchRecommendation(workflowId: String, userId: String) async throws -> Recommendation? {
    let url = URL(string: "https://reelevant.run/\(workflowId)/0?rlvt-u=\(userId)")!
    let (data, response) = try await URLSession.shared.data(from: url)
    
    guard let httpResponse = response as? HTTPURLResponse else { return nil }
    guard httpResponse.statusCode == 200 else { return nil }
    
    return try JSONDecoder().decode(Recommendation.self, from: data)
}

Kotlin (Android)

import kotlinx.serialization.Serializable
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*

@Serializable
data class Recommendation(
    val headline: String,
    val productName: String,
    val productPrice: Double,
    val productImageUrl: String,
    val productUrl: String
)

suspend fun fetchRecommendation(workflowId: String, userId: String): Recommendation? {
    val client = HttpClient()
    val response = client.get("https://reelevant.run/$workflowId/0") {
        parameter("rlvt-u", userId)
    }
    return if (response.status.value == 200) response.body() else null
}

Flutter (Dart)

import 'dart:convert';
import 'package:http/http.dart' as http;

class Recommendation {
  final String headline;
  final String productName;
  final double productPrice;
  final String productImageUrl;
  final String productUrl;

  Recommendation.fromJson(Map<String, dynamic> json)
      : headline = json['headline'],
        productName = json['productName'],
        productPrice = (json['productPrice'] as num).toDouble(),
        productImageUrl = json['productImageUrl'],
        productUrl = json['productUrl'];
}

Future<Recommendation?> fetchRecommendation(String workflowId, String userId) async {
  final uri = Uri.parse(
    'https://reelevant.run/$workflowId/0?rlvt-u=$userId',
  );
  final response = await http.get(uri);

  if (response.statusCode == 200) {
    return Recommendation.fromJson(jsonDecode(response.body));
  }
  return null;
}

React Native

import { useEffect, useState } from 'react'
import { View, Text, Image } from 'react-native'

type Recommendation = {
  headline: string
  productName: string
  productPrice: number
  productImageUrl: string
  productUrl: string
}

function RecommendationCard({ workflowId, userId }: { workflowId: string; userId: string }) {
  const [data, setData] = useState<Recommendation | null>(null)

  useEffect(() => {
    fetch(`https://reelevant.run/${workflowId}/0?rlvt-u=${userId}`)
      .then(res => res.ok ? res.json() : null)
      .then(setData)
  }, [workflowId, userId])

  if (!data) return null

  return (
    <View>
      <Text style={{ fontSize: 18, fontWeight: 'bold' }}>{data.headline}</Text>
      <Image source={{ uri: data.productImageUrl }} style={{ width: 200, height: 200 }} />
      <Text>{data.productName} — ${data.productPrice}</Text>
    </View>
  )
}

Identity management

Pass a stable user identifier in the rlvt-u query parameter:
Identifier typeWhen to use
Internal user ID or emailUser is logged in — best personalisation accuracy
Device ID (IDFV, Android ID)Anonymous users — maintains session continuity
Custom IDAny stable identifier your app generates
The same identity system powers all Reelevant channels (web, email, push), so personalisation is consistent across touchpoints.

Best practices

Store the last successful JSON response locally (e.g., UserDefaults, SharedPreferences, or a local database). Display cached data when offline and refresh when connectivity returns.
A 204 response means no personalised content is available. Display a sensible default or hide the personalisation zone entirely.
Initiate the API call as early as possible (e.g., on app launch or screen appear) so data is ready by the time the user scrolls to the personalised zone.
Define a struct/class matching your JSON Template schema. This catches schema mismatches at compile time and makes refactoring safe.

JSON Template configuration

JSON Templates are configured in the Reelevant platform. See the Websites > API (JSON) > Technical Integration page for full details on:
  • Template schema definition
  • Variable types (scalar vs array)
  • Template management API
  • Publish-time validation