API Reference
The olink.phone API for oxide-phone — o-link server and client functions, phone-only server exports, direct exports, server and client events, reserved app IDs and integration examples.
This page is for developers who want their resource to work with oxide-phone: sending texts, mail and notifications to players, putting an app or widget on the phone, or plugging into the evidence system. Server owners don't need anything on this page.
Use o-link. For messages, mail, notifications and apps, call olink.phone.* from the o-link bridge. Your resource then works with this phone, with the other phones o-link supports, and with no phone at all. The server_exports and client_exports in oxide-phone's fxmanifest.lua exist so o-link's adapter (o-link/modules/phone/oxide-phone/) can reach the phone. Only the phone-only server exports have no o-link equivalent.
olink = exports['o-link']:olink()Integration Rules
- Never depend on oxide-phone. Don't list it in your
dependencies. Go througholink.phoneso your resource still works when a different phone, or none, is installed. - Check for a phone with
IsAvailable(). It istrueonly while a real phone runs. Don't gate onolink.supports('phone.SendEmail'): o-link has fallback stubs, so that istrueeven with no phone installed. - Functions from o-link are callable tables. Nil-check the member and call it through
pcall. Don't comparetype(fn) == 'function'. - Check the return value. Every function returns
false(ornil) when the phone can't do what you asked, whether it's missing, stopped, or refused the call. - Pass
resource = GetCurrentResourceName()when registering apps and widgets. Your call reaches the phone through o-link, so the phone can't see which resource called. - Register apps on your start and on
olink:client:phone:ready. The phone keeps its app registry in memory on each client and forgets it when it restarts. See Building Apps.
o-link needs to be 1.11.0 or newer for olink.phone to reach oxide-phone. With an older o-link, olink.phone is o-link's fallback even while this phone runs: GetResourceName() answers '_default', SendEmail and GetPlayerPhone answer false, and the other functions on this page (IsAvailable, SendMessage, RegisterApp, AppService and the rest) are missing.
o-link Server Functions
IsAvailable
Whether a real phone is running.
local available = olink.phone.IsAvailable()Returns: boolean - true while oxide-phone is started.
GetResourceName / GetPhoneName
local name = olink.phone.GetResourceName() -- 'oxide-phone'Returns: string - 'oxide-phone' while this phone owns the namespace ('_default' with no phone).
GetPlayerPhone
The number of the phone a player is using: a phone of their own with a SIM (the open one first), else any other phone with a SIM they carry. Phones held by police never count.
local number = olink.phone.GetPlayerPhone(source)Parameters:
| Name | Type | Description |
|---|---|---|
| src | number | Player server ID |
Returns: string|false - The number, for example '213-555-0142', or false when the player carries no phone with a SIM.
GetSourceByNumber
The online player holding the phone a number is in.
local target = olink.phone.GetSourceByNumber('213-555-0142')Parameters:
| Name | Type | Description |
|---|---|---|
| number | string | A phone number, laid out as the phone stores it |
Returns: number|nil - The player's server ID while that phone can be reached: its SIM is in it, it is switched on, charged and not held by police, and it is carried by an online player who is alive and not in a no-signal area. nil otherwise.
GetNumbersByIdentifier
The numbers registered to a character, online or offline, oldest first. Burner SIMs and deactivated SIMs are never listed.
local numbers = olink.phone.GetNumbersByIdentifier(charId)Parameters:
| Name | Type | Description |
|---|---|---|
| identifier | string | The character identifier (as olink.character.GetIdentifier returns it) |
Returns: string[] - The numbers, or {}.
SendEmail
Mail to an online player.
local ok = olink.phone.SendEmail(source, 'Los Santos Customs', 'Your car is ready', 'Pick it up at the Burton shop.')Parameters:
| Name | Type | Description |
|---|---|---|
| src | number | Player server ID |
| sender | string | Display name of the sender, up to 100 characters |
| subject | string | Up to 100 characters |
| message | string | Up to 2000 characters. Longer text is cut |
Returns: boolean - true when the mail was delivered or is waiting for the player's phone. false when the player is offline or the sender or message is empty.
Notes: When the player's phone can't take mail right now (off, flat, no signal, no phone), the mail waits and arrives on the next phone that holds their number or any SIM registered to them, for up to Config.Mail.pendingDays.
SendEmailOffline
Mail to a character who may be offline.
local ok = olink.phone.SendEmailOffline(charId, 'City Hall', 'License approved', 'Your business license was approved.')Parameters:
| Name | Type | Description |
|---|---|---|
| identifier | string | The character identifier, up to 64 characters |
| sender | string | Up to 100 characters |
| subject | string | Up to 100 characters |
| message | string | Up to 2000 characters |
Returns: boolean - true when delivered or waiting. false for an empty identifier, sender or message.
SendMessage
A text to a player from a named service ("Pizza This"). It lands in a read-only conversation for that service on the player's phone.
local ok = olink.phone.SendMessage(source, 'Pizza This - Vespucci', 'Your next delivery is ready.')Parameters:
| Name | Type | Description |
|---|---|---|
| src | number | Player server ID |
| sender | string | The service name, up to 60 characters |
| message | string | Up to Config.Messages.maxLength characters (500 by default). A longer text is refused, not cut |
Returns: boolean - true when the text was sent. It arrives at once, or waits while the phone is off or out of signal. false when the player carries no phone with a SIM, or the message is empty or too long.
SendNotification
A notification on the player's phone. It is stored on the phone, survives restarts, and shows in the notification list.
local ok = olink.phone.SendNotification(source, {
title = 'Roadside Assistance',
message = 'New call: flat tyre on Route 68',
icon = 'fa-solid fa-truck-pickup',
})Parameters:
| Name | Type | Description |
|---|---|---|
| src | number | Player server ID |
| data.message | string | Required. Up to 280 characters |
| data.title | string | Optional. Up to 80 characters |
| data.icon | string | Optional. A Font Awesome class |
| data.app | string | Optional. An app ID; tapping the notification opens that app. Without one it is a system notification |
Returns: boolean - true when stored. false when the player carries no phone (a phone without a SIM counts only while it is open).
Open / Close
Open the phone the player carries, optionally straight into an app (after they unlock it), or put it away.
olink.phone.Open(source, 'my_app')
olink.phone.Close(source)Parameters:
| Name | Type | Description |
|---|---|---|
| src | number | Player server ID |
| appId | string|nil | App to open. Open only |
Returns: boolean - Open: false when the player carries no phone or the phone refused (flat, broken, can't open right now). Close: false when no phone was open.
Send
Relay a message into your app's page on a player's phone.
olink.phone.Send(source, 'my_app', { action = 'balance', data = { amount = 1200 } })Parameters:
| Name | Type | Description |
|---|---|---|
| src | number | Player server ID |
| appId | string | Your app ID |
| message | table | { action = 'name', data = ... } |
Returns: boolean - true when relayed to the player. The player's phone drops it unless that app is on screen.
SetWidgetData
Push data to one of your widgets on a player's phone. See Building Apps → Widgets for the data shapes.
olink.phone.SetWidgetData(source, 'my_widget', { value = '$1,200', label = 'Balance' })Returns: boolean - true when relayed.
Notify / DismissNotification / ClearNotifications
Notifications for your app. See Building Apps → Notifications for every field.
local id = olink.phone.Notify(source, { app = 'my_app', title = 'Loan', message = 'Your loan was approved' })
olink.phone.DismissNotification(source, id)
olink.phone.ClearNotifications(source, 'my_app') -- nil clears every app'sFor an app with accounts, add account = <account id>: only the phone the player carries that is logged in to that account shows it (from the pocket while another phone is out), and Notify returns false when they carry no such phone.
Returns: Notify - string|false, the notification ID in the form app:id. The player's phone can still drop it (app not registered on that client, or muted by the player). DismissNotification, ClearNotifications - boolean, true when relayed.
o-link Client Functions
| Function | Returns | Description |
|---|---|---|
IsAvailable() | boolean | true while oxide-phone is started |
GetResourceName() / GetPhoneName() | string | 'oxide-phone' |
SendEmail(sender, subject, message) | boolean | Mail to the local player, relayed through the server. The server checks types, caps the lengths (100 / 100 / 2000) and allows one mail per player every 5 seconds |
IsOpen() | boolean | The phone is on screen |
RegisterApp(def) / UnregisterApp(id) | boolean | Put your app on the phone. See Building Apps → Register an app |
Open(appId?) | boolean | Open the phone the player carries, straight into appId if given. With the phone already open, switch to appId |
Close() | boolean | Put the phone away |
CloseApp(appId?) | boolean | Back to the home screen if appId (or any app) is on screen |
GetCurrentApp() | string|nil | The app on screen |
Send(appId, message) | boolean | Post { action, data } into your page. false unless that app is on screen |
SetBadge(appId, count) | boolean | The number on your app's icon |
RegisterWidget(def) / UnregisterWidget(id) | boolean | Put a widget on the home screen. See Building Apps → Widgets |
SetWidgetData(id, data) | boolean | Push widget data. Up to 8 KB once encoded |
Notify(def) | string|false | A notification for your app. Returns the ID (app:id) |
DismissNotification(id) / ClearNotifications(appId?) | boolean | Remove one, or all of an app's |
-- client
local register = olink.phone and olink.phone.RegisterApp
if register then
local ok, registered = pcall(register, {
id = 'my_app', label = 'My App', icon = 'fa-solid fa-star',
resource = GetCurrentResourceName(), url = 'web/dist/index.html',
})
endPhone-Only Server Exports
These have no o-link equivalent. Call them only when oxide-phone is running.
GetSetting
The current value of one setting.
local battery = exports['oxide-phone']:GetSetting('Battery')Parameters:
| Name | Type | Description |
|---|---|---|
| key | string | A top-level setting name without Config., for example 'Battery' or 'Evidence' |
Returns: any - A copy of the value.
SetSetting
Save a setting to the database and apply it live, like pressing Save Changes in /phone settings.
local ok = exports['oxide-phone']:SetSetting('Battery', { enabled = true })Parameters:
| Name | Type | Description |
|---|---|---|
| key | string | A top-level setting name that is in the settings menu |
| value | any | The new value. For a group of settings (such as 'Battery'), pass only the options you change; the rest stay. A list is replaced whole |
Returns: boolean - false when the value breaks the settings menu's rules (the server console logs SetSetting refused with the reason), while settings are still loading at server start, or when the database write fails.
Notes: The value is checked against the same rules as the settings menu, so it must stay within the limits listed in Configuration.
AppService
The server side of the phone's services for hosted apps (accounts, media your app got, location, the phone's id and number, evidence). Call it through o-link so your resource also runs with other phones; full guide: Building Apps → Phone services.
local ok, account = olink.phone.AppService('snapgram', 'account.get', { src = source })Returns: boolean, any - true and the result, or false and a reason (not_owner,
not_on_screen, locked, rate_limited, ...). Only the resource whose fxmanifest.lua declares
the app id (phone_app 'snapgram' { label = 'Snapgram' }) may use it.
SetMediaProvider
Hand out upload addresses for photos, videos and voice clips from your own resource, for Storage: Custom. Full guide: Building Apps → Custom photo storage.
exports['oxide-phone']:SetMediaProvider(function(request)
-- request = { deviceId, kind = 'photo' | 'video' | 'audio', contentType, size }
return { method = 'PUT', url = uploadUrl, publicUrl = publicUrl }
end)Returns: boolean - true when registered. The registration ends when your resource stops.
SetStreamProvider
Follow live streams from your own resource, for example to bridge a media server, record who watched, or moderate. Full guide: Building Apps → Live streams.
exports['oxide-phone']:SetStreamProvider({
Admit = function(stream, viewer) return true end, -- false refuses (viewer is nil for a new stream)
Started = function(stream) end,
Joined = function(stream, viewer) end,
Left = function(stream, viewer) end,
Ended = function(stream, reason) end,
})Returns: boolean - true when registered. One resource at a time; the registration ends when
your resource stops. The video still goes through the server.
RegisterEvidenceBackend
Send every extraction report to your own MDT. Full guide: Evidence Bridge.
exports['oxide-phone']:RegisterEvidenceBackend('my-mdt', {
AttachReport = function(src, caseRef, report) return true end, -- required
FlagNumber = function(number, info) end, -- optional
})Returns: boolean - true when registered. The server owner must also list the name under Evidence → Report destinations.
RegisterMailbox / UnregisterMailbox
Take the mail players send to an address of your own, such as a tip line or a business inbox.
exports['oxide-phone']:RegisterMailbox('tips@lspd.gov', function(mail)
-- mail = { from, to, subject, body }
print(('Tip from %s: %s'):format(mail.from, mail.body))
end)
exports['oxide-phone']:UnregisterMailbox('tips@lspd.gov')Parameters:
| Name | Type | Description |
|---|---|---|
| address | string | Any valid address. A SIM address always wins over yours. Yours wins over a player's alias with the same name, and players can't claim it as an alias while it is registered |
| handler | function | Called once for each mail sent to the address |
Returns: boolean - true when registered. The registration ends when your resource stops. While your resource is stopped, mail to the address bounces back to the sender.
SetAuthorizationCheck
Decide every search authorization from your own resource, when Evidence → Search authorization is Hook. Full guide: Evidence Bridge.
exports['oxide-phone']:SetAuthorizationCheck(function(src, target)
-- target = { deviceId?, number?, ownerCharId? }
return false
end)Returns: boolean - true when registered. The check is cleared when your resource stops.
Direct Exports
The exports o-link's adapter calls. Prefer the o-link functions above; these are listed so you know what exists.
| Side | Export | o-link function |
|---|---|---|
| Server | IsAvailable() | IsAvailable |
| Server | GetPhoneNumber(src) | GetPlayerPhone |
| Server | GetSourceByNumber(number) | GetSourceByNumber |
| Server | GetNumbersByIdentifier(identifier) | GetNumbersByIdentifier |
| Server | SendEmail(src, sender, subject, message) | SendEmail |
| Server | SendEmailOffline(identifier, sender, subject, message) | SendEmailOffline |
| Server | SendMessage(src, sender, message) | SendMessage |
| Server | SendNotification(src, data) | SendNotification |
| Server | Open, Close, Send, SetWidgetData, Notify, DismissNotification, ClearNotifications | Same names |
| Client | IsAvailable, IsOpen, RegisterApp, UnregisterApp, Open, Close, CloseApp, GetCurrentApp, Send, SetBadge, RegisterWidget, UnregisterWidget, SetWidgetData, Notify, DismissNotification, ClearNotifications | Same names |
The client RegisterApp and RegisterWidget exports refuse a definition whose resource is oxide-phone itself.
Server Events
Fired on the server with TriggerEvent, so any server script can listen with AddEventHandler. Players can't trigger them.
| Event | Arguments | When |
|---|---|---|
oxide:phone:server:evidenceReport | reportId, report | An extraction report was stored, before it goes to any report destination. See Evidence Bridge for the report fields |
oxide:phone:server:opened | src, deviceId | A player opened a phone |
oxide:phone:server:closed | src, deviceId, reason | A phone was put away. reason is one of closed, dropped, dead, flat, powered_off, unloaded, stopped, custody, erased |
oxide:phone:server:simChanged | deviceId, number | A SIM went into a phone (number) or came out (number is nil) |
oxide:phone:server:powerChanged | deviceId, on | A phone was switched on (true) or off (false) |
oxide:phone:server:textDelivered | deviceId, threadId, messageId | A text reached a phone |
oxide:phone:settingsReady | Settings finished loading from the database at start | |
oxide:phone:configUpdated | key | A setting changed (key is its name), or all settings were loaded (key is nil) |
olink:server:phone:account | appId, event | A phone logged in to an account of that app, or out of one: event is { deviceId, src?, account? }, src being the player carrying the phone and account the account id (nil when it logged out or was signed out from elsewhere). See Accounts |
olink:server:phone:stream | appId, event | Something happened to a live stream of that app: event is { id, app, type, viewers?, account?, reason?, streamerSrc, streamerDeviceId }, type being started, join, leave, ended or kicked, and account the viewer's on join and leave. See Live streams |
AddEventHandler('oxide:phone:server:evidenceReport', function(reportId, report)
print(('[my-script] phone report %d for case %s'):format(reportId, report.caseRef))
end)Client Events
Fired on the player's client with TriggerEvent.
| Event | Arguments | When |
|---|---|---|
olink:client:phone:ready | The phone started (about a second after its client script loads). Register your apps and widgets again | |
olink:client:phone:appOpened | appId | An app came on screen |
olink:client:phone:appReady | appId | The page on screen reported ready |
olink:client:phone:appClosed | appId, reason | An app left the screen: closed, home, switch or unregistered, or the reason the phone was put away |
olink:client:phone:widgetReady | widgetId | A frame widget reported ready |
olink:client:phone:notificationTapped | id, appId, data | The player tapped one of your app's notifications |
olink:client:phone:opened | deviceId | A phone came out |
olink:client:phone:closed | The phone was put away | |
oxide:phone:opened / oxide:phone:closed | The phone came on screen / went away. Useful for a HUD that hides itself | |
oxide:phone:configUpdated | key | A setting changed on this client |
AddEventHandler('oxide:phone:opened', function() DisplayRadar(false) end)
AddEventHandler('oxide:phone:closed', function() DisplayRadar(true) end)Reserved App IDs
The phone's own apps use these IDs. While the phone has one of them registered, registering it from another resource returns false. The first resource to register any ID keeps it.
| ID | App |
|---|---|
phone_calls, phone_messages, phone_mail, phone_camera, phone_gallery | Phone, Messages, Mail, Camera, Photos |
phone_notes, phone_calculator, phone_clock, phone_maps, phone_bank | Notes, Calculator, Clock, Maps, Wallet |
phone_darkchat, phone_feed | Dark Chat, Feed |
phone_settings, phone_store | Settings, Store |
phone_forensics | Forensics |
phone_demo | Demo (only while Debug mode is on) |
Notifications from SendNotification without an app use phone_system.
Integration Examples
A text from your job, with mail as the fallback
The Oxide gig jobs use this pattern: a text on phones that take texts, the same message as mail otherwise.
olink = exports['o-link']:olink()
local SERVICE = 'Downtown Cab Co.'
local function tellDriver(src, text)
local sendText = olink.phone and olink.phone.SendMessage
local ok, sent = false, false
if sendText then ok, sent = pcall(sendText, src, SERVICE, text) end
if not (ok and sent) then
local sendMail = olink.phone and olink.phone.SendEmail
if sendMail then pcall(sendMail, src, SERVICE, 'Dispatch', text) end
end
endA notification only when a phone is running
local function phoneAvailable()
local fn = olink.phone and olink.phone.IsAvailable
if not fn then return false end
local ok, available = pcall(fn)
return ok and available and true or false
end
local function ringDriver(src, label)
if not phoneAvailable() then return end
pcall(olink.phone.SendNotification, src, { title = 'New fare', message = label, icon = 'fa-solid fa-taxi' })
endMail to a character who may be offline
local function mailOwner(charId, plate)
local send = olink.phone and olink.phone.SendEmailOffline
if not send then return false end
local ok, sent = pcall(send, charId, 'LS Impound', 'Vehicle impounded',
('Your vehicle %s was towed to the impound lot.'):format(plate))
return ok and sent and true or false
endNext Steps
Admin Guide
Every /phone command, player keys, the in-game settings editor section by section, the admin tools inside the phone's apps, and how to grant admin access in oxide-phone.
Building Apps for the Phone
Developer guide for putting your own resource on oxide-phone — registering apps, the web SDK, widgets, notifications and badges, security, and phone services such as permissions, media, links, accounts, evidence, live streams and custom photo storage.