Agency-Phone Custom Apps
Now with AgencyOS27: a free update for everyone who already owns the phone.

🧩 Build a custom app for Agency-Phone
There are two ways to put an app on the phone. Pick the one that fits you:
| Method | Best for | What you touch |
|---|---|---|
| A. SDK (external resource) | Vendors and scripts adding an app with their own icon | Only your own resource. Zero edits to agency-phone. |
| B. Drop-in (inside agency-phone) | Server owners adding a native screen inside the phone UI | config.lua + apps/ (+ html/js for a screen) |
If you ship an app to other servers, use Method A. It needs no source from us and keeps working across updates.
A SDK: register an app from your own resource
agency-phone exposes exports that let any resource put an app with its own icon on the phone home screen at runtime. Your app's interface lives in your own resource; the phone shows the icon and hands control to you when the player taps it. No edits to agency-phone, no unlocked build.
Minimal example
-- client side of YOUR resource
CreateThread(function()
Wait(1500) -- let agency-phone boot
exports['agency-phone']:RegisterApp({
id = 'myshop',
label = 'My Shop',
icon = 'fa-solid fa-cart-shopping', -- FontAwesome 6 class
-- image = 'https://cfx-nui-myresource/icon.png', -- or your own image instead
color = '#ff9500',
colorDark = '#e08600',
})
end)
-- fired when the player taps your icon on the phone
AddEventHandler('agency-phone:client:openApp', function(appId)
if appId ~= 'myshop' then return end
exports['agency-phone']:ClosePhone()
-- open your own NUI here
end)
RegisterApp config
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Unique id, lowercase, no spaces. |
label | string | yes | Name shown under the icon. |
icon | string | no | FontAwesome 6 class. Ignored if image is set. |
image | string | no | Custom icon image, e.g. https://cfx-nui-<resource>/icon.png. |
color / colorDark | string | no | Gradient behind the icon (hex). |
slot | number | no | Home screen position. Placed automatically if omitted. |
storeDesc | string | no | Short description on your app's Store page. Falls back to the label. |
SDK exports (client)
| Export | Description |
|---|---|
RegisterApp(config) | Add or update your app. Returns true, or false, err. |
UnregisterApp(id) | Remove your app. Also happens automatically when your resource stops. |
GetRegisteredApps() | Table of all registered external apps. |
ClosePhone() | Close the phone, e.g. before opening your own NUI. |
The tap event
When the player taps your icon, agency-phone fires both of these on the client. Use whichever you prefer:
| Event | Args |
|---|---|
agency-phone:client:openApp | appId (filter by your id) |
agency-phone:client:openApp:<yourId> | none (already scoped to your app) |
Add dependency 'agency-phone' to your fxmanifest.lua so the start order is correct.
B Drop-in, a native screen inside the phone
For a screen rendered by the phone itself (not your resource). This edits agency-phone's own files.
| Step | File | Editable in escrow build? |
|---|---|---|
1. App entry in AgencyConfig.Apps | config.lua | ✓ yes |
2. RegisterNUICallback logic | apps/<id>_client.lua (+ _server.lua) | ✓ yes |
3. Screen <div id="app-<id>"> | html/index.html | ✗ encrypted |
4. case '<id>': in openApp() + loader | html/js/phone.js | ✗ encrypted |
Steps 3 and 4 live in escrow-encrypted files, so a native screen needs a source / unlocked build. If you only have the escrow version, use Method A instead (your screen lives in your own resource). Reference files inside the resource: apps/testapp_client.lua and apps/dispatch_client.lua.
📋 Other phone exports
Client
| Export | Returns | Description |
|---|---|---|
IsPhoneOpen() | boolean | Phone currently open. |
IsPhonePoweredOn() | boolean | Device powered on. |
HasPhone() | boolean | Player owns a phone item. |
HasCarrierContract() | boolean | Active eSIM / contract. |
GetPhoneData() | table | Snapshot of phone state. |
PhoneNotification(title, text, icon, color, timeout, acceptIcon, denyIcon) | void | In-phone notification. |
StartCall(contact) | void | Call { name, number }. |
StartAgencyPayCheckout(opts) | void | AgencyPay checkout (see below). |
Server
| Export | Description |
|---|---|
SendMail(identifier, mailData) | Deliver mail to a player (online or offline). |
AddChirp(data) | Post a message to the Pulse social feed. |
VerifyAgencyPayCharge(source, requestId, minAmount) | Confirm a paid AgencyPay checkout on the server before you hand out goods (see below). |
⇄ NUI ↔ Lua bridge (Method B)
| Direction | JS side | Lua side |
|---|---|---|
| JS → Lua | fetchNUI('callbackName', data) | RegisterNUICallback('callbackName', fn) |
| Lua → JS | listen for action in window.onmessage | SendNUIMessage({ action = '...' }) |
💳 AgencyPay checkout
Charge the player's wallet from any resource.
exports['agency-phone']:StartAgencyPayCheckout({
merchant = 'My Shop', title = 'Agency Pay', subtitle = 'Checkout',
label = 'Premium Coffee', amount = 24.50,
icon = 'fa-solid fa-mug-hot', accent = '#ff9f0a',
clientEvent = 'myshop:client:agencyPayResult',
serverEvent = 'myshop:server:agencyPayResult',
metadata = { orderId = 'order_2048' },
})
RegisterNetEvent('myshop:client:agencyPayResult', function(result)
if result.status == 'paid' then
-- deliver the goods
end
end)
The result payload includes status (paid / cancelled / failed), amount, cardId, balance and your metadata.
A paid result that reaches your resource from the client proves nothing on its own. Pass your own id to StartAgencyPayCheckout and call exports['agency-phone']:VerifyAgencyPayCharge(source, id, amount) on the server before you hand anything out. It returns true and the amount only if this player paid that checkout in the last 60 seconds, and it uses the charge up, so the same payment cannot be claimed twice.
🐛 Troubleshooting
| Symptom | Fix |
|---|---|
| Icon doesn't appear (SDK) | Update agency-phone to the latest Keymaster build, Wait(1500) before RegisterApp, and add dependency 'agency-phone' to your manifest. |
| Tapping does nothing (SDK) | Your agency-phone:client:openApp handler must match your id exactly (case-sensitive). |
| Custom image not showing | Use a full NUI URL https://cfx-nui-<your-resource>/path/icon.png and list the file in files { } in your manifest. |
| Icon doesn't appear (drop-in) | enabled = true and installed = true in the config entry. |
Questions, or need a hand? Join the Agency Scripts Discord.