Use Coupon WELCOME to save 20%

$ USD
  • $ USD
  • € EUR
  • £ GBP
  • $ AUD
  • R$ BRL
  • CHF CHF
  • ¥ JPY
Build a Custom FiveM Phone App

Build a Small FiveM Phone App with NUI

Build a small phone-style NUI app first to learn the complete open, message, callback and close lifecycle. This example shows a session note that stays while the resource runs. It has no calls, messaging service, database or framework dependency; adding those requires separate server-side authorization and persistence.

Create three files

Create resources/[local]/tutorial_phone/ with fxmanifest.lua, client.lua and index.html. The example targets the GTA V Legacy gta5 resource runtime. Check Cfx.re’s Enhanced migration guidance before adapting it to an Enhanced deployment.

fxmanifest.lua:

fx_version 'cerulean'
game 'gta5'
ui_page 'index.html'
files { 'index.html' }
client_script 'client.lua'

client.lua:

local visible = false
local ready = false

local function closePhone()
    visible = false
    SetNuiFocus(false, false)
    SendNUIMessage({ action = 'close' })
end

RegisterCommand('tutorial_phone', function()
    if not ready then
        print('Phone UI is loading; try again shortly.')
        return
    end
    if visible then closePhone(); return end
    visible = true
    SetNuiFocus(true, true)
    SendNUIMessage({ action = 'open' })
end, false)

RegisterNUICallback('ready', function(_, cb)
    ready = true
    cb({ ok = true })
end)

RegisterNUICallback('close', function(_, cb)
    closePhone()
    cb({ ok = true })
end)

AddEventHandler('onClientResourceStop', function(name)
    if name == GetCurrentResourceName() and visible then
        SetNuiFocus(false, false)
    end
end)

index.html:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Session note</title>
<style>
body { margin: 0; background: transparent; font: 18px system-ui; }
#phone { position: fixed; right: 4vw; bottom: 4vh; width: min(320px, 85vw);
  padding: 24px; box-sizing: border-box; color: #fff; background: #18222d;
  border-radius: 20px; }
[hidden] { display: none !important; }
textarea { box-sizing: border-box; width: 100%; min-height: 140px; font: inherit;
  margin: 12px 0; padding: 8px; }
button { font: inherit; padding: 10px 16px; cursor: pointer; }
:focus-visible { outline: 3px solid #67d7ff; outline-offset: 3px; }
</style>
</head>
<body>
<section id="phone" role="dialog" aria-label="Session note" hidden>
  <h1>Session note</h1>
  <label for="note">What will your character do next?</label>
  <textarea id="note" maxlength="500"></textarea>
  <p>This note clears when the resource restarts.</p>
  <button id="close" type="button">Close</button>
  <p id="error" role="status"></p>
</section>
<script>
const phone = document.getElementById('phone');
const note = document.getElementById('note');
const error = document.getElementById('error');
async function call(name) {
  const response = await fetch(`https://${GetParentResourceName()}/${name}`, {
    method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}'
  });
  if (!response.ok) throw new Error('NUI callback failed');
  return response.json();
}
window.addEventListener('message', ({ data }) => {
  if (data?.action === 'open') {
    phone.hidden = false; error.textContent = ''; note.focus();
  } else if (data?.action === 'close') {
    phone.hidden = true;
  }
});
async function close() {
  try { await call('close'); }
  catch { error.textContent = 'Could not close. Check the F8 console.'; }
}
document.getElementById('close').addEventListener('click', close);
window.addEventListener('keydown', event => {
  if (event.key === 'Escape' && !phone.hidden) { event.preventDefault(); close(); }
});
call('ready').catch(() => console.error('Phone readiness callback failed.'));
</script>
</body>
</html>

Start and verify

Add ensure tutorial_phone to the active server configuration. On your test server, run refresh and ensure tutorial_phone, connect, then run tutorial_phone in F8. The app should open and focus its note field.

Type a note, close with the button and reopen. The note should remain for this resource session. Test Escape, repeated open/close and stopping the resource while open; game input should be released. Reconnecting or restarting the resource creates a new UI session and clears the note.

Inspect the NUI boundary

The browser calls the resource callback by its actual parent name. Lua always replies through cb, and the browser handles a failed request. Follow Cfx.re’s NUI debugging instructions to inspect console and callback requests on your client.

Add real phone features deliberately

An existing phone resource may expose its own app SDK; use that supported integration instead of running a second full phone. Persistent contacts need character ownership checks on the server, validated lengths, parameterized queries and a migration for the installed schema.

Real calls need the chosen voice resource’s supported integration. Never accept an arbitrary character identifier from the UI as proof of access, or claim a contacts form is a complete secure phone system.

Reference documentation

Cfx.re — nui callbacks · Cfx.re — resource manifest