Developer documentation
# Mobile Joe documentation
Mobile Joe adds Feature Plans, FAQs, and in-app Alerts to your iOS and Android apps. You manage the content in the dashboard; native SDKs display it in your app.
## Start here
1. [Set up your app](https://docs.mobilejoe.dev/setup.md) in the dashboard and copy its public SDK key.
2. Follow the [iOS quickstart](https://docs.mobilejoe.dev/ios/quickstart.md) (SwiftUI, SDK 1.0.1) or the [Android quickstart](https://docs.mobilejoe.dev/android/quickstart.md) (Jetpack Compose, SDK 1.0.2), or let a [coding agent](https://docs.mobilejoe.dev/coding-agents.md) do it.
3. Add the tools you need.
## Tools
- **Feature Plans** : share your roadmap and let app users vote on ideas. [iOS](https://docs.mobilejoe.dev/ios/feature-plans.md) · [Android](https://docs.mobilejoe.dev/android/feature-plans.md)
- **FAQs** : answer common questions and offer a support contact. [iOS](https://docs.mobilejoe.dev/ios/faqs.md) · [Android](https://docs.mobilejoe.dev/android/faqs.md)
- **Alerts** : display announcements to selected app users. [iOS](https://docs.mobilejoe.dev/ios/alerts.md) · [Android](https://docs.mobilejoe.dev/android/alerts.md)
Draft, translate, and publish content from Claude, Codex, or ChatGPT with the [Mobile Joe MCP server](https://docs.mobilejoe.dev/mcp.md). Agents find every guide as Markdown in [llms.txt](https://docs.mobilejoe.dev/llms.txt).
---
Get started · Dashboard
# Set up your app
Create your app in Mobile Joe before you install an SDK. Its public SDK key connects your mobile app to the content you publish there.
## Create an account
Sign up at [app.mobilejoe.dev](https://app.mobilejoe.dev/) and verify your email address. The API only answers SDK requests while the account's email address is verified and a trial or subscription is active.
## Create an app
Open the app switcher and choose **Create app**. Create one app in Mobile Joe per mobile app: its iOS and Android versions share the app, its SDK key, and its content.
## Copy the public SDK key
Open **App Settings** and copy the **Public API Key**. The key ships inside your app binary. It reads published content, registers devices, and records votes, but it cannot change content.
[App Settings → Set up SDK](https://app.mobilejoe.dev/setup) shows the configuration with your key filled in, a prompt for your coding agent, and a live check that confirms the first connection of your app.
## Publish a FAQ
Both quickstarts verify the integration with a published FAQ. Open **FAQs** , create a FAQ with a recognizable question and answer, then choose **Publish → Publish now**. Drafts never reach your app; see [Content delivery](https://docs.mobilejoe.dev/sdk/content-delivery.md).
## Next steps
Install the SDK with the [iOS quickstart](https://docs.mobilejoe.dev/ios/quickstart.md) or the [Android quickstart](https://docs.mobilejoe.dev/android/quickstart.md), or let your [coding agent](https://docs.mobilejoe.dev/coding-agents.md) do it.
---
Get started · Claude Code, Codex, Cursor
# Use a coding agent
Let your coding agent integrate the SDK. Every guide is available as plain Markdown, so the agent reads the same instructions you do.
## Before you start
[Set up your app](https://docs.mobilejoe.dev/setup.md) in the dashboard. [App Settings → Set up SDK](https://app.mobilejoe.dev/setup) provides these prompts with your public SDK key filled in; the versions here use the placeholder `YOUR_API_KEY`.
## iOS prompt
Paste this into your agent from the root of your iOS project:
```text
Integrate Mobile Joe into this iOS app using SwiftUI.
Read https://docs.mobilejoe.dev/llms.txt and https://docs.mobilejoe.dev/ios/quickstart.md first.
Use SDK 1.0.1 (Exact Version) from https://github.com/MobileJoeDev/mobilejoe-ios.git.
Requirements: iOS 17+, Swift 6.2+, and Xcode 26+. Add both MobileJoe and MobileJoeUI package products to the app target.
Preserve the app's existing startup, theme, and navigation. Adapt this shared configuration to the existing SwiftUI App entry point and root view:
import SwiftUI
import MobileJoe
@main
struct MyApp: App {
init() {
try? Joe.configure(withAPIKey: "YOUR_API_KEY")
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Use the public SDK key above for this app in Mobile Joe. Call Joe.configure in the App initializer, before any Mobile Joe view appears, not in .task. Follow the quickstart's error handling.
Add a first FAQ integration using FAQView and the presentation and support options at https://docs.mobilejoe.dev/ios/faqs.md.
Publish a FAQ in the same app in Mobile Joe, then display its question and answer in the app.
Run the app and verify the iOS connection confirmation in App Settings → Set up SDK → iOS (https://app.mobilejoe.dev/setup?platform=iOS), then verify the published FAQ and its refresh behavior.
Relevant tool guides: https://docs.mobilejoe.dev/ios/feature-plans.md and https://docs.mobilejoe.dev/ios/alerts.md. Do not add additional tools unless requested or invent SDK APIs.
If configuration fails or content is missing, follow https://docs.mobilejoe.dev/sdk/troubleshooting.md.
Report the exact SDK version, build result, device validation, and anything you could not verify.
```
## Android prompt
Paste this into your agent from the root of your Android project:
```text
Integrate Mobile Joe into this Android app using Kotlin and Jetpack Compose.
Read https://docs.mobilejoe.dev/llms.txt and https://docs.mobilejoe.dev/android/quickstart.md first.
Use SDK 1.0.2 (dev.mobilejoe:mobilejoe-ui-compose) from Maven Central.
Requirements: Android 8.0+ (API 26), compileSdk 35+, Android Gradle Plugin 8.6+, and Kotlin 2.1+.
Preserve the existing Application, activity, theme, and navigation. Call Joe.configure directly in Application.onCreate, without a coroutine. Adapt this startup configuration:
import android.app.Application
import dev.mobilejoe.api.Joe
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
Joe.configure(
applicationContext = this,
apiKey = "YOUR_API_KEY",
)
}
}
Add the Compose dependency and register the Application class in the manifest as shown in the quickstart.
Display FaqHost with the app's support address after a FAQ is published in the dashboard.
Run the app and verify the Android connection confirmation in App Settings → Set up SDK → Android (https://app.mobilejoe.dev/setup?platform=Android), then verify the published FAQ.
Follow the quickstart's identity, localization, and refresh behavior; do not invent SDK APIs.
If configuration fails or content is missing, follow https://docs.mobilejoe.dev/sdk/troubleshooting.md.
Report the exact SDK version, build result, and any unverified device checks.
```
## Markdown sources
Append `.md` to any page path for Markdown, for example [/ios/quickstart.md](https://docs.mobilejoe.dev/ios/quickstart.md). [llms.txt](https://docs.mobilejoe.dev/llms.txt) indexes all guides, [llms-full.txt](https://docs.mobilejoe.dev/llms-full.txt) contains them in one file, and the [OpenAPI contract](https://docs.mobilejoe.dev/api/openapi.yaml) describes the REST API. All files are public.
To let an agent write and publish content in your dashboard instead, see [Manage content with AI agents](https://docs.mobilejoe.dev/mcp.md).
---
iOS · SwiftUI · SDK 1.0.1
# iOS quickstart
Install Mobile Joe and display a FAQ published from your dashboard.
## What you need
- An iOS app built with SwiftUI: iOS 17+, Swift 6.2+, and Xcode 26+.
- Your app in Mobile Joe with a published FAQ. See [Set up your app](https://docs.mobilejoe.dev/setup.md).
- Its public SDK key. [Set up SDK → iOS](https://app.mobilejoe.dev/setup?platform=iOS) shows the configuration below with your key filled in.
## Install the SDK
1. In Xcode, choose **File → Add Package Dependencies…**.
2. Enter this package URL:
```text
https://github.com/MobileJoeDev/mobilejoe-ios.git
```
Set the dependency rule to **Exact Version** , enter **1.0.1** , and add both `MobileJoe` and `MobileJoeUI` to your app target.
## Configure your app
In your existing SwiftUI `App` type, import `MobileJoe` and configure it in the initializer, before any Mobile Joe view appears. Keep your app's existing name and root view.
```swift
import SwiftUI
import MobileJoe
@main
struct MyApp: App {
init() {
try? Joe.configure(withAPIKey: "YOUR_API_KEY")
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
```
Replace `YOUR_API_KEY` with the public SDK key of your app in Mobile Joe. `configure` returns right away and registers the device in the background. Do not configure in `.task`: a view that appears first misses its content, such as an active alert at a cold start. The example ignores errors with `try?`; use `do`/`catch` to handle them.
## Check your connection
Run your app and keep [App Settings → Set up SDK → iOS](https://app.mobilejoe.dev/setup?platform=iOS) open in the dashboard. The indicator confirms a registered iOS device of this app. An earlier registration can also confirm it, so verify current content next.
## Show your FAQs
Replace your starter `ContentView` with this example, or add its navigation link to your existing `NavigationStack`.
```swift
import SwiftUI
import MobileJoeUI
struct ContentView: View {
var body: some View {
NavigationStack {
List {
NavigationLink("FAQs") {
FAQView(
presentationMode: .inNavigationStack,
configuration: .standard(recipient: "")
)
}
}
.navigationTitle("Help")
}
}
}
```
`FAQView` loads its own data. The empty support recipient hides the email link. Run your app, open **FAQs** , and confirm your published question and answer. Edit the answer in the dashboard and pull to refresh to see the update. See the [FAQ guide](https://docs.mobilejoe.dev/ios/faqs.md) for presentation and support options.
## Before you ship
For signed-in users, pass a stable account ID as `appUserID` to `Joe.configure` or call `try Joe.identify(appUserID:)` after sign-in, and `try Joe.logout()` after sign-out; see [Identity and data](https://docs.mobilejoe.dev/sdk/identity.md). Disable [debug mode](https://docs.mobilejoe.dev/sdk/content-delivery.md#debug-mode) in release builds.
## Troubleshooting
If the package cannot resolve, check the package URL and the exact version. For a missing connection or FAQ, filter the Xcode console for the subsystem `MobileJoe` and follow [Troubleshooting](https://docs.mobilejoe.dev/sdk/troubleshooting.md).
## Next steps
Add [Feature Plans](https://docs.mobilejoe.dev/ios/feature-plans.md), [FAQs](https://docs.mobilejoe.dev/ios/faqs.md), or [Alerts](https://docs.mobilejoe.dev/ios/alerts.md).
---
iOS · SwiftUI · SDK 1.0.1
# Feature Plans
Let app users explore your plans, vote on features, and follow your roadmap.
Complete the [iOS quickstart](https://docs.mobilejoe.dev/ios/quickstart.md) first. These examples use the configured app and the `MobileJoeUI` package product.
## Create a feature plan
In the [dashboard](https://app.mobilejoe.dev/), open **Feature Plans** and save a plan with status **Open**. Every status except **Draft** is visible in your app.
## Show the list
Add this screen to your app, or move its navigation link into your existing navigation stack:
```swift
import SwiftUI
import MobileJoeUI
struct FeedbackView: View {
var body: some View {
NavigationStack {
List {
NavigationLink("Feature Plans") {
FeaturePlansView(
configuration: .standard(recipient: "support@example.com"),
presentationMode: .inNavigationStack
)
}
}
.navigationTitle("Feedback")
}
}
}
```
`FeaturePlansView` loads data, fetches additional pages while scrolling, and supports pull to refresh. Tapping a plan opens its details. The vote button adds a vote; tapping it again removes that vote.
Search matches titles and descriptions. The toolbar menu offers newest-first or score sorting and the [status filters](https://docs.mobilejoe.dev/sdk/content-delivery.md#feature-plans).
## Present a sheet
Use `.asSheet` to get the SDK's navigation stack and close button:
```swift
import SwiftUI
import MobileJoeUI
struct FeaturePlansButton: View {
@State private var showingPlans = false
var body: some View {
Button("Feature Plans") { showingPlans = true }
.sheet(isPresented: $showingPlans) {
FeaturePlansView(
configuration: .standard(recipient: "support@example.com"),
presentationMode: .asSheet
)
}
}
}
```
## Suggestions and deep links
Replace `support@example.com` with your support address. The suggestion button opens the user's email app; it does not create a feature plan. Use `.standard(recipient: "")` to hide it. For custom localized email content, use `FeaturePlansView.Configuration(recipient:subject:body:)`.
To open a specific plan, pass its ID as `selectedFeaturePlanID:` after `presentationMode:`. Your app handles incoming URLs and extracts the ID. Save a **Deep link base URL** in the Feature Plans settings to copy deep links in the dashboard.
## Use your own UI
Import `MobileJoe` and keep an observable `FeaturePlans()` model in `@State`. Read `all`, and call `load()`, `reload()`, `vote(_:)`, `featurePlan(id:)`, or `search(for:)` on the main actor; set `sorting` and `filtering` to change the list.
## Verify
Open your saved plan, vote twice, and confirm the score returns to its starting value. Change its dashboard status to **Planned** , refresh, and check the roadmap filter. If it is missing, check the selected filter and follow [Troubleshooting](https://docs.mobilejoe.dev/sdk/troubleshooting.md#missing-content).
---
iOS · SwiftUI · SDK 1.0.1
# FAQs
Give app users answers inside your app, with an optional email link for further help.
Complete the [iOS quickstart](https://docs.mobilejoe.dev/ios/quickstart.md) first. These examples assume Mobile Joe is configured and `MobileJoeUI` is added to your app target.
## Publish answers
In the [dashboard](https://app.mobilejoe.dev/), open **FAQs** , create a FAQ, and choose **Publish → Publish now**. The SDK receives live FAQs in dashboard order, with [translations](https://docs.mobilejoe.dev/sdk/localization.md) when available.
## Add navigation
The [quickstart example](https://docs.mobilejoe.dev/ios/quickstart.md#show-your-faqs) pushes `FAQView` with `.inNavigationStack` onto your app's navigation stack. The view lists the question titles; tapping one opens its answer.
## Present a sheet
For a help button elsewhere in your app, use `.asSheet`. It supplies a navigation stack and close button:
```swift
import SwiftUI
import MobileJoeUI
struct HelpButton: View {
@State private var showingFAQs = false
var body: some View {
Button("Help") { showingFAQs = true }
.sheet(isPresented: $showingFAQs) {
FAQView(
presentationMode: .asSheet,
configuration: .standard(recipient: "support@example.com")
)
}
}
}
```
## Offer email support
A nonempty recipient adds a support hint and email link below the list of questions and below each answer. Replace `support@example.com` with your address, or use `.standard(recipient: "")` to hide both. The link opens the user's email app.
For your own localized wording, use `FAQView.Configuration(recipient:subject:body:contactSupportMailToLabel:contactSupportHint:)`. The subject and body are optional; the label and hint accept `LocalizedStringResource` values.
## Loading and custom UI
The standard view loads on appearance and supports pull to refresh. It includes loading, empty, and failure states; a failed refresh keeps the questions already shown.
For your own interface, import `MobileJoe`, keep an observable `FAQs()` model in `@State`, and call `try await faqs.load()` on the main actor after configuration. Read the returned `FAQItem` values from `faqs.all`; each exposes `id`, `title`, and `body`. Handle loading errors in your interface. You can also pass your model to `FAQView(faqs:presentationMode:configuration:)`.
## Verify
Open a published question, edit it in the dashboard, and pull to refresh. Test the support link on a device with email configured. If no questions appear, follow [Troubleshooting](https://docs.mobilejoe.dev/sdk/troubleshooting.md#missing-content).
---
iOS · SwiftUI · SDK 1.0.1
# Alerts
Display in-app warning and error banners for service problems or other issues affecting your users.
Complete the [iOS quickstart](https://docs.mobilejoe.dev/ios/quickstart.md) first. These examples use your configured app and `MobileJoeUI`.
## Deliver an alert
In the [dashboard](https://app.mobilejoe.dev/), open **Alerts** , create a **Warning** or **Error** alert, set its **Targeting** (or **Send to all users** ), and choose **Deliver → Deliver now**. Targeting matches exact app and OS versions; see [Content delivery](https://docs.mobilejoe.dev/sdk/content-delivery.md#alerts). Archive the alert when the issue is resolved.
## Place the banners
Add `AlertsView()` where users should see alerts. This example places them above the screen's list:
```swift
import SwiftUI
import MobileJoeUI
struct HomeView: View {
var body: some View {
NavigationStack {
List {
Text("Your app content")
}
.navigationTitle("Home")
.safeAreaInset(edge: .top, spacing: 0) {
AlertsView()
.padding(.horizontal)
}
}
}
}
```
The view shows one banner per alert, colored by kind. Tapping a banner opens its details; closing them leaves the banner visible, as alerts cannot be dismissed permanently.
## Refresh and testing
`AlertsView` loads on appearance and when the app becomes active, at most once every 15 minutes. Add `debugMode: true` to `Joe.configure` during integration to skip the interval and receive alerts with **Debug delivery** ; see [Debug mode](https://docs.mobilejoe.dev/sdk/content-delivery.md#debug-mode).
## Use your own UI
Import `MobileJoe` and keep an observable `Alerts()` model in `@State`. Call `try await alerts.load()` on the main actor and read `alerts.all`; the same interval applies. Handle errors yourself: the standard view hides them.
Pass a model to `AlertsView(alerts:)`, render individual banners with `AlertView(alert:)`, or build your own interface from each alert's `title`, `message`, `kind`, and `occurredAt`.
## Verify
Run the app, confirm your delivered alert appears, and open its details. Archive it, then trigger an eligible refresh and confirm it disappears. If it is missing, follow [Troubleshooting](https://docs.mobilejoe.dev/sdk/troubleshooting.md#missing-content).
---
Android · Kotlin · Jetpack Compose · SDK 1.0.2
# Android quickstart
Configure Mobile Joe and show a FAQ published from your dashboard.
## What you need
- An Android app built with Kotlin and Jetpack Compose: Android 8.0+ (API 26), compileSdk 35+, Android Gradle Plugin 8.6+, and Kotlin 2.1+.
- Your app in Mobile Joe with a published FAQ. See [Set up your app](https://docs.mobilejoe.dev/setup.md).
- Its public SDK key. [Set up SDK → Android](https://app.mobilejoe.dev/setup?platform=Android) shows the configuration below with your key already filled in, plus a prompt for your coding agent.
Start with an Android Studio Compose project with Material 3 and the Kotlin Compose compiler plugin enabled.
## Install the SDK
In `settings.gradle.kts`, include these dependency repositories:
```kotlin
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
```
Add this to `app/build.gradle.kts`:
```kotlin
dependencies {
implementation("dev.mobilejoe:mobilejoe-ui-compose:1.0.2")
}
```
The Compose module includes `dev.mobilejoe:mobilejoe-api` transitively. For a custom UI, depend on it alone. A working sample is in the [Android repository](https://github.com/MobileJoeDev/mobilejoe-android).
## Configure your app
In `app/src/main/java/your/package/MyApplication.kt`, retain your package declaration and add configuration to your existing Application class. Replace `YOUR_API_KEY` with the public SDK key of your app in Mobile Joe.
```kotlin
import android.app.Application
import dev.mobilejoe.api.Joe
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
Joe.configure(
applicationContext = this,
apiKey = "YOUR_API_KEY",
)
}
}
```
Merge the following into `app/src/main/AndroidManifest.xml`, using your actual Application class name. Keep existing activities and application attributes.
```xml
```
Call `Joe.configure` directly in `Application.onCreate`, without a coroutine. It returns right away and registers the device in the background. In a coroutine, a component can load first and miss its content, such as an active alert at a cold start.
## Check your connection
Run your app and keep [App Settings → Set up SDK → Android](https://app.mobilejoe.dev/setup?platform=Android) open in the dashboard. The indicator confirms a registered Android device of this app. An earlier registration can also confirm it, so verify current content next.
## Show your FAQs
Publish a recognizable FAQ in your app in Mobile Joe. Adapt `MainActivity.kt` below, keeping your package declaration, theme and existing navigation. Replace the support address.
```kotlin
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import dev.mobilejoe.ui.compose.faq.FaqHost
import dev.mobilejoe.ui.compose.faq.FaqUiConfiguration
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
FaqHost(
configuration = FaqUiConfiguration(
context = this,
recipient = "support@example.com",
),
)
}
}
}
}
```
Open the FAQ and check its answer. Edit it in the dashboard and pull to refresh to see the update. See the [FAQ guide](https://docs.mobilejoe.dev/android/faqs.md) for more options.
## Before you ship
For signed-in users, pass a stable account ID as `appUserId` to `Joe.configure` or call `Joe.identify(appUserId)` after sign-in, and `Joe.logout()` after sign-out; see [Identity and data](https://docs.mobilejoe.dev/sdk/identity.md). The SDK captures your app's languages during configuration, so configure after selecting the app language. Disable [debug mode](https://docs.mobilejoe.dev/sdk/content-delivery.md#debug-mode) in release builds.
## Troubleshooting
If the dependency cannot resolve, check `mavenCentral()` and the exact version. The SDK needs JDK 17 and JVM target 17 or later. For a missing connection or FAQ, filter Logcat for the tag `Joe` and follow [Troubleshooting](https://docs.mobilejoe.dev/sdk/troubleshooting.md).
## Next steps
Add [Feature Plans](https://docs.mobilejoe.dev/android/feature-plans.md), [FAQs](https://docs.mobilejoe.dev/android/faqs.md), or [Alerts](https://docs.mobilejoe.dev/android/alerts.md).
---
Android · Jetpack Compose · SDK 1.0.2
# Feature Plans
Share your roadmap, collect votes, and accept suggestions by email.
## Prepare your content
Complete the [Android quickstart](https://docs.mobilejoe.dev/android/quickstart.md), then save a feature plan in the dashboard with a status other than **Draft**. The status controls the [list filters](https://docs.mobilejoe.dev/sdk/content-delivery.md#feature-plans).
## Compose integration
```kotlin
import androidx.compose.runtime.Composable
import dev.mobilejoe.ui.compose.feature.FeaturePlanHost
import dev.mobilejoe.ui.compose.feature.FeaturePlanUiConfiguration
@Composable
fun RoadmapScreen(onNavigateUp: () -> Unit) {
FeaturePlanHost(
configuration = FeaturePlanUiConfiguration(
recipient = "support@example.com",
),
onNavigateUp = onNavigateUp,
)
}
```
Place this in `RoadmapScreen.kt` under your app package and use it as a destination in your existing Compose navigation inside your Material theme. The host handles list and detail navigation. Supply `selectedFeatureId` to open an existing plan directly. The current component name is `FeaturePlanHost`.
## Suggestions and filtering
The suggestion action opens email to `recipient`, with optional `subject` and `body`. It does not create an in-app submission form. The list provides search, sorting, and status filters; detail screens offer voting.
## Core API and refresh
For a custom UI, `dev.mobilejoe.api.feature.FeaturePlans` exposes suspending `load()`, `load(id)`, `reload()`, `vote(featurePlan)`, `setStatusFilter`, `setSorting`, and `setSearchQuery`. Run calls in a lifecycle-owned coroutine with error handling. Collection loads paginate; use `reload()` to restart the collection. The Compose screen manages its own loads and filter changes and supports pull to refresh, without automatic polling.
## Verify
Find your plan, open its detail, cast a vote and check its state. Try search and status filters, and test the suggestion email on a device with an email app. If a plan is missing, check the selected filter and follow [Troubleshooting](https://docs.mobilejoe.dev/sdk/troubleshooting.md#missing-content).
---
Android · Jetpack Compose · SDK 1.0.2
# FAQs
Show published answers and provide an email support contact.
## Prepare your content
Complete the [Android quickstart](https://docs.mobilejoe.dev/android/quickstart.md) and publish a FAQ in the dashboard.
## Compose integration
```kotlin
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import dev.mobilejoe.ui.compose.faq.FaqHost
import dev.mobilejoe.ui.compose.faq.FaqUiConfiguration
@Composable
fun HelpScreen(onNavigateUp: () -> Unit) {
FaqHost(
configuration = FaqUiConfiguration(
context = LocalContext.current,
recipient = "support@example.com",
),
onNavigateUp = onNavigateUp,
)
}
```
Place this in `HelpScreen.kt` under your app package and call it from your existing Compose navigation destination inside your Material theme. The host provides its own list-to-detail navigation: the list shows only the question titles, and tapping one opens its answer. Pass an existing published FAQ ID as `selectedFaqId` to start at its detail.
## Support configuration
Use your support address for `recipient`; optional `subject` and `body` prefill the email. The context constructor supplies localized support labels. The primary constructor accepts `contactSupportMailToLabel` and `contactSupportHint` for custom text. The support hint and email link appear below the list of questions and below each answer. Pass an empty `recipient`, or use `FaqUiConfiguration()`, to hide both.
## Loading and refresh
The screen loads on creation, offers retry after a load error, and supports pull to refresh. For a custom UI, `dev.mobilejoe.api.faq.Faqs` exposes `all` and `single` flows and suspending `load()`, `reload()`, and `load(id)` methods. Run these in your lifecycle-owned coroutine and handle errors. Repeated `load()` calls append to the same collection; use `reload()` to replace it.
## Verify
Check your published question and answer, then test the support action on a device with an email app. If loading fails or questions are missing, follow [Troubleshooting](https://docs.mobilejoe.dev/sdk/troubleshooting.md).
---
Android · Jetpack Compose · SDK 1.0.2
# Alerts
Display targeted announcements alongside your app's content.
## Prepare an alert
Complete the [Android quickstart](https://docs.mobilejoe.dev/android/quickstart.md) and deliver an alert in the dashboard. Start with **Send to all users** , then test your intended filters.
## Compose integration
```kotlin
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import dev.mobilejoe.ui.compose.alert.AlertsView
@Composable
fun HomeScreen() {
Column {
AlertsView()
Text("Welcome")
}
}
```
Place this in `HomeScreen.kt` under your app package and show it inside your existing Material theme. Put `AlertsView` where alerts belong in your layout. Tapping a banner opens its information sheet; dismissing the sheet does not permanently dismiss the alert.
## Loading and refresh
`AlertsView` loads when it is created and reloads whenever its screen starts, for example when the user returns to it. It renders nothing while loading, when empty, or on error; inspect Logcat for `Error loading alerts`. The Android component has no automatic polling or iOS-style refresh interval.
For a custom UI, `dev.mobilejoe.api.alert.Alerts` exposes an `all` flow and suspending `load()`. Each load requests data and replaces the collection. Invoke it from your lifecycle-owned coroutine with error handling and choose your own refresh trigger.
## Targeting and debug mode
Targeting matches the exact `versionName` and OS API level; see [Content delivery](https://docs.mobilejoe.dev/sdk/content-delivery.md#alerts). Pass `isDebugMode = true` to `Joe.configure` during development to receive alerts with **Debug delivery** ; see [Debug mode](https://docs.mobilejoe.dev/sdk/content-delivery.md#debug-mode).
## Verify
Run with debug mode disabled, confirm the published alert appears for an eligible device, and open its details. Check that excluded devices do not receive it. If nothing appears, follow [Troubleshooting](https://docs.mobilejoe.dev/sdk/troubleshooting.md#missing-content).
---
Content via MCP
# Manage content with AI agents
Connect an agent to manage FAQs, translations, feature plans, and app settings. You choose which apps it can access and what it can do; Mobile Joe applies the same rules as the dashboard.
## Connect
Add `https://app.mobilejoe.dev/mcp` as a remote MCP server with OAuth authentication. Leave optional client ID and secret fields empty. When you connect, sign in to Mobile Joe, select the apps, and choose an access level. Then ask the agent: **List my Mobile Joe apps.**
### Claude
Open **Customize → Connectors** , choose **Add custom connector** , name it **Mobile Joe** , and click **Connect**. Your workspace may require an owner to add it first. Claude's **Needs approval** setting is separate from the access granted in Mobile Joe. See [Claude's connector guide](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp).
### Codex
In the Codex desktop app, add an HTTP MCP server named **Mobile Joe** and start OAuth sign-in. With the CLI, request only the scopes you need:
```shell
codex mcp add mobilejoe --url https://app.mobilejoe.dev/mcp
codex mcp login mobilejoe --scopes faqs:read,faqs:write,faqs:publish,feature_plans:read,feature_plans:write,feature_plans:publish,apps:write
```
### ChatGPT
Enable developer mode in ChatGPT's settings in the browser; your workspace must allow custom MCP apps. Add a custom MCP app named **Mobile Joe** and connect it. If ChatGPT asks to reconnect for additional permissions, review the expanded access.
### Other agents
Any agent that supports remote MCP servers with OAuth works. Settings and names vary by client.
## Choose access
When you connect, choose an access level for FAQs, feature plans, and app settings. It applies to every selected app, and only levels your agent requests are offered.
- **Read only** (default): inspect FAQs, translations, feature plans, and votes.
- **Read & write:** also create and edit drafts and translations, and merge draft feature plans.
- **Read, write & publish:** also change published FAQs and visible feature plans, reorder FAQs, and set feature plan statuses.
- **App settings — Edit:** change an app's title, website, and deep link URL.
## Work with FAQs
> Create FAQs for the new export feature, add German and French translations, and save them as drafts. Show me the result. Wait for my approval before publishing.
New FAQs stay drafts until you ask the agent to publish them. The agent sees the original text and each translation separately, and updating one language preserves the others. Changes to published FAQs are visible to app users immediately, so ask the agent to show proposed changes first.
## Curate feature plans
> A customer emailed asking for a CSV export of all time entries. Does this fit our roadmap? Check for overlapping plans and suggest how we could phrase it.
The agent treats user requests as input for your decision: it looks for overlapping plans and suggests a formulation before creating one. New plans are drafts until you approve a status such as **Open**. When merging duplicates, votes move to the plan you keep and count once per app user.
## Disconnect or reconnect
Open **Account → Connections** in the dashboard to inspect or revoke a connection. Reconnecting for more permissions creates a separate connection; revoke the older entry, identified by its creation time.
---
iOS · Android · SDK
# Identity and data
How the SDK identifies app users, connects them to your accounts, and what it stores and sends.
## Anonymous by default
On first configuration, the SDK creates an anonymous identity and stores it on the device. Later launches reuse it. Apps without their own accounts need nothing else and should not call `identify`. Feature plan votes belong to the identity; FAQs and alerts are the same for every identity.
## Sign in
Pass your account ID during configuration when the user is already signed in at launch, or identify the user after sign-in:
| Platform | At configuration | After sign-in |
| --- | --- | --- |
| iOS | `try Joe.configure(withAPIKey:appUserID:)` | `try Joe.identify(appUserID:)` |
| Android | `Joe.configure(applicationContext, apiKey, appUserId = …)` | `Joe.identify(appUserId)` |
The first identification keeps the anonymous ID, so votes cast before sign-in carry over to the account. Identifying the same account again changes nothing, and configuring without an app user ID keeps the stored identity, including a signed-in account. Devices identified with the same app user ID share their votes.
## Choose an app user ID
- **Stable and unique:** an ID that belongs to exactly one account and never changes, such as your account's primary key.
- **Not personal data:** no email address or phone number.
- **Not temporary:** no placeholders, receipt IDs, or purchase SDK IDs that change after a restore, such as anonymous RevenueCat IDs.
- **Not empty:** a blank ID throws `MobileJoeError.invalidAppUserID` on iOS and `IllegalArgumentException` on Android.
## Sign out and switch accounts
After sign-out, call `Joe.logout()`. The SDK continues with new anonymous and device IDs; the account keeps its votes. Identifying a different account logs out first, so you do not need to call `logout()` before switching.
## Identify requests
The SDK sends the identity to Mobile Joe in the background, once per app start and once per identity change. If that fails, the SDK stays usable and retries on the next app start.
## Order of calls
Configure once at app start; further `configure` calls change nothing until the app relaunches. `identify` and `logout()` take effect immediately, in call order, and throw when the identity cannot be stored. `identify` also throws before configuration; `logout()` does too on Android, while on iOS it replaces the stored identity locally.
## Identifiers
| Identifier | Format | Purpose |
| --- | --- | --- |
| Anonymous ID | `$MBJAnonymousID:` followed by a random UUID as 32 hex characters | Identifies the app user, for example for votes. Created without an account. |
| Device ID | `$MBJDeviceID:` followed by a random UUID as 32 hex characters | Identifies the installation of an app user. It is generated by the SDK, not read from the hardware. |
| External ID | `$MBJExternalID:` followed by the first 32 hex characters of the unsalted SHA-256 hash of your app user ID | Optional. Associates the app user with an account in your app across devices. Only set after configuring or identifying with an app user ID. |
The SDK never sends the app user ID itself. The hash is unsalted, so use an opaque account ID.
## Local storage
Both SDKs store exactly one identity: the anonymous ID, the device ID, and the external ID if set.
| Platform | Location | Format |
| --- | --- | --- |
| iOS | `Application Support/MobileJoe/identity.mbj` in the app container | JSON file, written without a Data Protection class |
| Android | Private SharedPreferences file `dev.mobilejoe.identity` | Keys `anonymous_id`, `device_id`, and `external_id` |
The SDKs store nothing else, apart from the iOS [response cache](https://docs.mobilejoe.dev/sdk/identity.md#response-cache).
## Backups
The SDKs do not exclude the identity from backups. Platform rules apply:
- **iOS:** the Application Support directory is part of iCloud and computer backups. To exclude the identity, set `isExcludedFromBackup` on the `Application Support/MobileJoe` directory.
- **Android:** SharedPreferences are part of Auto Backup unless your app disables or limits it. To exclude the identity, add `dev.mobilejoe.identity.xml` in the `sharedpref` domain to your backup rules.
```xml
```
On Android 12 and higher, add the rule to `cloud-backup` and `device-transfer` in your data extraction rules. On earlier versions, add it to your full backup content rules.
## Request metadata
Each request to the API contains these headers:
| Header | iOS | Android |
| --- | --- | --- |
| `Authorization` | Public SDK API key | Public SDK API key |
| `Identity-Anonymous-ID` | Anonymous ID | Anonymous ID |
| `Identity-Device-ID` | Device ID | Device ID |
| `Identity-External-ID` | External ID, only when set | External ID, only when set |
| `Framework-Version` | SDK version | SDK version |
| `App-Version` | `CFBundleShortVersionString` | `versionName` |
| `App-Build-Version` | `CFBundleVersion` | `versionCode` |
| `OS-Name` | `iOS` | `Android` |
| `OS-Version` | System version, for example `18.2.0` | API level, for example `35` |
| `Device-Model` | Model identifier, for example `iPhone16,2` | `Build.MODEL`, for example `Pixel 8` |
| `Device-Brand` | Not sent | `Build.BRAND`, for example `google` |
| `Preferred-Languages` | Up to three languages of `Locale.preferredLanguages` at the first request | Up to three app locales at configuration |
| `Debug-Mode` | `debugMode` passed to `Joe.configure` | `isDebugMode` passed to `Joe.configure` |
Requests also contain the parameters of the operation, such as a search term or a feature plan ID. See the [REST API reference](https://docs.mobilejoe.dev/api.md).
## Response cache
The iOS SDK uses `URLSession.shared`, whose `URLCache` stores API responses in the Caches directory. The system can clear it at any time, and it is not backed up. The Android SDK does not cache responses.
---
iOS · Android · SDK
# Content delivery
Which dashboard content reaches your app, when it refreshes, and how debug mode helps you test alerts.
## The same app in Mobile Joe
The public SDK key selects your app in Mobile Joe. iOS and Android use the same key and receive the same content; content of your other apps never appears.
## FAQs
New FAQs start as drafts. **Publish → Publish now** makes a FAQ live, and the SDK receives live FAQs in dashboard order. Drafts and archived FAQs are not delivered. Edits to a live FAQ reach your app with the next load.
## Alerts
An alert needs targeting before it can be delivered: enable **Send to all users** , or add at least one iOS or Android version filter. **Deliver → Deliver now** makes it live; archiving removes it from your app. Filters match the exact values the SDK sends:
| Filter | iOS | Android |
| --- | --- | --- |
| App versions | `CFBundleShortVersionString`, for example `2.4` | `versionName`, for example `2.4.0` |
| OS versions | System version with patch component, for example `18.5.0` | API level, for example `35` |
- When both filters of a platform contain values, both must match. An alert with filters for only one platform does not reach the other.
- Build numbers are not used for targeting.
- The filters offer only versions that devices of your app have reported. Run a new version once with the SDK configured to select it.
## Feature Plans
Draft feature plans are hidden. Every other status is visible and open for votes, so changing a draft to a visible status publishes it. The status controls where the standard screens list a plan:
| Filter | Statuses |
| --- | --- |
| All (default) | Open, Under review, Planned, In progress |
| Roadmap | Under review, Planned, In progress |
| Completed | Completed |
Closed plans appear in none of these lists, but a deep link still opens them.
## When content refreshes
The SDKs do not poll. Dashboard changes reach your app with the next request:
- **FAQs:** when the screen appears (iOS) or is created (Android), on pull to refresh, and on retry.
- **Feature Plans:** the same, plus while scrolling and on filter, sort, or search changes.
- **Alerts:** on iOS on appearance and when the app becomes active, at most once every 15 minutes, also in your own UI; on Android whenever their screen starts.
## Debug mode
Pass `debugMode: true` on iOS or `isDebugMode = true` on Android to `Joe.configure`. Debug mode changes only alerts: alerts with **Debug delivery** enabled reach debug-mode apps before delivery and regardless of targeting, and the iOS 15-minute interval does not apply. Disable debug delivery or debug mode before you test targeting.
Debug mode uses the live API, so votes are real votes. Enable it only in development builds, for example with `#if DEBUG` or `BuildConfig.DEBUG`, and relaunch the app after switching it.
## Test an alert
1. Create a draft alert with **Debug delivery** and confirm it appears with debug mode enabled.
2. Target your device's version, deliver the alert, and confirm it appears with debug mode disabled.
3. Archive it and confirm it disappears after the next refresh.
---
iOS · Android · SDK
# Localization
How the SDK requests languages, how the API selects translated content, and which texts your app controls.
## Requested language
The SDK sends language tags with region and script, such as `pt-BR` or `zh-Hant-TW`, with each request.
| Platform | Source | When it changes |
| --- | --- | --- |
| iOS | Up to three languages of `Locale.preferredLanguages`, in the user's order | Captured at the first request; a language change applies after the app relaunches. |
| Android | Up to three of the app's configured locales, in order | Captured during `Joe.configure`; a language change applies after the app relaunches. |
On iOS, the languages follow the user's language settings, not the localizations of your app. A German user of an English-only app gets German FAQs if you added a `de` translation, while your app and the SDK screens stay in English.
## Translated content
Add translations to FAQs and alerts in the dashboard. For each requested language in order, the API uses an exact translation such as `pt-BR`, then the language translation `pt`. Without a match, or when the requested language is your app's source language, it returns the original text. For `en-GB,de-DE` with English original text, users get the original text, not the German translation.
- A language translation such as `pt` serves every region. Regional translations (`en-AU`, `en-CA`, `en-GB`, `en-US`, `fr-CA`, `pt-BR`, `pt-PT`) serve only their region, also for the source language.
- A translation replaces the whole text: an alert translation without a message shows no message.
- Feature plans are not translated. FAQs keep their dashboard order in every language.
Change the source language, English by default, in the app settings.
## Interface labels
The standard screens include labels in English and German. The configuration of FAQs and Feature Plans accepts your own email subject and body; the FAQ configuration also accepts support labels.
## Test a translation
Add a `de` translation to a live FAQ, set your device or app language to German, relaunch the app, and open the FAQs. If the original text remains, check the translation's language code and follow [Troubleshooting](https://docs.mobilejoe.dev/sdk/troubleshooting.md#missing-content).
---
iOS · Android · SDK
# Troubleshooting
Find the cause when configuration fails, requests are rejected, or content does not appear in your app.
## Start here
1. The SDK key and the content belong to the same app in [Mobile Joe](https://app.mobilejoe.dev/).
2. `Joe.configure` runs at app start, before any Mobile Joe screen appears.
3. [App Settings → Set up SDK](https://app.mobilejoe.dev/setup) confirms a registered device of your platform.
4. The content is live, targeted at your device, and in the selected list filter; see [Content delivery](https://docs.mobilejoe.dev/sdk/content-delivery.md).
5. The [SDK logs](https://docs.mobilejoe.dev/sdk/troubleshooting.md#logs) show no error.
## Initialization
- **Screens load too early:** configure in the `App` initializer on iOS and directly in `Application.onCreate` on Android. Configuring in `.task` or a coroutine lets a screen load first: iOS shows “Sorry! Something went wrong.”, Android “Something went wrong!”, and `AlertsView` misses an active alert.
- **Not configured:** iOS throws `MobileJoeError.notConfigured`, Android an `IllegalStateException`.
- **Configuration errors:** `configure` rejects an empty app user ID and fails when the identity cannot be stored. Replace the quickstart's `try?` with `do`/`catch` to see the error on iOS; Android logs it with the tag `Joe`.
- **Android:** register your `Application` class in the manifest with `android:name`. `StrictMode` disk-access warnings from `configure` are expected.
## Rejected requests
| Status | Cause | Fix |
| --- | --- | --- |
| 401 | Unknown or missing SDK key | Copy the key again from [Set up SDK](https://app.mobilejoe.dev/setup); check for whitespace and a key of another app. |
| 403 | No API access | Verify the account's email address and keep a trial or subscription active. |
| 429 | Rate limit exceeded | Wait a minute; avoid configuring or voting in a loop. |
A 401 disables the SDK until the app restarts with a valid key: iOS throws `MobileJoeError.invalidAPIKey`, then `notConfigured`; Android throws `JoeInvalidApiKeyException`, then `IllegalStateException`. Other statuses surface as `MobileJoeError.httpFailure` or `JoeHttpException` with status, method, and path.
## Missing content
| Symptom | Check |
| --- | --- |
| No FAQs | The FAQ is live, not a draft or archived. |
| No alerts | Alert views hide errors, so check the [logs](https://docs.mobilejoe.dev/sdk/troubleshooting.md#logs) first. Then check that the alert is live and targets your exact app and OS version. On iOS, wait 15 minutes or use [debug mode](https://docs.mobilejoe.dev/sdk/content-delivery.md#debug-mode). |
| Feature plan missing | The selected filter includes its status; closed plans are never listed. |
| Changes do not appear | Pull to refresh or reopen the screen. The SDKs do not poll. |
| Original text instead of a translation | A translation exists for a requested language; see [Localization](https://docs.mobilejoe.dev/sdk/localization.md). |
| Votes disappear after sign-in or sign-out | Votes belong to the identity; see [Identity and data](https://docs.mobilejoe.dev/sdk/identity.md). |
An empty FAQ or Feature Plan list without an error means the request succeeded and returned nothing. Previews and sample data never confirm the connection.
## Logs
**iOS:** filter Xcode's console or Console.app for `subsystem:MobileJoe`. The standard screens do not log their errors; load a model yourself, for example `try await FAQs().load()`, and log what it throws.
**Android:** filter Logcat for the tag `Joe`. The Compose screens log under their view model's tag, for example `Error loading alerts` from `AlertsViewModel`. SLF4J warnings about missing providers at the first request are harmless; add `runtimeOnly("org.slf4j:slf4j-nop:2.0.20")` to silence them.
Messages to look for:
- “SDK configured successfully.”
- “Failed to identify … Retrying on next app start.”: the identify request failed; the SDK stays usable.
- “Invalid API key. The SDK is disabled until the app restarts with a valid key.”
---
REST API
# Use Mobile Joe over HTTP
Read app content, register devices, and toggle Feature Plan votes using an app API key.
[Open the dashboard](https://app.mobilejoe.dev/) · [Download OpenAPI](https://docs.mobilejoe.dev/api/openapi.yaml) · [Response schemas](https://docs.mobilejoe.dev/api/schemas.md)
## First request
Copy your app's API key from App Settings in the dashboard and replace YOUR\_API\_KEY below. The base URL is https://mbj-api.com/v1, not the dashboard host. An empty array [] means the request succeeded and no FAQs are published.
```sh
curl --include --request GET 'https://mbj-api.com/v1/faqs' \
--header 'Authorization: Token token=YOUR_API_KEY'
```
## Authentication
Every operation requires Authorization: Token token=YOUR\_API\_KEY; Bearer YOUR\_API\_KEY is also accepted. A key selects one app, so IDs of another app return 404. Missing or invalid keys return 401; an account without a verified email or an active trial or subscription returns 403.
## Identity without an SDK
Feature Plan operations and device registration require Identity-Anonymous-ID: $MBJAnonymousID: followed by a random lowercase UUID without hyphens, persisted per installation. For a signed-in user, also send Identity-External-ID: $MBJExternalID: followed by the first 32 lowercase hexadecimal characters of SHA-256 over the UTF-8 bytes of your user ID; the server does not hash raw user IDs. The first request that pairs both identities links them and moves anonymous votes and devices to an existing account. Generate new anonymous and device identifiers on sign-out and before switching accounts, as the SDKs do. FAQs and Alerts do not use identity headers.
## Device registration
Persist a device identifier, $MBJDeviceID: followed by a lowercase UUID without hyphens, and send it to Register or update device. Repeating the registration updates the same device; omitted metadata headers clear their stored values.
## Retries and limits
Voting toggles: repeating a successful POST removes the vote, so after a timeout fetch the plan and inspect voted before retrying. Limits are 300 requests per minute, plus 30 per minute each for identify and voting, per API key and client IP. Back off after a 429; no Retry-After header is promised. Do not share identity-dependent responses between users.
## Operations
| Method | Path (relative to /v1) | Reference |
| --- | --- | --- |
| GET | `/feature_plans` | [List feature plans](https://docs.mobilejoe.dev/api/list-feature-plans.md) |
| GET | `/feature_plans/{id}` | [Get a feature plan](https://docs.mobilejoe.dev/api/get-feature-plan.md) |
| POST | `/feature_plans/{id}/vote` | [Toggle vote on a feature plan](https://docs.mobilejoe.dev/api/vote-feature-plan.md) |
| GET | `/alerts` | [List alerts](https://docs.mobilejoe.dev/api/list-alerts.md) |
| GET | `/faqs` | [List FAQs](https://docs.mobilejoe.dev/api/list-faqs.md) |
| POST | `/identify` | [Register or update device](https://docs.mobilejoe.dev/api/identify-device.md) |
| GET | `/faqs/{id}` | [Get an FAQ](https://docs.mobilejoe.dev/api/get-faq.md) |
## Headers
Each operation lists the headers it accepts; _Required_ applies to those operations only. Every request also needs the [Authorization](https://docs.mobilejoe.dev/api.md#authentication) header.
| Name / location | Required / type | Details |
| --- | --- | --- |
| `Identity-Anonymous-ID` / header | Yes / string | Required persisted anonymous transport identity, including when an external identity is present. See the introduction for generation and merging. Pattern: `^\$MBJAnonymousID:[a-z0-9]{32}$` Example: `"$MBJAnonymousID:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"` |
| `Identity-External-ID` / header | No / string | Optional external transport identity. Use the prefixed truncated SHA-256 value, not a raw SDK-facing user ID. See the introduction. Pattern: `^\$MBJExternalID:[a-z0-9]{32}$` Example: `"$MBJExternalID:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"` |
| `OS-Name` / header | No / string | Operating system. Alert filtering recognizes ios and android case-insensitively; unknown or absent values do not match filtered audiences. Example: `"ios"` |
| `App-Version` / header | No / string | App version string. Alert audience filters use exact string membership, not semantic version ranges. Example: `"1.5.0"` |
| `OS-Version` / header | No / string | OS version string. Alert audience filters use exact string membership, not semantic version ranges. Example: `"17.5"` |
| `Preferred-Languages` / header | No / string | Comma-separated BCP 47 language tags in the user's priority order. Takes precedence over Language-Code. Empty, invalid, and duplicate entries are ignored; at most five valid entries are used. FAQs and Alerts resolve each entry like Language-Code before trying the next one; if no entry matches, they return the original content. An entry that matches the app's source language returns the original content. Identify stores the first entry as device metadata. Example: `"de-DE,en-GB"` |
| `Language-Code` / header | No / string | BCP 47 language tag, used when Preferred-Languages has no valid entry. FAQs and Alerts use the most specific localization (exact tag, language and script, language and region, then language). Values matching the app's source language, absent values, and unmatched values return the original content. No Accept-Language negotiation. Identify stores the normalized tag as device metadata. Example: `"en"` |
## Errors
Errors return a JSON body with an `error` message, for example `{"error":"Unauthorized"}`.
| Status | Meaning | Body |
| --- | --- | --- |
| 400 | Missing or malformed request information | `{"error":"Invalid request"}` |
| 401 | Invalid or missing API token | `{"error":"Unauthorized"}` |
| 403 | The app owner does not currently have API access. | `{"error":"Forbidden"}` |
| 404 | Resource or page not found | `{"error":"Resource not found"}` |
| 422 | Invalid request data or headers | `{"error":"Invalid request"}` |
| 429 | Request limit exceeded. Back off before retrying; no Retry-After header is promised. | `{"error":"Rate limit exceeded"}` |
---
REST API
# List feature plans
`GET https://mbj-api.com/v1/feature_plans`
Lists published plans of every status, including completed and closed; drafts are excluded. No debug filter. Content is not localized. Search matches all query words as prefixes in the original title/body. Pagination returns a JSON array, with counts and navigation in response headers. Page 1 is valid for an empty list.
## Request
```sh
curl --include --request GET 'https://mbj-api.com/v1/feature_plans' \
--header 'Authorization: Token token=YOUR_API_KEY' \
--header 'Identity-Anonymous-ID: $MBJAnonymousID:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
```
## Parameters
| Name / location | Required / type | Details |
| --- | --- | --- |
| `search` / query | No / string | Full-text search on title and body (case-insensitive, partial matches) Example: `"export pdf"` |
| `statuses[]` / query | No / array of FeaturePlanStatus | Repeat the key, for example statuses[]=open&statuses[]=planned. Case-sensitive OR filter; unknown values match nothing. Omit for all statuses. Example: `["open","planned"]` |
| `sort` / query | No / string | Defaults to updated\_desc for absent or unknown values. created sorts newest first; score aliases score\_desc. Status sorts alphabetically. roadmap orders in\_progress, planned, under\_review, then all others, with newest updates first within groups; it does not filter statuses. Values: `created, score, title_asc, title_desc, status_asc, status_desc, score_asc, score_desc, updated_asc, updated_desc, roadmap` Default: `updated_desc` |
| `limit` / query | No / integer | Items per page; converted to an integer and clamped to 1–100. Omitted: 20; nonnumeric values: 1. Default: `20` |
| `page` / query | No / integer | One-based page number. Omission defaults to 1. Nonnumeric, fractional, and non-positive values return 400; pages beyond the last available page return 404. Page 1 supports an empty result set. Default: `1` Example: `1` |
Headers: `Identity-Anonymous-ID` (required), `Identity-External-ID`. See [Headers](https://docs.mobilejoe.dev/api.md#headers).
## Responses
### 200 — Successful response
| Response header | Type / details |
| --- | --- |
| `current-page` | integer — Current page number |
| `page-items` | integer — Configured items per page |
| `total-count` | integer — Total matching plans |
| `total-pages` | integer — Total pages |
| `link` | string — Pagination links with rel="first", "prev", "next", and "last" as applicable. |
`application/json` · [array of FeaturePlan schema](https://docs.mobilejoe.dev/api/schemas.md)
```json
[
{
"id": 1,
"title": "PDF Export",
"body": "Allow exporting data to PDF format for offline viewing",
"score": 42,
"status": "open",
"voted": false,
"created_at": "2024-01-08T12:00:00Z",
"updated_at": "2024-01-08T12:00:00Z"
}
]
```
Errors: 400, 401, 403, 404, 422, and 429. See [Errors](https://docs.mobilejoe.dev/api.md#errors).
---
REST API
# Get a feature plan
`GET https://mbj-api.com/v1/feature_plans/{id}`
Returns one published plan with voted for the supplied identity. Drafts return 404; no localization or debug filter.
## Request
```sh
curl --include --request GET 'https://mbj-api.com/v1/feature_plans/1' \
--header 'Authorization: Token token=YOUR_API_KEY' \
--header 'Identity-Anonymous-ID: $MBJAnonymousID:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
```
Replace the example ID with an ID returned by the corresponding list operation.
## Parameters
| Name / location | Required / type | Details |
| --- | --- | --- |
| `id` / path | Yes / integer | Feature plan ID Example: `1` |
Headers: `Identity-Anonymous-ID` (required), `Identity-External-ID`. See [Headers](https://docs.mobilejoe.dev/api.md#headers).
## Responses
### 200 — Successful response
`application/json` · [FeaturePlan schema](https://docs.mobilejoe.dev/api/schemas.md)
```json
{
"id": 1,
"title": "PDF Export",
"body": "Allow exporting data to PDF format for offline viewing",
"score": 42,
"status": "open",
"voted": false,
"created_at": "2024-01-08T12:00:00Z",
"updated_at": "2024-01-08T12:00:00Z"
}
```
Errors: 400, 401, 403, 404, 422, and 429. See [Errors](https://docs.mobilejoe.dev/api.md#errors).
---
REST API
# Toggle vote on a feature plan
`POST https://mbj-api.com/v1/feature_plans/{id}/vote`
Toggles the supplied identity’s vote in any published plan; drafts return 404. No body is required. Not idempotent: retrying can undo a vote. After an ambiguous timeout, GET the plan and inspect voted before deciding whether to toggle again. The response includes the resulting voted state and current score.
## Request
```sh
curl --include --request POST 'https://mbj-api.com/v1/feature_plans/1/vote' \
--header 'Authorization: Token token=YOUR_API_KEY' \
--header 'Identity-Anonymous-ID: $MBJAnonymousID:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
```
Replace the example ID with an ID returned by the corresponding list operation.
## Parameters
| Name / location | Required / type | Details |
| --- | --- | --- |
| `id` / path | Yes / integer | Feature plan ID Example: `1` |
Headers: `Identity-Anonymous-ID` (required), `Identity-External-ID`. See [Headers](https://docs.mobilejoe.dev/api.md#headers).
## Responses
### 200 — Vote toggled successfully
`application/json` · [FeaturePlan schema](https://docs.mobilejoe.dev/api/schemas.md)
```json
{
"id": 1,
"title": "PDF Export",
"body": "Allow exporting data to PDF format for offline viewing",
"score": 42,
"status": "open",
"voted": false,
"created_at": "2024-01-08T12:00:00Z",
"updated_at": "2024-01-08T12:00:00Z"
}
```
Errors: 400, 401, 403, 404, 422, and 429. See [Errors](https://docs.mobilejoe.dev/api.md#errors).
---
REST API
# List alerts
`GET https://mbj-api.com/v1/alerts`
Returns an unpaginated array; no ordering is guaranteed. Normally returns live alerts for all users or matching the device audience. Filtered audiences require a recognized OS and configured filters for that platform. Each configured app/OS version list must contain the corresponding header exactly; missing versions fail configured lists. Alerts without an audience are not normally delivered. Debug-Mode: true (case-insensitive) additionally includes every non-archived debug-marked alert, including drafts, bypassing audience filters. Archived alerts are always excluded. Localized by Preferred-Languages or Language-Code with original-content fallback. No identity headers required.
## Request
```sh
curl --include --request GET 'https://mbj-api.com/v1/alerts' \
--header 'Authorization: Token token=YOUR_API_KEY'
```
## Parameters
| Name / location | Required / type | Details |
| --- | --- | --- |
| `Debug-Mode` / header | No / string | Enable debug mode to include draft alerts marked for debugging Values: `true, false` Example: `"false"` |
Headers: `OS-Name`, `App-Version`, `OS-Version`, `Preferred-Languages`, `Language-Code`. See [Headers](https://docs.mobilejoe.dev/api.md#headers).
## Responses
### 200 — Successful response
`application/json` · [array of Alert schema](https://docs.mobilejoe.dev/api/schemas.md)
```json
[
{
"id": 1,
"title": "Service Outage",
"message": "Some users may experience login issues",
"status": "live",
"kind": "error",
"occurred_at": "2024-01-08T00:00:00Z",
"created_at": "2024-01-08T12:00:00Z",
"updated_at": "2024-01-08T12:00:00Z"
}
]
```
Errors: 401, 403, and 429. See [Errors](https://docs.mobilejoe.dev/api.md#errors).
---
REST API
# List FAQs
`GET https://mbj-api.com/v1/faqs`
Returns live FAQs sorted by position, with no pagination. Search matches all query words as prefixes against the original title/body, not translations. Localized by Preferred-Languages or Language-Code with original-content fallback. No identity headers required.
## Request
```sh
curl --include --request GET 'https://mbj-api.com/v1/faqs' \
--header 'Authorization: Token token=YOUR_API_KEY'
```
## Parameters
| Name / location | Required / type | Details |
| --- | --- | --- |
| `search` / query | No / string | Full-text search on title and body Example: `"password reset"` |
Headers: `Preferred-Languages`, `Language-Code`. See [Headers](https://docs.mobilejoe.dev/api.md#headers).
## Responses
### 200 — Successful response
`application/json` · [array of Faq schema](https://docs.mobilejoe.dev/api/schemas.md)
```json
[
{
"id": 1,
"title": "How do I reset my password?",
"body": "Click the 'Forgot Password' link on the login page...",
"position": 1,
"created_at": "2024-01-08T12:00:00Z",
"updated_at": "2024-01-08T12:00:00Z"
}
]
```
Errors: 401, 403, and 429. See [Errors](https://docs.mobilejoe.dev/api.md#errors).
---
REST API
# Register or update device
`POST https://mbj-api.com/v1/identify`
Creates or updates a device for the resolved app user. Send a nonblank Identity-Device-ID header OR device\_id in JSON or query parameters; the header takes precedence. Device metadata comes only from headers; omitted metadata is cleared. Identity may be created/merged before missing device data returns 400. Successful response has no body.
## Request
```sh
curl --include --request POST 'https://mbj-api.com/v1/identify' \
--header 'Authorization: Token token=YOUR_API_KEY' \
--header 'Identity-Anonymous-ID: $MBJAnonymousID:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' \
--header 'Identity-Device-ID: $MBJDeviceID:cccccccccccccccccccccccccccccccc'
```
## Parameters
| Name / location | Required / type | Details |
| --- | --- | --- |
| `Identity-Device-ID` / header | No / string | Required unless device\_id is supplied in JSON or query. Nonblank device identifier, persisted across requests. Takes precedence over device\_id. Example: `"$MBJDeviceID:cccccccccccccccccccccccccccccccc"` |
| `App-Build-Version` / header | No / string | Application build number Example: `"8796845"` |
| `Device-Model` / header | No / string | Device model name Example: `"iPhone 15 Pro"` |
| `Framework-Version` / header | No / string | Mobile Joe SDK version Example: `"2.1.0"` |
| `device_id` / query | No / string | Alternative to Identity-Device-ID or the JSON property; provide at least one device ID. |
Headers: `Identity-Anonymous-ID` (required), `Identity-External-ID`, `App-Version`, `OS-Name`, `OS-Version`, `Preferred-Languages`, `Language-Code`. See [Headers](https://docs.mobilejoe.dev/api.md#headers).
## Request body
Optional alternative device ID. Other device metadata is read only from headers.
`application/json`
```json
{
"device_id": "$MBJDeviceID:cccccccccccccccccccccccccccccccc"
}
```
## Responses
### 204 — Device registered/updated successfully
No response body.
Errors: 400, 401, 403, 422, and 429. See [Errors](https://docs.mobilejoe.dev/api.md#errors).
---
REST API
# Get an FAQ
`GET https://mbj-api.com/v1/faqs/{id}`
Returns one live FAQ. Draft, archived, missing, or other-app IDs return 404. Localized by Preferred-Languages or Language-Code with original-content fallback.
## Request
```sh
curl --include --request GET 'https://mbj-api.com/v1/faqs/1' \
--header 'Authorization: Token token=YOUR_API_KEY'
```
Replace the example ID with an ID returned by the corresponding list operation.
## Parameters
| Name / location | Required / type | Details |
| --- | --- | --- |
| `id` / path | Yes / integer | Published FAQ ID from List FAQs. Example: `1` |
Headers: `Preferred-Languages`, `Language-Code`. See [Headers](https://docs.mobilejoe.dev/api.md#headers).
## Responses
### 200 — Published FAQ
`application/json` · [Faq schema](https://docs.mobilejoe.dev/api/schemas.md)
```json
{
"id": 1,
"title": "How do I reset my password?",
"body": "Click the 'Forgot Password' link on the login page...",
"position": 1,
"created_at": "2024-01-08T12:00:00Z",
"updated_at": "2024-01-08T12:00:00Z"
}
```
Errors: 401, 403, 404, and 429. See [Errors](https://docs.mobilejoe.dev/api.md#errors).
---
REST API
# Response schemas
Field definitions from the [OpenAPI contract](https://docs.mobilejoe.dev/api/openapi.yaml). See the [API introduction](https://docs.mobilejoe.dev/api.md) for authentication and operations.
## FeaturePlan
| Field | Required / type | Details |
| --- | --- | --- |
| `id` | Yes / integer | Unique identifier |
| `title` | Yes / string | Feature plan title |
| `body` | Yes / string | Detailed description of the feature plan |
| `score` | Yes / integer | Total score, including initial votes. The toggle response reflects the score after the vote change. |
| `status` | Yes / string | Current status of the feature plan Values: `open, under_review, planned, in_progress, completed, closed` |
| `voted` | Yes / boolean | Whether the current user has voted for this plan |
| `created_at` | Yes / string, date-time | Creation timestamp |
| `updated_at` | Yes / string, date-time | Last update timestamp |
## FeaturePlanStatus
Current status of the feature plan
string. Values: `open, under_review, planned, in_progress, completed, closed`
## Alert
| Field | Required / type | Details |
| --- | --- | --- |
| `id` | Yes / integer | Unique identifier |
| `title` | Yes / string | Alert title (localized for the requested languages) |
| `message` | Yes / string, nullable | Alert message body (localized for the requested languages) |
| `status` | Yes / string | Alert publication status Values: `draft, live, archived` |
| `kind` | Yes / string | Type/severity of the alert Values: `error, warning` |
| `occurred_at` | Yes / string, date-time, nullable | Timestamp when the alert event occurred |
| `created_at` | Yes / string, date-time | Creation timestamp |
| `updated_at` | Yes / string, date-time | Last update timestamp |
## AlertStatus
Alert publication status
string. Values: `draft, live, archived`
## AlertKind
Type/severity of the alert
string. Values: `error, warning`
## Faq
| Field | Required / type | Details |
| --- | --- | --- |
| `id` | Yes / integer | Unique identifier |
| `title` | Yes / string | FAQ question/title (localized for the requested languages) |
| `body` | Yes / string | FAQ answer/content (localized for the requested languages) |
| `position` | Yes / integer, nullable | Display order position |
| `created_at` | Yes / string, date-time | Creation timestamp |
| `updated_at` | Yes / string, date-time | Last update timestamp |
## Error
| Field | Required / type | Details |
| --- | --- | --- |
| `error` | Yes / string | Error message |