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 through olink.phone so your resource still works when a different phone, or none, is installed.
  • Check for a phone with IsAvailable(). It is true only while a real phone runs. Don't gate on olink.supports('phone.SendEmail'): o-link has fallback stubs, so that is true even with no phone installed.
  • Functions from o-link are callable tables. Nil-check the member and call it through pcall. Don't compare type(fn) == 'function'.
  • Check the return value. Every function returns false (or nil) 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.


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:

NameTypeDescription
srcnumberPlayer 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:

NameTypeDescription
numberstringA 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:

NameTypeDescription
identifierstringThe 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:

NameTypeDescription
srcnumberPlayer server ID
senderstringDisplay name of the sender, up to 100 characters
subjectstringUp to 100 characters
messagestringUp 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:

NameTypeDescription
identifierstringThe character identifier, up to 64 characters
senderstringUp to 100 characters
subjectstringUp to 100 characters
messagestringUp 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:

NameTypeDescription
srcnumberPlayer server ID
senderstringThe service name, up to 60 characters
messagestringUp 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:

NameTypeDescription
srcnumberPlayer server ID
data.messagestringRequired. Up to 280 characters
data.titlestringOptional. Up to 80 characters
data.iconstringOptional. A Font Awesome class
data.appstringOptional. 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:

NameTypeDescription
srcnumberPlayer server ID
appIdstring|nilApp 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:

NameTypeDescription
srcnumberPlayer server ID
appIdstringYour app ID
messagetable{ 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's

For 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.


FunctionReturnsDescription
IsAvailable()booleantrue while oxide-phone is started
GetResourceName() / GetPhoneName()string'oxide-phone'
SendEmail(sender, subject, message)booleanMail 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()booleanThe phone is on screen
RegisterApp(def) / UnregisterApp(id)booleanPut your app on the phone. See Building Apps → Register an app
Open(appId?)booleanOpen the phone the player carries, straight into appId if given. With the phone already open, switch to appId
Close()booleanPut the phone away
CloseApp(appId?)booleanBack to the home screen if appId (or any app) is on screen
GetCurrentApp()string|nilThe app on screen
Send(appId, message)booleanPost { action, data } into your page. false unless that app is on screen
SetBadge(appId, count)booleanThe number on your app's icon
RegisterWidget(def) / UnregisterWidget(id)booleanPut a widget on the home screen. See Building Apps → Widgets
SetWidgetData(id, data)booleanPush widget data. Up to 8 KB once encoded
Notify(def)string|falseA notification for your app. Returns the ID (app:id)
DismissNotification(id) / ClearNotifications(appId?)booleanRemove 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',
    })
end

Phone-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:

NameTypeDescription
keystringA 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:

NameTypeDescription
keystringA top-level setting name that is in the settings menu
valueanyThe 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:

NameTypeDescription
addressstringAny 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
handlerfunctionCalled 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.

SideExporto-link function
ServerIsAvailable()IsAvailable
ServerGetPhoneNumber(src)GetPlayerPhone
ServerGetSourceByNumber(number)GetSourceByNumber
ServerGetNumbersByIdentifier(identifier)GetNumbersByIdentifier
ServerSendEmail(src, sender, subject, message)SendEmail
ServerSendEmailOffline(identifier, sender, subject, message)SendEmailOffline
ServerSendMessage(src, sender, message)SendMessage
ServerSendNotification(src, data)SendNotification
ServerOpen, Close, Send, SetWidgetData, Notify, DismissNotification, ClearNotificationsSame names
ClientIsAvailable, IsOpen, RegisterApp, UnregisterApp, Open, Close, CloseApp, GetCurrentApp, Send, SetBadge, RegisterWidget, UnregisterWidget, SetWidgetData, Notify, DismissNotification, ClearNotificationsSame 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.

EventArgumentsWhen
oxide:phone:server:evidenceReportreportId, reportAn extraction report was stored, before it goes to any report destination. See Evidence Bridge for the report fields
oxide:phone:server:openedsrc, deviceIdA player opened a phone
oxide:phone:server:closedsrc, deviceId, reasonA phone was put away. reason is one of closed, dropped, dead, flat, powered_off, unloaded, stopped, custody, erased
oxide:phone:server:simChangeddeviceId, numberA SIM went into a phone (number) or came out (number is nil)
oxide:phone:server:powerChangeddeviceId, onA phone was switched on (true) or off (false)
oxide:phone:server:textDelivereddeviceId, threadId, messageIdA text reached a phone
oxide:phone:settingsReadySettings finished loading from the database at start
oxide:phone:configUpdatedkeyA setting changed (key is its name), or all settings were loaded (key is nil)
olink:server:phone:accountappId, eventA 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:streamappId, eventSomething 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.

EventArgumentsWhen
olink:client:phone:readyThe phone started (about a second after its client script loads). Register your apps and widgets again
olink:client:phone:appOpenedappIdAn app came on screen
olink:client:phone:appReadyappIdThe page on screen reported ready
olink:client:phone:appClosedappId, reasonAn app left the screen: closed, home, switch or unregistered, or the reason the phone was put away
olink:client:phone:widgetReadywidgetIdA frame widget reported ready
olink:client:phone:notificationTappedid, appId, dataThe player tapped one of your app's notifications
olink:client:phone:openeddeviceIdA phone came out
olink:client:phone:closedThe phone was put away
oxide:phone:opened / oxide:phone:closedThe phone came on screen / went away. Useful for a HUD that hides itself
oxide:phone:configUpdatedkeyA 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.

IDApp
phone_calls, phone_messages, phone_mail, phone_camera, phone_galleryPhone, Messages, Mail, Camera, Photos
phone_notes, phone_calculator, phone_clock, phone_maps, phone_bankNotes, Calculator, Clock, Maps, Wallet
phone_darkchat, phone_feedDark Chat, Feed
phone_settings, phone_storeSettings, Store
phone_forensicsForensics
phone_demoDemo (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
end

A 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' })
end

Mail 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
end

Next Steps