Browse docs

Exports

Client and server exports provided by Free Phone, with arguments, return values, security boundaries, and complete Lua examples.

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.

Keep the resource folder named 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): Local web/... entry file or allowed remote UI. Required.
      • description (string | table?): Optional description.
      • developer (string?): Developer or resource label.
      • category (string?): games, productivity, shopping, social, or utilities.
      • icon (string?): Local web/... icon or allowed remote icon.
      • permissions (table?): Requested app permissions.
      • orientation (string?): portrait, landscape, or any.
      • defaultInstalled (boolean?): Installs the app by default.
      • removable (boolean?): Allows the player to uninstall the app.
  • Returns: boolean, string?true on success, otherwise false and a stable error code.
client.lua
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 existing id.
  • Returns: boolean, string?.
client.lua
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?.
client.lua
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?.
client.lua
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?.
client.lua
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?.
client.lua
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, or soft.
      • critical (boolean?): Requires notifications.critical.
      • persistent (boolean?): Keeps the notification visible until handled.
      • route (string?): Must resolve to the app's own route.
  • Returns: boolean, string?.
client.lua
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.
client.lua
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?.
server.lua
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 including id and every allowed permission.
  • Returns: boolean, string?.
server.lua
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?.
server.lua
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.
server.lua
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.
server.lua
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.
server.lua
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 or nil and an error code.
server.lua
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 by CreateInvoice.
    • actorIdentifier (string): Character or integration actor responsible for cancellation.
  • Returns: boolean.
server.lua
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 as ambulance.
  • Returns: number | nil.
server.lua
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.
server.lua
local removed = exports["sky_phone"]:RemoveBillingAccountBalance(
    "ambulance",
    5000
)

if not removed then
    print("Insufficient billing balance or invalid request")
end

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.
server.lua
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.
server.lua
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.
server.lua
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.
server.lua
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, or info.
      • 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.
server.lua
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 by CreateCrewLinkPing.
  • Returns: boolean.
server.lua
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.

RuntimeExportSignature
ClientAddCustomAppFromAdapter(ownerResource, definition)
ClientUpdateCustomAppFromAdapter(ownerResource, definition)
ClientRemoveCustomAppFromAdapter(ownerResource, appId)
ClientOpenCustomAppFromAdapter(ownerResource, appId, data?)
ClientCloseCustomAppFromAdapter(ownerResource, appId)
ClientCloseActiveCustomAppFromAdapter(ownerResource)
ClientSendCustomAppMessageFromAdapter(ownerResource, appId, payload)
ClientSendCustomAppNotificationFromAdapter(ownerResource, appId, notification)
ServerAddCustomAppPolicyFromAdapter(ownerResource, definition)
ServerUpdateCustomAppPolicyFromAdapter(ownerResource, definition)
ServerRemoveCustomAppPolicyFromAdapter(ownerResource, appId)
client.lua
-- 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 aliasSupported client exports
lb-phoneAddCustomApp, RemoveCustomApp, SendCustomAppMessage, OpenApp, CloseApp, SendNotification
17mov_PhoneAddApplication, RemoveApplication, SendAppMessage
high-phoneaddApplication, sendAppNui
qs-smartphoneaddCustomApp, addCustomAppsBatch, updateCustomApp, removeCustomApp, getCustomApps, OpenPhoneApp
yseriesAddCustomApp, 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