Browse docs

Client Exports

Client Exports for the Free Phone custom-app, billing and CrewLink integrations.

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

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.