> For the complete documentation index, see [llms.txt](https://docs.sourcedev.pro/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.sourcedev.pro/src-phone/api-exports.md).

# API and Exports

This is the supported integration surface for resources that need to control or read `src-phone`. Client exports are called from a client script; server exports are called from a server script. Internal `src-phone:client:*` and `src-phone:server:*` transport events are not public API.

## Result conventions

Commands normally return `true` on success or `false, errorCode` when validation fails. Read-only exports return the requested value or `nil` when it is unavailable. Database-backed client reads use a callback because FiveM client exports cannot synchronously wait for a server callback.

```lua
local ok, reason = exports['src-phone']:openApp('messages', { number = '555-1234' })
if not ok then
    print(('phone request failed: %s'):format(reason))
end
```

## Client exports

### Phone and application control

| Export             | Parameters       | Return            | Purpose                                                                  |
| ------------------ | ---------------- | ----------------- | ------------------------------------------------------------------------ |
| `openPhone`        | `options?`       | `boolean, error?` | Opens the phone. `options.appId` and `options.params` can launch an app. |
| `closePhone`       | none             | `boolean, error?` | Closes the phone and releases its normal focus flow.                     |
| `togglePhone`      | none             | `boolean, error?` | Toggles the phone; the boolean is the resulting open state.              |
| `isPhoneOpen`      | none             | `boolean`         | Returns current visibility.                                              |
| `getPhoneState`    | none             | `table`           | Returns open, locked, active app, call, airplane and online state.       |
| `openApp`          | `appId, params?` | `boolean, error?` | Opens an installed built-in or registered external app.                  |
| `closeApp`         | none             | `boolean`         | Returns to the phone home screen.                                        |
| `getActiveApp`     | none             | `string?`         | Returns the active app ID.                                               |
| `refreshApp`       | `appId`          | `boolean, error?` | Increments that app's refresh key.                                       |
| `isAppInstalled`   | `appId`          | `boolean`         | Checks the player's installed-app list.                                  |
| `getInstalledApps` | none             | `string[]`        | Returns installed app IDs.                                               |

```lua
exports['src-phone']:openPhone({
    appId = 'dispatch-board',
    params = { incidentId = 2481 }
})
```

### External application registry

| Export              | Parameters                | Return            | Purpose                                                  |
| ------------------- | ------------------------- | ----------------- | -------------------------------------------------------- |
| `registerApp`       | `manifest`                | `boolean, error?` | Registers an external app owned by the calling resource. |
| `unregisterApp`     | `appId`                   | `boolean`         | Removes an app owned by the calling resource.            |
| `isAppRegistered`   | `appId`                   | `boolean`         | Checks the runtime registry.                             |
| `getRegisteredApps` | none                      | `manifest[]`      | Returns accepted, sanitized runtime manifests.           |
| `sendAppEvent`      | `appId, action, payload?` | `boolean, error?` | Delivers a typed event to an external app iframe.        |

Registration takes metadata from the external resource's root `config.lua`. See [Custom applications](/src-phone/custom-apps.md) for the complete manifest and resource lifecycle.

The owner is always `GetInvokingResource()`. Re-registering the same ID from the same resource updates it atomically; another resource cannot claim that ID. Registrations are removed automatically when their owner stops.

Registration errors:

| Error                                            | Meaning                                                                 |
| ------------------------------------------------ | ----------------------------------------------------------------------- |
| `manifest_must_be_a_table`                       | The manifest is not a Lua table.                                        |
| `unsupported_api_version`                        | `apiVersion` is missing or unsupported.                                 |
| `invalid_id`                                     | The ID fails the length or kebab-case rules.                            |
| `reserved_id`                                    | The ID belongs to a built-in application.                               |
| `id_already_registered`                          | A different resource already owns the ID.                               |
| `invalid_name`                                   | The display name is empty or longer than 18 Unicode characters.         |
| `invalid_ui_page` / `invalid_icon`               | An asset is not a safe relative resource path.                          |
| `invalid_accent_color`                           | The accent is not a six-digit hex color.                                |
| `invalid_permissions` / `unsupported_permission` | The permissions table is invalid or requests an unsupported capability. |
| `invalid_icon_class`                             | The fallback class contains unsupported characters or is too long.      |
| `external_resource_required`                     | The call did not originate from another resource.                       |
| `resource_not_started`                           | The owner resource is not starting or started.                          |

```lua
local ok, reason = exports['src-phone']:registerApp(PhoneAppConfig)
exports['src-phone']:sendAppEvent('dispatch-board', 'incident:updated', {
    id = 2481,
    status = 'assigned'
})
```

The external TypeScript app receives the event through the vendored SDK:

```ts
phone.onEvent((action, payload) => {
  if (action === 'incident:updated') {
    renderIncident(payload);
  }
});
```

### Notifications and badges

| Export                       | Parameters     | Return            |
| ---------------------------- | -------------- | ----------------- |
| `sendNotification`           | `notification` | `id?, error?`     |
| `removeNotification`         | `id`           | `boolean, error?` |
| `clearNotifications`         | `appId?`       | `boolean, error?` |
| `getUnreadNotificationCount` | `appId?`       | `number`          |
| `setAppBadge`                | `appId, count` | `boolean, error?` |
| `clearAppBadge`              | `appId`        | `boolean, error?` |

```lua
local id = exports['src-phone']:sendNotification({
    appId = 'dispatch-board',
    appName = 'Dispatch',
    title = 'New incident',
    body = 'Unit requested at Alta Street.',
    duration = 6000,
    params = { incidentId = 2481 }
})

exports['src-phone']:setAppBadge('dispatch-board', 3)
```

Notification limits are 80 characters for `title`, 500 for `body`, and 1-15 seconds for `duration`. Pressing a public notification opens `appId` with `params` and emits `src-phone:client:notificationPressed`.

### Identity and connectivity

| Export              | Parameters | Return                     |
| ------------------- | ---------- | -------------------------- |
| `getPhoneProfile`   | `callback` | `boolean, error?`          |
| `getPhoneNumber`    | `callback` | `boolean, error?`          |
| `getMailAddress`    | `callback` | `boolean, error?`          |
| `isPhoneAvailable`  | none       | `boolean`                  |
| `getConnectivity`   | none       | `{ online, airplaneMode }` |
| `isOnline`          | none       | `boolean`                  |
| `hasInternetAccess` | none       | `boolean`                  |
| `getAirplaneMode`   | none       | `boolean`                  |

```lua
exports['src-phone']:getPhoneProfile(function(profile)
    if not profile then return end
    print(profile.phoneNumber, profile.mailAddress, profile.firstName)
end)
```

### Calls

| Export              | Parameters         | Return            |
| ------------------- | ------------------ | ----------------- |
| `startCall`         | `number, options?` | `boolean, error?` |
| `answerCall`        | `number?`          | `boolean, error?` |
| `declineCall`       | `number?`          | `boolean, error?` |
| `endCall`           | `number?`          | `boolean, error?` |
| `getCallState`      | none               | `table?`          |
| `isInCall`          | none               | `boolean`         |
| `setSpeakerEnabled` | `enabled`          | `boolean, error?` |

`options.video = true` requests a video call. Call state is server-authoritative; a successful request does not guarantee that the receiver is online or accepts the call.

### Messages and mail

| Export                  | Parameters                    | Return            |
| ----------------------- | ----------------------------- | ----------------- |
| `sendMessage`           | `number, message, options?`   | `boolean, error?` |
| `openConversation`      | `number`                      | `boolean, error?` |
| `markConversationRead`  | `number`                      | `boolean, error?` |
| `getUnreadMessageCount` | none                          | `number`          |
| `sendMail`              | `address, { subject?, body }` | `boolean, error?` |
| `openMail`              | `mailId?`                     | `boolean, error?` |
| `getUnreadMailCount`    | none                          | `number`          |

```lua
exports['src-phone']:sendMessage('555-1234', 'Meet at the station.')
exports['src-phone']:sendMail('alex.doe@sourcedevpro', {
    subject = 'Shift report',
    body = 'The report is ready.'
})
```

### Contacts, pickers, media and location

| Export               | Parameters                      | Return                |
| -------------------- | ------------------------------- | --------------------- |
| `getContacts`        | `callback`                      | `boolean, error?`     |
| `getContactByNumber` | `number, callback`              | `boolean, error?`     |
| `addContact`         | `{ name, number, avatar? }`     | `boolean, error?`     |
| `editContact`        | `id, { name, number }`          | `boolean, error?`     |
| `deleteContact`      | `id`                            | `boolean, error?`     |
| `openContactPicker`  | `options, callback`             | `boolean, requestId?` |
| `openPhotoPicker`    | `options, callback`             | `boolean, requestId?` |
| `openLocationPicker` | `options, callback`             | `boolean, requestId?` |
| `openShareSheet`     | `options, callback`             | `boolean, requestId?` |
| `openConfirmDialog`  | `options, callback`             | `boolean, requestId?` |
| `openCamera`         | `options?`                      | `boolean, error?`     |
| `openGallery`        | `params?`                       | `boolean, error?`     |
| `saveSharedMedia`    | `{ url, mediaType? }, callback` | `boolean, error?`     |
| `getCurrentLocation` | none                            | `{ street, x, y, z }` |
| `setPhoneWaypoint`   | `{ x, y }`                      | `boolean, error?`     |
| `openMap`            | `params?`                       | `boolean, error?`     |
| `shareLocation`      | `number, options?`              | `boolean, error?`     |
| `stopLocationShare`  | none                            | `boolean`             |

Pickers are asynchronous. Cancellation and the 60-second timeout both return `nil` to the callback.

```lua
exports['src-phone']:openContactPicker({ title = 'Send to' }, function(contact)
    if not contact then return end
    exports['src-phone']:sendMessage(contact.number, 'Selected from the picker')
end)

exports['src-phone']:openConfirmDialog({
    title = 'Delete report',
    message = 'This cannot be undone.'
}, function(confirmed)
    if confirmed then deleteReport() end
end)
```

## Client lifecycle events

These are local client events. Listen with `AddEventHandler`; do not send them over the network.

| Event                                  | Arguments               |
| -------------------------------------- | ----------------------- |
| `src-phone:client:phoneOpened`         | none                    |
| `src-phone:client:phoneClosed`         | none                    |
| `src-phone:client:appOpened`           | `appId`                 |
| `src-phone:client:appClosed`           | `appId`                 |
| `src-phone:client:connectivityChanged` | `online`                |
| `src-phone:client:callStateChanged`    | `callState?`            |
| `src-phone:client:notificationPressed` | `{ id, appId, params }` |

```lua
AddEventHandler('src-phone:client:appOpened', function(appId)
    if appId == 'dispatch-board' then startLocalUpdates() end
end)
```

## Server exports

Server exports always take an explicit player `source` when the operation acts on a player. This makes ownership visible at the call site and prevents accidental access through the event sender global.

### Identity and state

| Export                       | Parameters | Return           |
| ---------------------------- | ---------- | ---------------- |
| `getPhoneNumber`             | `source`   | `string?`        |
| `getMailAddress`             | `source`   | `string?`        |
| `getPhoneIdentity`           | `source`   | `table?, error?` |
| `getSourceByPhoneNumber`     | `number`   | `number?`        |
| `getIdentifierByPhoneNumber` | `number`   | `string?`        |
| `isPhoneNumberOnline`        | `number`   | `boolean`        |
| `isPhoneAvailable`           | `source`   | `boolean`        |
| `getConnectivity`            | `source`   | `table?, error?` |
| `getPlayerPhoneState`        | `source`   | `table?`         |

### Player UI, notifications and app events

| Export               | Parameters                        | Return            |
| -------------------- | --------------------------------- | ----------------- |
| `openApp`            | `source, appId, params?`          | `boolean, error?` |
| `sendNotification`   | `source, notification`            | `boolean, error?` |
| `removeNotification` | `source, id`                      | `boolean, error?` |
| `clearNotifications` | `source, appId?`                  | `boolean, error?` |
| `setAppBadge`        | `source, appId, count`            | `boolean, error?` |
| `clearAppBadge`      | `source, appId`                   | `boolean, error?` |
| `sendAppEvent`       | `source, appId, action, payload?` | `boolean, error?` |

```lua
exports['src-phone']:sendNotification(source, {
    appId = 'dispatch-board',
    title = 'Assignment updated',
    body = 'You are now assigned to incident 2481.',
    params = { incidentId = 2481 }
})
```

### Calls, messages and mail

| Export                  | Parameters                          | Return               |
| ----------------------- | ----------------------------------- | -------------------- |
| `requestCall`           | `source, number, options?`          | `boolean, error?`    |
| `endPlayerCall`         | `source`                            | `boolean, error?`    |
| `getPlayerCallState`    | `source`                            | `table?`             |
| `isNumberBusy`          | `number`                            | `boolean`            |
| `sendMessage`           | `source, number, message, options?` | `boolean, error?`    |
| `sendSystemMessage`     | `number, message, options?`         | `boolean, id/error?` |
| `getUnreadMessageCount` | `source`                            | `number`             |
| `sendMail`              | `source, address, data`             | `boolean, error?`    |
| `sendSystemMail`        | `address, data`                     | `boolean, id/error?` |
| `sendMailToPlayer`      | `source, data`                      | `boolean, id/error?` |
| `getUnreadMailCount`    | `source`                            | `number`             |

`sendMessage` and `sendMail` act as the player and follow the normal player-owned validation path. `sendSystemMessage` and `sendSystemMail` create server-authored records and should only be called after the integrating resource has completed its own authorization checks.

```lua
exports['src-phone']:sendSystemMessage('555-1234', 'Your vehicle is ready.', {
    sender = '5550000'
})

exports['src-phone']:sendMailToPlayer(source, {
    sender = 'dispatch@sourcedevpro',
    subject = 'Assignment',
    body = 'Report to Mission Row.'
})
```

### Server contact access

| Export          | Parameters            | Return               |
| --------------- | --------------------- | -------------------- |
| `getContacts`   | `source`              | `contacts, error?`   |
| `addContact`    | `source, contact`     | `boolean, id/error?` |
| `editContact`   | `source, id, contact` | `boolean, error?`    |
| `deleteContact` | `source, id`          | `boolean, error?`    |

Contacts are scoped to the player's active MyCloud account. The exports do not accept an arbitrary account or owner key.

## Validation and security boundary

The public API validates IDs, phone numbers, text lengths, URLs, badge ranges, player sources and external manifest ownership. It deliberately does not expose:

* Bank balance mutations or transfers.
* Another player's message history, mailbox contents or contacts without an explicit player source.
* Raw SQL, KV storage or MyCloud owner keys.
* RTC channels, call routing tables or raw internal network events.
* Shell DOM access or unrestricted overlay creation.

An export confirms that a request was accepted; it does not replace authorization in the calling resource. Job checks, permissions, prices, inventory changes and rate limits remain the caller's server-side responsibility.

## Start order

```cfg
ensure src-phone
ensure your-integration
```

For an external application, also declare `dependency 'src-phone'` in its `fxmanifest.lua`. Registration is client-side and the boilerplate automatically re-registers when `src-phone` restarts.
