Skip to main content

Configuration

Navigate to /public/static/config/config.json and locate the AppStore section.
Do not change the redirect value — it tells the phone where to route requests.

Configuration Properties

name
string
required
Display name of the app
icon
string
required
Path to the app icon
url
string
required
Website URL to display (must support iFrame) or a self-hosted HTML file
custom_app_id
string
required
Unique identifier for your app. Set once and never change it.
darkmode
boolean
default:"false"
Changes header and home button theme
allowJobs
array
List of jobs that can access this app
disallowJobs
array
List of jobs that cannot access this app
custom_event
object
Event configuration with the following properties:

Examples

Website App

{
  "name": "CustomApp",
  "icon": "/public/img/Apps/custom.jpg",
  "default": true,
  "category": "apps",
  "custom_app_id": "SET_YOUR_OWN_ID_DONT_CHANGE_AFTERWARDS_NO_DOUBLE_ID",
  "redirect": "custom_app",
  "url": "https://www.test.com",
  "darkmode": false,
  "allowJobs": [],
  "disallowJobs": [],
  "custom_event": {
    "active": false,
    "closeWhenOpenApp": false
  }
}

Self-Developed App

{
  "name": "Template App",
  "icon": "/public/img/Apps/custom.jpg",
  "default": true,
  "category": "apps",
  "custom_app_id": "TEMPLATE_APP_1",
  "redirect": "custom_app",
  "url": "https://cfx-nui-roadphone-app-template/html/static/index.html",
  "darkmode": true,
  "allowJobs": [],
  "disallowJobs": [],
  "custom_event": {
    "active": false,
    "closeWhenOpenApp": false
  }
}

App Template

Get started quickly with our self-development app template on GitHub

NUI Messaging

-- Send a custom event to your app
exports['roadphone']:SendMessageNUI({
  customevent = "yourEvent",
  data = "yourData"
})

Exports

Input Focus Control

Control the NUI focus behavior for the phone:
exports['roadphone']:inputFocus(boolean) -- true or false
Use true to enable input focus (keyboard/mouse control) and false to disable it.

Overview

RoadPhone exposes a global window.roadphone API that allows developers to build custom applications with full access to phone data, settings, and events.
The API is available globally via window.roadphone and can be accessed from any iframe-based custom app.

Quick Start

// Check if API is available
if (window.roadphone) {
  const isDark = window.roadphone.isDarkMode()
  const playerName = window.roadphone.getPlayerName()

  console.log(`Hello ${playerName}, dark mode is ${isDark ? 'on' : 'off'}`)
}

App Setup

Run these calls once when your app boots to declare your identity, isolate your data, and verify the phone supports the features you rely on.

app.setName(name)

Registers the display name shown in permission and consent dialogs.
name
string
required
The human-readable name of your app
window.roadphone.app.setName('My App')

app.setNamespace(namespace)

Sets a unique namespace that isolates your app’s stored data from other apps. Set this once and never change it.
namespace
string
required
A unique identifier for your app’s storage
window.roadphone.app.setNamespace('my-app')

app.getNamespace()

Returns the currently configured namespace.
returns
string
The active storage namespace
const namespace = window.roadphone.app.getNamespace()
// "my-app"

minVersion(version)

Validates that the phone meets the minimum required API version. Throws if the installed phone is too old.
version
string
required
The minimum semantic version your app requires (e.g., "1.3.0")
roadphone.minVersion('1.3.0') // throws if the phone is older

requireFeature(feature)

Ensures a given capability is available before your app uses it. Throws if the feature is missing.
feature
string
required
The feature key to require (e.g., "storage")
roadphone.requireFeature('storage') // throws if unavailable

features

An object describing the API capabilities the current phone exposes. Use it to gracefully degrade when a feature is unavailable.
if (window.roadphone.features.storage) {
  // safe to use the storage API
}

Getter Functions

isDarkMode()

Returns whether dark mode is currently enabled.
returns
boolean
true if dark mode is enabled, false otherwise
const isDark = window.roadphone.isDarkMode()
// true or false

getPhoneNumber()

Returns the current phone number.
returns
string
The player’s phone number (e.g., "1234567")
const phoneNumber = window.roadphone.getPhoneNumber()
// "1234567"

getPlayerName()

Returns the player’s character name.
returns
string
The player’s name (e.g., "John Doe")
const name = window.roadphone.getPlayerName()
// "John Doe"

getJob()

Returns the player’s current job.
returns
string
The job identifier (e.g., "police", "ambulance", "unemployed")
const job = window.roadphone.getJob()
// "police"

getIdentifier()

Returns the player’s unique identifier.
returns
string
The player identifier (format depends on framework)
const identifier = window.roadphone.getIdentifier()
// ESX: "license:xxxxx"
// QBCore: "citizenid"

getBrightness()

Returns the current screen brightness level.
returns
number
Brightness value between 10 and 100
const brightness = window.roadphone.getBrightness()
// 75

isFlightMode()

Returns whether flight mode is enabled.
returns
boolean
true if flight mode is on, false otherwise
const flightMode = window.roadphone.isFlightMode()
// false

getConfig()

Returns the full phone configuration object.
returns
object
The complete config.json configuration
const config = window.roadphone.getConfig()
// { lockscreen: true, ... }

getLanguage()

Returns the player’s current locale.
returns
string
The active language code (e.g., "de_DE", "en_US")
const lang = window.roadphone.getLanguage()
// "de_DE"

Utility Functions

copyToClipboard(text)

Copies text to the clipboard.
text
string
required
The text to copy
window.roadphone.copyToClipboard('Hello World')

post(event, data)

Sends data to the Lua backend. Use this to communicate with your server-side scripts.
event
string
required
The event name to trigger
data
object
Optional data to send with the event
// Send custom event to Lua
window.roadphone.post('myCustomEvent', {
  action: 'doSomething',
  value: 123
})
Make sure to register the corresponding NUI callback in your Lua code to handle the event.

showNotification(options)

Displays a phone notification.
options
object
required
Notification configuration object
window.roadphone.showNotification({
  appTitle: 'My App',
  title: 'New Message',
  message: 'You have received a new message!',
  icon: '/public/img/Apps/light_mode/custom.webp'
})

takePhoto(options)

Opens the phone’s camera and returns the captured media. Resolves once the player confirms or cancels.
options
object
Capture configuration
returns
Promise<object>
The captured photo object (or null if cancelled)
const photo = await window.roadphone.takePhoto({ allowVideo: false })

showBottomSheet(options)

Displays a native bottom sheet with grouped, selectable actions and resolves with the player’s choice.
options
object
required
Bottom sheet configuration
returns
Promise<object>
The selected option (or null if dismissed)
const choice = await window.roadphone.showBottomSheet({
  groups: [
    {
      items: [
        { id: 'edit', label: 'Edit' },
        { id: 'delete', label: 'Delete' }
      ]
    }
  ]
})

pickEmoji()

Opens the native emoji picker and resolves with the chosen emoji.
returns
Promise<string>
The selected emoji (or null if dismissed)
const emoji = await window.roadphone.pickEmoji()
// "😀"

Storage

RoadPhone exposes two storage backends scoped to your app’s namespace.
Synchronous storage is backed by localStorage and is fast but local to the device. Metadata storage is asynchronous, server-backed, and persists across character and device changes.

Synchronous Storage

Fast, namespace-scoped key/value storage for non-critical data.
window.roadphone.storage.set('lastTab', 'home')
const tab = window.roadphone.storage.get('lastTab') // "home"
window.roadphone.storage.delete('lastTab')
const keys = window.roadphone.storage.keys()        // ["lastTab", ...]
window.roadphone.storage.clear()                    // wipes the namespace
FunctionReturnsDescription
storage.set(key, value)voidStore a value
storage.get(key)anyRead a value
storage.delete(key)voidRemove a value
storage.keys()string[]List all stored keys
storage.clear()voidRemove everything in the namespace

Metadata Storage

Asynchronous, persistent storage that survives character and device changes. Requires the storage.metadata permission.
await window.roadphone.storage.metadata.set('city', 'Berlin')
const city = await window.roadphone.storage.metadata.get('city') // "Berlin"
await window.roadphone.storage.metadata.delete('city')
FunctionReturnsDescription
storage.metadata.set(key, value)Promise<void>Persist a value
storage.metadata.get(key)Promise<any>Read a persisted value
storage.metadata.delete(key)Promise<void>Remove a persisted value

Permissions

Sensitive phone data (contacts, messages, bank, alarms) is gated behind a permission system. Request the scopes you need before calling the corresponding APIs — the player is shown a consent dialog.

permissions.request(scope)

Requests access to a scope. Resolves to true if granted; denied requests reject.
scope
string
required
The permission scope to request
returns
Promise<boolean>
true if the player granted the permission
const granted = await window.roadphone.permissions.request('contacts.read')
if (granted) {
  const contacts = await window.roadphone.contacts.list()
}

permissions.has(scope)

Checks whether a scope has already been granted.
returns
boolean
true if the permission is currently granted
if (window.roadphone.permissions.has('bank.read')) {
  // already authorized
}

permissions.revoke(scope)

Revokes a previously granted scope.
window.roadphone.permissions.revoke('contacts.read')

permissions.list()

Returns all permission decisions made for your app.
returns
object
A map of scope strings to their granted/denied state
const decisions = window.roadphone.permissions.list()

Available Scopes

ScopeGrants access to
contacts.readReading the player’s contacts
messages.readReading message threads
messages.sendSending messages
bank.readReading balance, IBAN, and accounts
alarms.readReading alarms
alarms.writeCreating and deleting alarms
storage.metadataPersistent metadata storage

Phone Data APIs

These APIs read and write the player’s phone data and require the matching permission scope. Each method is asynchronous.

Contacts

Requires contacts.read.
const contacts = await window.roadphone.contacts.list()       // [{ id, firstname, ... }]
const contact = await window.roadphone.contacts.find('1234567') // contact object
const total = await window.roadphone.contacts.count()          // number

Messages

list requires messages.read; send requires messages.send.
await window.roadphone.messages.send('1234567', 'Hi!')
const thread = await window.roadphone.messages.list('1234567') // message array

Bank

Requires bank.read.
const balance = await window.roadphone.bank.getBalance()  // number
const iban = await window.roadphone.bank.getIban()        // "..."
const accounts = await window.roadphone.bank.getAccounts() // accounts array

Alarms

list requires alarms.read; create and delete require alarms.write.
const alarms = await window.roadphone.alarms.list()
await window.roadphone.alarms.create({ time: '07:30', label: 'Wake up' })
await window.roadphone.alarms.delete(id)

Event System

The API includes a powerful event system that allows your custom app to react to phone state changes in real-time.

on(event, callback)

Subscribe to an event.
event
string
required
The event name to listen for
callback
function
required
Function to call when the event fires
window.roadphone.on('darkModeChanged', (isDark) => {
  document.body.classList.toggle('dark-mode', isDark)
})

off(event, callback)

Unsubscribe from an event.
event
string
required
The event name to unsubscribe from
callback
function
required
The same function reference used when subscribing
const handler = (isDark) => console.log(isDark)

// Subscribe
window.roadphone.on('darkModeChanged', handler)

// Unsubscribe
window.roadphone.off('darkModeChanged', handler)

Available Events

Fired when the phone is opened.
window.roadphone.on('phoneOpened', () => {
console.log('Phone opened!')
// Initialize your app, fetch data, etc.
})
Fired when the phone is closed.
window.roadphone.on('phoneClosed', () => {
console.log('Phone closed!')
// Save state, cleanup, etc.
})
Fired when dark mode is toggled.Payload: boolean - true if dark mode is now enabled
window.roadphone.on('darkModeChanged', (isDark) => {
if (isDark) {
document.body.style.background = '#1c1c1e'
document.body.style.color = '#ffffff'
} else {
document.body.style.background = '#ffffff'
document.body.style.color = '#000000'
}
})
Fired when screen brightness changes.Payload: number - Brightness value (10-100)
window.roadphone.on('brightnessChanged', (brightness) => {
console.log(`Brightness: ${brightness}%`)
})
Fired when flight mode is toggled.Payload: boolean - true if flight mode is now enabled
window.roadphone.on('flightModeChanged', (isEnabled) => {
if (isEnabled) {
showOfflineMessage()
} else {
fetchLatestData()
}
})
Fired when the player changes the phone language.Payload: string - The new locale (e.g., "en_US")
window.roadphone.on('languageChanged', (locale) => {
applyTranslations(locale)
})
Fired when your custom app is opened.
window.roadphone.on('appOpened', () => {
refreshData()
})
Fired when your custom app is closed.
window.roadphone.on('appClosed', () => {
saveState()
})
Fired when the player receives a call.Payload: object - Call details (caller number, etc.)
window.roadphone.on('incomingCall', (call) => {
console.log('Incoming call from', call.number)
})
Fired when an active call ends.
window.roadphone.on('callEnded', () => {
console.log('Call ended')
})
Fired when the phone receives a notification.Payload: object - The notification data
window.roadphone.on('notificationReceived', (notification) => {
console.log('New notification:', notification.title)
})

Setup

Custom Apps are loaded as external URLs inside an iframe within the phone. To set up your custom app:
1

Create your app

Build your custom app as a standalone HTML page and host it on a web server or locally.
2

Configure the URL

Open public/static/config/config.json and find the AppStore section. Set your CustomApp URL:
{
    "AppStore": {
    "CustomApp": {
    "url": "https://your-server.com/my-custom-app.html",
    "darkmode": true
}
}
}
3

Access the API

Your app runs inside an iframe and can access window.parent.roadphone to use the API.
Since your custom app runs inside an iframe, you must access the API via window.parent.roadphone instead of window.roadphone.

Complete Example

Here’s a complete example of a custom app that displays player information. This app is designed to fit perfectly within the phone’s iframe and uses vh units for proper scaling.
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  <title>My Custom App</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    html, body {
      width: 100%;
      height: 100%;
      overflow: hidden;
    }

    body {
      font-family: -apple-system, BlinkMacSystemFont, 'SF Pro', sans-serif;
      transition: background-color 0.3s, color 0.3s;
    }

    body.dark {
      background-color: #000000;
      color: #ffffff;
    }

    body.light {
      background-color: #f2f2f7;
      color: #1d1d1f;
    }

    .app-container {
      width: 100%;
      height: 100%;
      padding: 1.5vh;
      padding-top: 5vh; /* Space for phone status bar */
      overflow-y: auto;
    }

    .header {
      text-align: center;
      padding: 2vh 0 3vh;
    }

    .header-title {
      font-size: 2vh;
      font-weight: 700;
    }

    .card {
      border-radius: 1.2vh;
      padding: 1.5vh;
      margin-bottom: 1vh;
    }

    body.light .card {
      background: #ffffff;
    }

    body.dark .card {
      background: rgba(255, 255, 255, 0.08);
    }

    .label {
      font-size: 0.9vh;
      color: #86868b;
      text-transform: uppercase;
      letter-spacing: 0.05vh;
      margin-bottom: 0.5vh;
    }

    .value {
      font-size: 1.4vh;
      font-weight: 600;
    }

    .button {
      width: 100%;
      padding: 1.2vh;
      border: none;
      border-radius: 1vh;
      font-size: 1.1vh;
      font-weight: 600;
      font-family: inherit;
      cursor: pointer;
      margin-top: 2vh;
      background: #007aff;
      color: #ffffff;
    }

    body.dark .button {
      background: #0a84ff;
    }
  </style>
</head>
<body class="light">
  <div class="app-container">
    <div class="header">
      <div class="header-title">My Custom App</div>
    </div>

    <div class="card">
      <div class="label">Player Name</div>
      <div class="value" id="playerName">Loading...</div>
    </div>

    <div class="card">
      <div class="label">Phone Number</div>
      <div class="value" id="phoneNumber">Loading...</div>
    </div>

    <div class="card">
      <div class="label">Job</div>
      <div class="value" id="job">Loading...</div>
    </div>

    <button class="button" onclick="showNotification()">
      Send Test Notification
    </button>
  </div>

  <script>
    // Access the API from parent window (since we're in an iframe)
    const roadphone = window.parent.roadphone

    function init() {
      // Check if API is available
      if (!roadphone) {
        console.error('RoadPhone API not available')
        document.getElementById('playerName').textContent = 'API not available'
        return
      }

      // Load initial data
      document.getElementById('playerName').textContent = roadphone.getPlayerName() || 'Unknown'
      document.getElementById('phoneNumber').textContent = roadphone.getPhoneNumber() || 'Unknown'
      document.getElementById('job').textContent = roadphone.getJob() || 'Unemployed'

      // Apply initial theme
      applyTheme(roadphone.isDarkMode())

      // Listen for dark mode changes
      roadphone.on('darkModeChanged', applyTheme)
    }

    function applyTheme(isDark) {
      document.body.classList.remove('dark', 'light')
      document.body.classList.add(isDark ? 'dark' : 'light')
    }

    function showNotification() {
      if (!roadphone) return

      roadphone.showNotification({
        appTitle: 'My Custom App',
        title: 'Hello!',
        message: 'This is a test notification from your custom app.'
      })
    }

    // Initialize when DOM is ready
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', init)
    } else {
      init()
    }
  </script>
</body>
</html>
Use vh units for sizing (like the rest of RoadPhone) to ensure your app scales correctly on different screen sizes.

Lua Backend Integration

To handle custom events from your app, register NUI callbacks in your Lua code:
-- server/custom_app.lua
RegisterNUICallback('myCustomEvent', function(data, cb)
    local src = source

    print('Received custom event:', json.encode(data))

    -- Do something with the data
    if data.action == 'doSomething' then
        -- Your logic here
    end

    cb('ok')
end)

API Reference

Core
FunctionReturnsDescription
isDarkMode()booleanCheck if dark mode is enabled
getPhoneNumber()stringGet the phone number
getPlayerName()stringGet the player’s name
getJob()stringGet the player’s job
getIdentifier()stringGet the player’s identifier
getLanguage()stringGet the active locale
getBrightness()numberGet brightness (10-100)
isFlightMode()booleanCheck if flight mode is on
getConfig()objectGet the full config
copyToClipboard(text)voidCopy text to clipboard
post(event, data)PromiseSend data to Lua backend
showNotification(opts)voidShow a notification
takePhoto(opts)Promise<object>Capture a photo/video
showBottomSheet(opts)Promise<object>Show a native action sheet
pickEmoji()Promise<string>Open the emoji picker
on(event, callback)voidSubscribe to an event
off(event, callback)voidUnsubscribe from an event
App & Compatibility
FunctionReturnsDescription
app.setName(name)voidSet the app display name
app.setNamespace(ns)voidSet the storage namespace
app.getNamespace()stringGet the storage namespace
minVersion(version)voidRequire a minimum phone version
requireFeature(feature)voidRequire a capability
featuresobjectAvailable API capabilities
Storage
FunctionReturnsDescription
storage.set/get/delete(key, [value])anySynchronous key/value storage
storage.keys()string[]List stored keys
storage.clear()voidClear the namespace
storage.metadata.set/get/delete(key, [value])PromisePersistent metadata storage
Permissions & Data (require the matching scope)
FunctionReturnsDescription
permissions.request(scope)Promise<boolean>Request a scope
permissions.has(scope)booleanCheck a granted scope
permissions.revoke(scope)voidRevoke a scope
permissions.list()objectList all decisions
contacts.list/find/count()PromiseRead contacts (contacts.read)
messages.send/list()PromiseSend/read messages (messages.*)
bank.getBalance/getIban/getAccounts()PromiseRead bank data (bank.read)
alarms.list/create/delete()PromiseManage alarms (alarms.*)
EventPayloadDescription
phoneOpenednonePhone was opened
phoneClosednonePhone was closed
darkModeChangedbooleanDark mode toggled
brightnessChangednumberBrightness changed
flightModeChangedbooleanFlight mode toggled
languageChangedstringLanguage changed
appOpenednoneCustom app opened
appClosednoneCustom app closed
incomingCallobjectIncoming call received
callEndednoneActive call ended
notificationReceivedobjectNotification received

Version

Current API Version: 1.3.0Access via: window.roadphone.version