Browse docs
Exports
This page documents the supported public integration calls. Client exports must run in a client script and server exports must run in a server script. Internal NUI callbacks and net events are not public APIs.
sky_phone. Validate every returned success value and keep money,
permissions, ownership, and other authoritative changes on the server.Client Exports
Client exports register and control custom apps owned by the calling resource. Free Phone binds the
owner automatically through GetInvokingResource().
AddCustomApp(definition)
- Purpose: Registers a custom app owned by the calling resource.
- Arguments:
definition(table): Complete app definition.id(string): Stable lowercase app ID. Required.name(string | table): Display name or localized names. Required.ui(string): Localweb/...entry file or allowed remote UI. Required.description(string | table?): Optional description.developer(string?): Developer or resource label.category(string?):games,productivity,shopping,social, orutilities.icon(string?): Localweb/...icon or allowed remote icon.permissions(table?): Requested app permissions.orientation(string?):portrait,landscape, orany.defaultInstalled(boolean?): Installs the app by default.removable(boolean?): Allows the player to uninstall the app.
- Returns:
boolean, string?—trueon success, otherwisefalseand a stable error code.
local app = {
id = "dispatch-board",
name = {
en = "Dispatch Board",
de = "Leitstelle",
},
description = {
en = "Live dispatch information",
de = "Live-Informationen der Leitstelle",
},
developer = "my_dispatch",
category = "productivity",
ui = "web/index.html",
icon = "web/icon.webp",
orientation = "portrait",
defaultInstalled = true,
removable = true,
permissions = {
"app.close",
"device.storage",
"notifications",
"theme.read",
"locale.read",
},
}
local registered, error_code = exports["sky_phone"]:AddCustomApp(app)
if not registered then
print(("[my_dispatch] Registration failed: %s"):format(error_code))
end
Local UI and icon files belong to the calling resource and must be included in that resource's
fxmanifest.lua.
UpdateCustomApp(definition)
- Purpose: Replaces an owned custom app definition.
- Arguments:
definition(table): The complete definition, including the existingid.
- Returns:
boolean, string?.
local updated, error_code = exports["sky_phone"]:UpdateCustomApp({
id = "dispatch-board",
name = "Dispatch Board",
description = "Live incidents and unit status",
developer = "my_dispatch",
category = "productivity",
ui = "web/index.html",
icon = "web/icon.webp",
defaultInstalled = true,
removable = true,
permissions = {
"app.close",
"device.storage",
"notifications",
},
})
if not updated then
print(("[my_dispatch] Update failed: %s"):format(error_code))
end
This export does not accept a partial patch. Omitted fields are removed from the stored definition.
RemoveCustomApp(appId)
- Purpose: Removes an app owned by the calling resource.
- Arguments:
appId(string): App ID used during registration.
- Returns:
boolean, string?.
local removed, error_code = exports["sky_phone"]:RemoveCustomApp("dispatch-board")
if not removed then
print(("[my_dispatch] Removal failed: %s"):format(error_code))
end
Owned apps are also removed automatically when their resource stops.
OpenCustomApp(appId, data?)
- Purpose: Opens an owned app in an already visible phone.
- Arguments:
appId(string): Registered app ID.data(table?): JSON-compatible opening payload delivered to the iframe.
- Returns:
boolean, string?.
RegisterCommand("dispatchphone", function()
local opened, error_code = exports["sky_phone"]:OpenCustomApp(
"dispatch-board",
{
incidentId = 184,
source = "command",
}
)
if not opened then
print(("[my_dispatch] Could not open app: %s"):format(error_code))
end
end)
This export navigates inside an open phone. It does not bypass the phone item, lock screen, or device session.
CloseCustomApp(appId)
- Purpose: Closes the active owned custom app.
- Arguments:
appId(string): Registered app ID.
- Returns:
boolean, string?.
local closed, error_code = exports["sky_phone"]:CloseCustomApp("dispatch-board")
if not closed then
print(("[my_dispatch] Close failed: %s"):format(error_code))
end
SendCustomAppMessage(appId, payload)
- Purpose: Sends JSON-compatible data to an active and ready owned app.
- Arguments:
appId(string): Registered app ID.payload(any): JSON-compatible payload within the configured message-size limit.
- Returns:
boolean, string?.
local sent, error_code = exports["sky_phone"]:SendCustomAppMessage(
"dispatch-board",
{
type = "incident:update",
incident = {
id = 184,
status = "responding",
assignedUnit = "ADAM-12",
},
}
)
if not sent then
print(("[my_dispatch] Message failed: %s"):format(error_code))
end
Messages sent while the app is opening are queued up to the configured queue limit and delivered after the iframe reports bridge readiness.
SendCustomAppNotification(appId, notification)
- Purpose: Creates a validated phone notification for an owned app.
- Arguments:
appId(string): Registered app ID.notification(table): Notification definition.title(string?): Defaults to the app name.text(string): Notification body. Required.subtitle(string?): Optional secondary text.sound(string?):chime,signal, orsoft.critical(boolean?): Requiresnotifications.critical.persistent(boolean?): Keeps the notification visible until handled.route(string?): Must resolve to the app's own route.
- Returns:
boolean, string?.
local sent, error_code = exports["sky_phone"]:SendCustomAppNotification(
"dispatch-board",
{
title = "New dispatch",
subtitle = "Incident #184",
text = "Priority call at Mission Row",
sound = "signal",
}
)
if not sent then
print(("[my_dispatch] Notification failed: %s"):format(error_code))
end
The app definition must contain notifications. Critical notifications additionally require
notifications.critical.
GetCustomAppCapabilities()
- Purpose: Reads the active custom-app ABI, protocol, bridge methods, context capabilities, and configured limits.
- Returns:
table.
local capabilities = exports["sky_phone"]:GetCustomAppCapabilities()
print("ABI", capabilities.abiVersion)
print("Protocol", capabilities.protocolVersion)
print("Enabled", capabilities.enabled)
for _, method in ipairs(capabilities.bridgeMethods or {}) do
print("Bridge method", method)
end
Use capability detection instead of assuming that a future Free Phone version supports a specific bridge extension.
Server Exports
Server exports handle custom-app policies, billing, and CrewLink integrations. Do not proxy these
exports through an unvalidated RegisterNetEvent.
Custom-app policies
Client definitions and server policies are separate. A client definition cannot grant itself server-side permissions.
AddCustomAppPolicy(definition)
- Purpose: Registers a server policy owned by the calling resource.
- Arguments:
definition(table): Policy definition.id(string): Matching custom-app ID. Required.permissions(table): Server-approved permissions.
- Returns:
boolean, string?.
local registered, error_code = exports["sky_phone"]:AddCustomAppPolicy({
id = "dispatch-board",
permissions = {
"device.storage",
"notifications",
},
})
if not registered then
error(("[my_dispatch] Policy registration failed: %s"):format(error_code))
end
UpdateCustomAppPolicy(definition)
- Purpose: Replaces the complete policy owned by the calling resource.
- Arguments:
definition(table): Full policy includingidand every allowed permission.
- Returns:
boolean, string?.
local updated, error_code = exports["sky_phone"]:UpdateCustomAppPolicy({
id = "dispatch-board",
permissions = {
"device.storage",
"notifications",
"notifications.critical",
},
})
if not updated then
print(("[my_dispatch] Policy update failed: %s"):format(error_code))
end
RemoveCustomAppPolicy(appId)
- Purpose: Removes a policy owned by the calling resource.
- Arguments:
appId(string): Registered app ID.
- Returns:
boolean, string?.
local removed, error_code = exports["sky_phone"]:RemoveCustomAppPolicy("dispatch-board")
if not removed then
print(("[my_dispatch] Policy removal failed: %s"):format(error_code))
end
GetCustomAppPolicy(appId)
- Purpose: Reads the normalized server policy for an app.
- Arguments:
appId(string): App ID.
- Returns:
table | nil.
local policy = exports["sky_phone"]:GetCustomAppPolicy("dispatch-board")
if policy then
print("Owner", policy.ownerResource)
for _, permission in ipairs(policy.permissions or {}) do
print("Permission", permission)
end
end
Treat the returned table as a snapshot. Mutating it does not update the registered policy.
HasCustomAppPermission(appId, permission)
- Purpose: Checks whether a registered policy contains one permission.
- Arguments:
appId(string): App ID.permission(string): Supported permission name.
- Returns:
boolean.
local can_store = exports["sky_phone"]:HasCustomAppPermission(
"dispatch-board",
"device.storage"
)
if not can_store then
print("Dispatch Board has no storage permission")
end
GetCustomAppCapabilities()
- Purpose: Reads the server-side custom-app ABI and policy capabilities.
- Returns:
table.
local capabilities = exports["sky_phone"]:GetCustomAppCapabilities()
print("External apps enabled", capabilities.enabled)
print("ABI", capabilities.abiVersion)
Billing Exports
Billing exports are server-only and validate invoice state and monetary mutations inside Free Phone.
CreateInvoice(data)
- Purpose: Creates a validated invoice for an online player or known character identifier.
- Arguments:
data(table): Invoice definition.recipientSource(number?): Online recipient server ID.recipientIdentifier(string?): Known character identifier when no source is supplied.issuerSource(number?): Online issuer server ID.issuerIdentifier(string?): Known issuer identifier.issuerAccount(string): Account receiving paid funds.issuerLabel(string): Display name shown to the recipient.title(string): Invoice title.description(string?): Invoice details.amount(number): Server-validated invoice amount.dueDays(number?): Optional payment period.
- Returns:
string | nil, string?— invoice ID orniland an error code.
local invoice_id, error_code = exports["sky_phone"]:CreateInvoice({
recipientSource = target_source,
issuerSource = source,
issuerAccount = "ambulance",
issuerLabel = "Los Santos Medical",
title = "Medical treatment",
description = "Emergency treatment and medication.",
amount = 1300,
dueDays = 7,
})
if not invoice_id then
print(("[medical] Invoice failed: %s"):format(error_code))
return
end
print(("[medical] Created invoice %s"):format(invoice_id))
Never accept an invoice amount from an untrusted client without validating the service and price on the server.
CancelInvoice(invoiceId, actorIdentifier)
- Purpose: Cancels an open invoice.
- Arguments:
invoiceId(string): Invoice ID returned byCreateInvoice.actorIdentifier(string): Character or integration actor responsible for cancellation.
- Returns:
boolean.
local cancelled = exports["sky_phone"]:CancelInvoice(
invoice_id,
issuer_identifier
)
if not cancelled then
print("Invoice could not be cancelled")
end
GetBillingAccountBalance(accountKey)
- Purpose: Reads the current internal balance for a billing account.
- Arguments:
accountKey(string): Account key such asambulance.
- Returns:
number | nil.
local balance = exports["sky_phone"]:GetBillingAccountBalance("ambulance") or 0
print(("Medical billing balance: $%s"):format(balance))
RemoveBillingAccountBalance(accountKey, amount)
- Purpose: Atomically withdraws an amount when sufficient balance is available.
- Arguments:
accountKey(string): Billing account key.amount(number): Positive server-calculated withdrawal amount.
- Returns:
boolean.
local removed = exports["sky_phone"]:RemoveBillingAccountBalance(
"ambulance",
5000
)
if not removed then
print("Insufficient billing balance or invalid request")
end
CrewLink Exports
CrewLink read exports require a valid phone session. External ping mutations additionally require
the calling resource in Config.CrewLink.ExternalPingResources.
GetCrewLinkActiveGroup(source)
- Purpose: Returns the active CrewLink group of a player with an open, unlocked phone session.
- Arguments:
source(number): Player server ID.
- Returns:
table | nil.
local group = exports["sky_phone"]:GetCrewLinkActiveGroup(source)
if group then
print(("Active CrewLink group: %s"):format(group.id))
end
GetCrewLinkGroupMembers(groupId)
- Purpose: Returns public member data for a CrewLink group.
- Arguments:
groupId(string): CrewLink group ID.
- Returns:
table— array of member records.
local members = exports["sky_phone"]:GetCrewLinkGroupMembers(group_id)
for _, member in ipairs(members) do
print(member.id, member.username, member.role, member.joinedAt)
end
IsCrewLinkGroupMember(source, groupId)
- Purpose: Checks whether a player belongs to a CrewLink group.
- Arguments:
source(number): Player server ID.groupId(string): CrewLink group ID.
- Returns:
boolean.
if exports["sky_phone"]:IsCrewLinkGroupMember(source, group_id) then
print("Player is a member of the group")
end
GetCrewLinkGroupRole(source, groupId)
- Purpose: Returns a player's role in a CrewLink group.
- Arguments:
source(number): Player server ID.groupId(string): CrewLink group ID.
- Returns:
string | nil.
local role = exports["sky_phone"]:GetCrewLinkGroupRole(source, group_id)
if role == "owner" or role == "moderator" then
print("Player can manage this CrewLink group")
end
CreateCrewLinkPing(groupId, data)
- Purpose: Creates a shared external ping for a CrewLink group.
- Arguments:
groupId(string): Target group ID.data(table): Ping definition.type(string):meeting,danger,help,target, orinfo.label(string): Player-facing ping label.coords(table):{ x, y, z }world coordinates.lifetimeSeconds(number?): Requested lifetime within configured limits.
- Returns:
table | nil, string?— created ping or an error code.
local ping, error_code = exports["sky_phone"]:CreateCrewLinkPing(
group_id,
{
type = "target",
label = "Mission target",
coords = {
x = 125.0,
y = -840.0,
z = 30.0,
},
lifetimeSeconds = 180,
}
)
if not ping then
print(("CrewLink ping failed: %s"):format(error_code))
end
RemoveCrewLinkPing(groupId, pingId)
- Purpose: Removes an external ping created by the calling resource.
- Arguments:
groupId(string): CrewLink group ID.pingId(string): Ping ID returned byCreateCrewLinkPing.
- Returns:
boolean.
local removed = exports["sky_phone"]:RemoveCrewLinkPing(group_id, ping.id)
if not removed then
print("Ping does not exist or belongs to another resource")
end
Trusted Adapter Exports
Exports ending in FromAdapter are reserved for resources explicitly listed in
Config.CustomApps.TrustedAdapters. Normal custom apps must use the owner-bound exports above.
| Runtime | Export | Signature |
|---|---|---|
| Client | AddCustomAppFromAdapter | (ownerResource, definition) |
| Client | UpdateCustomAppFromAdapter | (ownerResource, definition) |
| Client | RemoveCustomAppFromAdapter | (ownerResource, appId) |
| Client | OpenCustomAppFromAdapter | (ownerResource, appId, data?) |
| Client | CloseCustomAppFromAdapter | (ownerResource, appId) |
| Client | CloseActiveCustomAppFromAdapter | (ownerResource) |
| Client | SendCustomAppMessageFromAdapter | (ownerResource, appId, payload) |
| Client | SendCustomAppNotificationFromAdapter | (ownerResource, appId, notification) |
| Server | AddCustomAppPolicyFromAdapter | (ownerResource, definition) |
| Server | UpdateCustomAppPolicyFromAdapter | (ownerResource, definition) |
| Server | RemoveCustomAppPolicyFromAdapter | (ownerResource, appId) |
-- Trusted adapter example only. Normal apps must not use this export.
local registered, error_code = exports["sky_phone"]:AddCustomAppFromAdapter(
"legacy_dispatch",
converted_definition
)
The adapter resource remains responsible for mapping the external contract and preserving the real owner resource.
Compatibility Aliases
Free Phone exposes a documented subset of common third-party phone contracts:
| Resource alias | Supported client exports |
|---|---|
lb-phone | AddCustomApp, RemoveCustomApp, SendCustomAppMessage, OpenApp, CloseApp, SendNotification |
17mov_Phone | AddApplication, RemoveApplication, SendAppMessage |
high-phone | addApplication, sendAppNui |
qs-smartphone | addCustomApp, addCustomAppsBatch, updateCustomApp, removeCustomApp, getCustomApps, OpenPhoneApp |
yseries | AddCustomApp, RemoveCustomApp, SendAppMessage, CloseApp, GetDataLoaded |
Compatibility is contract-based and does not emulate every feature of the original phone.
Support
Need help with an integration? Our support team is ready to assist.
Join Discord