API Reference
The olink.tablet API for oxide-tablet — server and client functions, reserved IDs, client events, hosted page messages, widget data shapes and integration examples.
This page is for developers who want their resource to appear on oxide-tablet: registering an app or a home-screen widget, opening the tablet, pushing widget data and sending notifications. Server owners don't need anything on this page.
Use o-link
The tablet's API is olink.tablet, provided by the o-link bridge. It's the only supported way to integrate. The client_exports and server_exports in oxide-tablet's fxmanifest.lua exist so o-link's adapter (o-link/modules/tablet/oxide-tablet/) can reach the tablet. Don't call them yourself: going through o-link keeps your resource working when the tablet is missing, stopped or restarting.
olink = exports['o-link']:olink()Building your first app or widget? Follow Building Apps and Widgets. It walks through the web shim, the message protocol and a complete example. This page is the full reference behind that guide.
Integration Rules
Never depend on oxide-tablet
The tablet is optional. Don't list oxide-tablet in your fxmanifest.lua dependencies, and don't call exports['oxide-tablet']. Your resource must still work, with its own UI, when the tablet isn't installed.
Check availability the supported way
Pick one of these two approaches:
- Gate on the return value. Every function returns
false(ornil) when the tablet can't do what you asked, whether it's missing, stopped, disabled, or refused the call. Ifolink.tablet.Open(APP_ID)returnsfalse, open your own window instead. - Check before tablet-only work, such as deciding whether to register at all:
local function isTabletReady()
return GetResourceState('oxide-tablet') == 'started'
and olink.tablet.GetResourceName() == 'oxide-tablet'
endYou need both checks:
GetResourceName()returns'oxide-tablet'whenever o-link loaded the tablet adapter, even if the tablet has since been stopped.GetResourceStatealone doesn't tell you the adapter loaded. The adapter only loads ifoxide-tabletwas installed when o-link started. If the tablet was added later,olink.tabletstays on the stubs until o-link restarts.- Checking the resource state first also stops the stub from printing its one-time console warning.
Never gate on olink.supports('tablet') or olink.supports('tablet.RegisterApp'). o-link registers fallback stubs for the tablet namespace, so supports returns true even when the tablet isn't installed.
Register at your start and on the ready event
The tablet keeps its app and widget registry in memory on each client. Register in both places:
- When your client starts. This works if the tablet is already running.
- On
olink:client:tablet:ready. The tablet fires this about one second after its client script starts. It covers the tablet starting after you, and a tablet restart, which empties the registry.
Registering the same id again from the same resource replaces the earlier definition, so doing both is safe. Track the result with the return value, and reset your "registered" flag on olink:client:tablet:ready and when onClientResourceStop fires for oxide-tablet.
Widget data, badges and notifications are cleared when the character unloads (olink:client:playerUnload). Registered apps and widgets stay. Push widget data again after the next olink:client:playerReady.
How apps and widgets disappear
- Your resource stops. The tablet removes every app and widget whose
resourcefield matches your resource name. If one of your apps is on screen, the tablet returns to Home (appClosedwith reasonunregistered) and clears that app's notifications. - You call
UnregisterApporUnregisterWidget. Same effect, immediately. - The tablet stops. Its registry is gone. If it was open,
appClosed(reasonclosed) andclosedfire. When it starts again,olink:client:tablet:readyfires and you register again.
Automatic removal depends on def.resource. Your call reaches the tablet through o-link, so the tablet can't see which resource called it. Always pass resource = GetCurrentResourceName().
A player's saved home-screen position for a removed app or widget is kept. When it registers again, it goes back to its old spot.
Visibility rules are not security
requires, the item allowlist and App Store installs only decide what the launcher, App Store, widgets and notifications show. Open(appId) bypasses all of them, and your hosted page calls your own resource's NUI callbacks. Check permissions on your server before you send data. A common pattern is to call your own server callback from olink:client:tablet:appOpened and close the app if it refuses.
Telling your own opens apart
olink:client:tablet:appOpened fires for launcher taps, notification taps, widget taps and your own Open calls. It can fire before Open returns. If you need to know who opened the app, set a flag before calling Open.
Reserved IDs
App IDs and widget IDs are separate namespaces. An ID that belongs to another resource can't be registered: the call returns false.
| Kind | ID | Registered by the tablet |
|---|---|---|
| App | app_store | The App Store, while Config.Store.enabled is on |
| App | tablet_settings | The Settings app, always |
| App | tablet_map | Maps, while Config.MapEnabled is on |
| App | tablet_demo | Demo app, only while Config.Debug is on |
| Widget | calendar | Calendar, always |
| Widget | profile | ID card, always |
| Widget | tablet_demo_stat, tablet_demo_frame | Demo widgets, only while Config.Debug is on |
IDs must be 1 to 64 characters: letters, digits, _ and -.
Function Overview
The Stub default column is what the fallback stub returns when o-link loaded without the tablet adapter. Each stub function also prints a one-time [o-link] ... fallback defaults loaded for "tablet" warning. When the adapter is loaded but the tablet isn't started, the same values come back without a warning, except GetResourceName, which returns 'oxide-tablet'.
| Function | Side | Returns | Stub default |
|---|---|---|---|
GetResourceName() | Server + Client | string | 'none' |
Open(src, appId?) | Server | boolean | false |
Close(src) | Server | boolean | false |
Send(src, appId, message) | Server | boolean | false |
GetDevice(src) | Server | table|nil | nil |
SetWidgetData(src, id, data) | Server | boolean | false |
Notify(src, def) | Server | string|false | false |
DismissNotification(src, id) | Server | boolean | false |
ClearNotifications(src, appId?) | Server | boolean | false |
RegisterApp(def) | Client | boolean | false |
UnregisterApp(id) | Client | boolean | false |
Open(appId?) | Client | boolean | false |
Close() | Client | boolean | false |
CloseApp(appId) | Client | boolean | false |
IsOpen() | Client | boolean | false |
GetCurrentApp() | Client | string|nil | nil |
Send(appId, message) | Client | boolean | false |
SetBadge(appId, count) | Client | boolean | false |
GetDevice() | Client | table|nil | nil |
RegisterWidget(def) | Client | boolean | false |
UnregisterWidget(id) | Client | boolean | false |
SetWidgetData(id, data) | Client | boolean | false |
Notify(def) | Client | string|false | false |
DismissNotification(id) | Client | boolean | false |
ClearNotifications(appId?) | Client | boolean | false |
Server Functions
The server functions relay a request to one player's client. A true result means the request was sent, not that the player's tablet acted on it. The client applies the same rules as the matching client function.
Every server function returns false (or nil) when src isn't a positive number.
GetResourceName
Name of the resource providing the namespace.
local provider = olink.tablet.GetResourceName()Returns: string — 'oxide-tablet' when the adapter is loaded, 'none' from the stub.
Open
Opens a player's tablet, optionally straight into an app.
local sent = olink.tablet.Open(source, 'mechanic_orders')Parameters:
| Name | Type | Description |
|---|---|---|
src | number | Player server ID |
appId | string|nil | App to open. nil opens the home screen |
Returns: boolean — true when relayed. false when appId isn't a string or nil, or the tablet isn't running.
An open through the API never needs a tablet item and never starts a device session. If the player already had the tablet out from an item, that device session continues and GetDevice(src) still returns it. See the client Open for when the client refuses.
Close
Puts a player's tablet away.
olink.tablet.Close(source)Parameters:
| Name | Type | Description |
|---|---|---|
src | number | Player server ID |
Returns: boolean — true when relayed.
Send
Relays a message into a player's hosted app.
olink.tablet.Send(source, 'mechanic_orders', { action = 'setOrders', data = orders })Parameters:
| Name | Type | Description |
|---|---|---|
src | number | Player server ID |
appId | string | Your app ID |
message | table | { action = string, data = any }, the same shape as SendNUIMessage |
Returns: boolean — true when relayed. false when appId isn't a string or message isn't a table. The client drops the message unless the app is on screen. See the client Send.
GetDevice
The tablet item the player's open tablet was started from, resolved from the inventory on the server.
local device = olink.tablet.GetDevice(source)
if device then
print(device.imei, device.name, device.item, device.slot)
endParameters:
| Name | Type | Description |
|---|---|---|
src | number | Player server ID |
Returns: table|nil
| Field | Type | Description |
|---|---|---|
imei | string | 15-digit IMEI stamped in the item metadata |
name | string|nil | Name the player gave the tablet, nil if never renamed |
item | string | Item name, for example 'tablet' |
slot | number | Inventory slot the tablet was in when opened |
Returns nil when the player has no device session:
- the tablet was only ever opened by a resource (
Open) rather than from an item or the key; Config.RequireItemis off;- the inventory can't keep item metadata.
The device session ends when the client puts the tablet away, when the item leaves the player's inventory (checked every 5 seconds, which also closes the tablet), when the player drops, or when the character unloads. A keybind open that the client then refused, because the player was dead or downed, leaves one open until the item leaves their inventory, so a device can come back while nothing is on screen.
Key per-device state on this value. Never trust an IMEI sent by a client, including the tabletImei URL parameter.
SetWidgetData
Relays widget data to a player. Same effect as the client SetWidgetData.
olink.tablet.SetWidgetData(source, 'mechanic_open_orders', { value = '4', label = 'Open orders' })Parameters:
| Name | Type | Description |
|---|---|---|
src | number | Player server ID |
id | string | Your widget ID |
data | table|nil | Widget data (see Widget Data Shapes). nil shows the loading state |
Returns: boolean — true when relayed. false when id isn't a string or data isn't a table or nil. The server doesn't check whether the widget is registered or how large the data is. The client does.
Notify
Relays a push notification to a player. The ID is fixed on the server, so it comes back straight away.
local id = olink.tablet.Notify(source, {
app = 'mechanic_orders',
message = 'A customer is waiting at Hayes Autos',
})Parameters:
| Name | Type | Description |
|---|---|---|
src | number | Player server ID |
def | table | Same fields as the client Notify |
Returns: string|false — the notification ID app:id. The id part is def.id, or a generated s<n> when you leave it out. That number comes from one counter shared by every server-side Notify, so the numbers one app sees are not consecutive. Returns false when def isn't a table, def.app isn't a string, def.message is missing or empty, or def.data isn't a table or nil.
Relayed, not shown. The server can't see the player's client, so it returns an ID even when the client drops the notification: app not registered, app muted in the player's Settings app, notifications disabled, data over 8 KB, or an invalid id. Only the client return value reflects those.
Because generated IDs take the form s<number>, don't use custom IDs of that form for the same app, or they will replace each other.
DismissNotification
Removes one notification from a player's tablet.
olink.tablet.DismissNotification(source, 'mechanic_orders:order-12')Parameters:
| Name | Type | Description |
|---|---|---|
src | number | Player server ID |
id | string | The full ID Notify returned |
Returns: boolean — true when relayed. false when id isn't a string.
ClearNotifications
Removes every notification of one app, or all of them, from a player's tablet.
olink.tablet.ClearNotifications(source, 'mechanic_orders')Parameters:
| Name | Type | Description |
|---|---|---|
src | number | Player server ID |
appId | string|nil | App ID. nil clears every app |
Returns: boolean — true when relayed. false when appId isn't a string or nil.
Client Functions
GetResourceName
local provider = olink.tablet.GetResourceName()Returns: string — 'oxide-tablet' when the adapter is loaded, 'none' from the stub.
RegisterApp
Makes an app available on the tablet. Registering doesn't put it on anyone's home screen: players install it from the App Store, unless the owner turned the store off, preinstalled the app, or the tablet item lists it.
local ok = olink.tablet.RegisterApp({
id = 'mechanic_orders',
label = 'Work Orders',
icon = 'fa-solid fa-wrench',
resource = GetCurrentResourceName(),
url = 'web/dist/index.html',
requires = { jobs = { 'mechanic' }, duty = true },
category = 'business',
color = '#f59e0b',
})Parameters: def table
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | string | Yes | — | 1 to 64 characters: letters, digits, _, - |
label | string | Yes | — | Name under the icon. Must not be empty |
resource | string | Yes | — | Your resource name, GetCurrentResourceName(). Used for the page URL and for automatic removal |
icon | string | No | 'fa-solid fa-cube' | Font Awesome class |
url | string | No | 'web/dist/index.html' | Page inside your resource. Leading / is removed |
query | table | No | nil | Extra URL query parameters, { name = value } |
requires | table | No | nil | Who sees the app. See below |
order | number | No | 100 | Sort order, lower first. Ties sort by label |
color | string | No | nil | Six-digit hex such as '#f59e0b'. Anything else falls back to #9B2C2C |
readyTimeoutMs | number | No | Config.AppReadyTimeout (8000) | Milliseconds your page has to post ready before the tablet shows a retry screen |
category | string | No | 'utilities' | App Store category: business, finance, law, medical, social, utilities, other. Case-insensitive; unknown values become utilities |
description | string | No | fxmanifest description | App Store description. Trimmed and cut at 280 bytes |
tagline | string | No | nil | One-line App Store subtitle. Cut at 80 bytes |
publisher | string | No | fxmanifest author | Cut at 60 bytes |
version | string | No | fxmanifest version | Cut at 24 bytes |
The fields system, native and always are reserved for the tablet's own apps and are ignored from other resources.
requires:
| Field | Type | Description |
|---|---|---|
jobs | string[] | Job names allowed to see the app |
duty | boolean | With jobs: the player must also be on duty |
gangs | string[] | Gang names allowed to see the app |
- If both
jobsandgangsare given, matching either one is enough. - Missing or empty lists don't restrict anything.
- The tablet re-checks when
olink:client:jobChangedorolink:client:gangChangedfires. If the app on screen is no longer allowed, it closes (reasonclosed). requiresonly affects what's shown. See Visibility rules are not security.
Returns: boolean — true when registered or updated. false when:
defisn't a table, orid,labelorresourceis invalid;GetResourceState(def.resource)is'missing';- another resource already registered that
id; - the tablet isn't running.
UnregisterApp
Removes an app.
olink.tablet.UnregisterApp('mechanic_orders')Parameters:
| Name | Type | Description |
|---|---|---|
id | string | App ID |
Returns: boolean — true when removed. false when the ID isn't registered or is one of the tablet's built-in apps (app_store, tablet_settings).
If the app is on screen, the tablet returns to Home (appClosed with reason unregistered). The app's notifications and badge are cleared. Widgets that name the app in app stay registered but are hidden until the app is registered again.
The tablet doesn't check which resource registered the app, so only unregister your own IDs.
Open
Opens the tablet, optionally straight into an app.
local shown = olink.tablet.Open('mechanic_orders')Parameters:
| Name | Type | Description |
|---|---|---|
appId | string|nil | App to open. nil opens the home screen |
Returns: boolean — true when the tablet is open (and the app is on screen, if given). false when:
Config.Enabledis off;appIdisn't registered on this client;- the admin settings editor is open;
- the player is dead or downed (the player also sees "You can't use a tablet while you're down.");
- the tablet isn't running.
Behaviour:
- Opening by ID is the resource acting on its own behalf. It skips
requires, the item allowlist and App Store installs. Nothing is installed for the player. The app stays on screen until they leave it, until a job or gang change makes it no longer allowed byrequires, or, on a curated tablet item, because it is not on that item'sappslist. - Calling
Openfor the app already on screen returnstrueand changes nothing. Calling it for another app switches (appClosedwith reasonswitch, thenappOpened). - An API open starts no device session, so
GetDevice()returnsniland Home shows the character's home screen. If the tablet was already out from an item, that device stays. appOpenedcan fire beforeOpenreturns.
Close
Puts the tablet away.
olink.tablet.Close()Returns: boolean — true when the tablet was open and is now closed. false when it wasn't open.
Fires appClosed (reason closed) for the app on screen, then closed.
CloseApp
Returns to the home screen if the given app is on screen.
olink.tablet.CloseApp('mechanic_orders')Parameters:
| Name | Type | Description |
|---|---|---|
appId | string | Your app ID |
Returns: boolean — true when that app was on screen and was closed (reason closed). false when appId isn't a string, the tablet isn't open, no app is on screen, or a different app is on screen.
IsOpen
local open = olink.tablet.IsOpen()Returns: boolean — true while the tablet is out.
GetCurrentApp
local appId = olink.tablet.GetCurrentApp()Returns: string|nil — ID of the app on screen, including the built-in apps. nil on the home screen or when the tablet is closed.
Send
Relays a message into your app's page.
olink.tablet.Send('mechanic_orders', { action = 'setOrders', data = orders })Parameters:
| Name | Type | Description |
|---|---|---|
appId | string | Your app ID |
message | table | { action = string, data = any }. action must be a string |
Returns: boolean — true when handed to the tablet UI. false when:
message.actionisn't a string;- the tablet is closed;
appIdisn't the app on screen;- the app is one of the tablet's own native apps (App Store, Settings, Maps);
- the tablet isn't running.
Messages sent before your page posts ready are queued and delivered in order. The queue holds 100 messages and drops the oldest beyond that. It's emptied when the player presses Retry after a timeout.
SetBadge
Sets the number on your app's icon.
olink.tablet.SetBadge('mechanic_orders', 3)Parameters:
| Name | Type | Description |
|---|---|---|
appId | string | Your app ID |
count | number | Badge number. Rounded down; negative or non-numbers become 0 |
Returns: boolean — true when set. false when appId isn't registered.
The badge shown is the larger of your count and the app's pending notifications. SetBadge(appId, 0) doesn't hide notifications — call ClearNotifications(appId) once your app has handled them. Widgets that name the app in app show the same badge. Badges are cleared when the character unloads.
GetDevice
The device the open tablet was started from.
local device = olink.tablet.GetDevice()Returns: table|nil
| Field | Type | Description |
|---|---|---|
imei | string | 15-digit IMEI |
name | string|nil | Name the player gave the tablet |
battery | number | Charge as a whole percent, 0 to 100. It goes down while the tablet is out and is kept up to date while the tablet is open. It stays at its stored value when the owner has battery drain turned off |
Returns nil when the tablet is closed, or the open session has no device: it was started by Open, the item isn't required, or the inventory can't keep metadata. For anything that matters, confirm on the server with GetDevice(src).
RegisterWidget
Makes a home-screen widget available. Players add widgets from the widget gallery. Nothing is placed on a screen automatically.
local ok = olink.tablet.RegisterWidget({
id = 'mechanic_open_orders',
label = 'Open Orders',
resource = GetCurrentResourceName(),
type = 'stat',
sizes = { 'small', 'medium' },
icon = 'fa-solid fa-wrench',
app = 'mechanic_orders',
})Parameters: def table
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | string | Yes | — | 1 to 64 characters: letters, digits, _, -. Separate from app IDs |
label | string | Yes | — | Name in the gallery and widget header |
resource | string | Yes | — | GetCurrentResourceName() |
type | string | Yes | — | 'stat', 'list', 'progress', 'text' or 'frame' |
sizes | string[] | No | { 'small' } | Any of 'small' (2x2 cells), 'medium' (4x2), 'large' (4x4). Unknown and duplicate entries are dropped. The first entry is used when a saved size is no longer allowed. A page is 8x5 cells |
icon | string | No | 'fa-solid fa-cube' | Font Awesome class |
color | string | No | nil | Six-digit hex accent |
app | string | No | nil | App ID. The widget only shows while that app is visible to the player, shows the app's badge, and opens the app when tapped |
requires | table | No | nil | Same shape as the app's requires |
order | number | No | 100 | Gallery sort order |
data | table | No | nil | Initial data. Same as calling SetWidgetData, with the same 8192-byte limit: data over the limit is dropped and the widget still registers |
url | string | No | 'web/dist/index.html' | frame only: page inside your resource |
query | table | No | nil | frame only: extra URL query parameters |
readyTimeoutMs | number | No | Config.AppReadyTimeout (8000) | frame only: time to post ready before the widget shows "Unavailable" |
Returns: boolean — true when registered or updated. false when:
defisn't a table, orid,label,resourceortypeis invalid;GetResourceState(def.resource)is'missing';- another resource already registered that
id; - the tablet isn't running.
Re-registering without data keeps the data already cached.
UnregisterWidget
olink.tablet.UnregisterWidget('mechanic_open_orders')Parameters:
| Name | Type | Description |
|---|---|---|
id | string | Widget ID |
Returns: boolean — true when removed, false when not registered.
A placed widget disappears from the screen and its cached data is dropped. The player's saved slot is kept for when it registers again.
SetWidgetData
Pushes a widget's data.
olink.tablet.SetWidgetData('mechanic_open_orders', { value = '4', label = 'Open orders', trend = 'up' })Parameters:
| Name | Type | Description |
|---|---|---|
id | string | Widget ID |
data | table|nil | See Widget Data Shapes. nil or an empty table shows the loading state |
Returns: boolean — true when stored. false when the widget isn't registered, data isn't a table or nil, or data is larger than 8192 bytes once JSON-encoded.
The value is cached whether or not the tablet is open, so the home screen shows the latest data as soon as it appears. Calls are cheap: the tablet only redraws while open. Template widgets redraw; frame widgets receive { action = 'tablet:widgetData', data = data }. The cache is cleared when the character unloads.
Notify
Posts a push notification for a registered app. The tablet draws it; your app never does.
local id = olink.tablet.Notify({
app = 'mechanic_orders',
id = 'order-12',
title = 'New work order',
message = 'Vehicle 46EEK572 needs a repair',
data = { orderId = 12 },
})Parameters: def table
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
app | string | Yes | — | A registered app ID |
message | string | Yes | — | Body text. Cut at 280 characters |
title | string | No | nil | Title. Cut at 80 characters |
icon | string | No | The app's icon | Font Awesome class |
id | string|number | No | Generated #<n> | Your own ID, namespaced per app. 1 to 64 bytes, must not start with #. Posting the same ID again replaces the earlier notification, moves it to the top and shows the banner again |
data | table | No | nil | At most 8192 bytes JSON-encoded. Stays in Lua until the player taps, then goes to your page as { action = 'tablet:notification', data = data } |
sound | boolean | No | true | false never plays the sound |
toast | boolean|nil | No | nil | Controls the banner and bubble. See the table below |
Returns: string|false — the ID app:id (for example mechanic_orders:order-12). false when:
def.appisn't registered;- the player switched that app's notifications off in their Settings app;
Config.Notifications.enabledis off;messageis missing or empty;dataisn't a table or is over 8 KB;idis invalid;- the tablet isn't running.
Where it shows:
toast | Tablet open, your app on screen | Tablet open, anything else on screen | Tablet put away |
|---|---|---|---|
nil | Tray only | Banner + tray | Bubble at the bottom right |
true | Banner + tray | Banner + tray | Bubble at the bottom right |
false | Tray only | Tray only | Nothing on screen, kept for the tray |
- Bubbles also need
Config.Notifications.showWhenClosed. - The player's Do Not Disturb, Banners and Sound switches apply on top of this table.
- The sound plays whenever the table calls for a banner or bubble, unless
sound = false, sound is off for the server or the player, or the player has Do Not Disturb on. The player's Banners switch hides the banner but doesn't silence the sound. - A notification only appears for an app that is visible on this tablet (installed, allowed by
requiresand by the item). Otherwise it's kept silently, counts towards the badge, and shows in the tray once the app is visible.
Notifications stay until tapped, dismissed or cleared. They're dropped beyond Config.Notifications.maxStored (oldest first), on character unload, and when the app is unregistered or its resource stops. Tapping opens the app under the same rules as a launcher tap, relays data, removes the notification and fires notificationTapped.
DismissNotification
olink.tablet.DismissNotification('mechanic_orders:order-12')Parameters:
| Name | Type | Description |
|---|---|---|
id | string | The full ID Notify returned |
Returns: boolean — true when removed, false when not found.
ClearNotifications
olink.tablet.ClearNotifications('mechanic_orders')Parameters:
| Name | Type | Description |
|---|---|---|
appId | string|nil | App ID. nil clears every app |
Returns: boolean — true for a valid argument, even if nothing was removed. false when appId isn't a string or nil, or the tablet isn't running.
Server Exports Outside o-link
oxide-tablet's fxmanifest.lua also lists two server exports, GetSetting and SetSetting. They belong to the in-game settings system that Oxide resources with a /<resource> settings editor share. They are not part of olink.tablet: the o-link adapter doesn't wrap them, the API above doesn't include them, and no Oxide resource calls them. There is no supported o-link path to them. Calling them means depending on oxide-tablet directly, which the integration rules rule out for apps and widgets.
What they do, for completeness:
| Export | Returns | Behaviour |
|---|---|---|
GetSetting(key) | any | The current value of Config[key] on the server, for example 'RequireItem'. nil for unknown keys. Returns the shared/config.lua value until settings finish loading from the database at boot |
SetSetting(key, value) | boolean | Saves the value to oxide_settings, sends it to every client and applies runtime effects: Items re-registers usable items, and Enabled = false closes every open tablet |
SetSetting has no admin check. It returns false before settings finish loading, for values containing NaN or infinity, and for invalid values of MapEnabled, MapAutoBlips, MapMode, MapLocations and MapBlipOverrides. Other keys are not checked against the limits the settings editor enforces.
Server Events
oxide-tablet fires no server-side events for other resources. Its oxide:tablet:* net events and oxide-tablet:server:* callbacks are internal. Don't trigger or listen to them.
Client Events
All tablet events are local client events fired with TriggerEvent. Listen with AddEventHandler. They aren't net events, so RegisterNetEvent isn't needed.
olink:client:tablet:ready
The tablet's client started or restarted, about one second after its script loads. (Re)register your apps and widgets here, then push widget data.
AddEventHandler('olink:client:tablet:ready', function()
-- the registry is empty: register again
end)olink:client:tablet:appOpened
An app was put on screen by a launcher tap, a notification tap, a widget tap or an Open call. Fires before the page has loaded, and can fire before Open returns. Also fires for the built-in apps.
AddEventHandler('olink:client:tablet:appOpened', function(appId)
-- appId: string
end)olink:client:tablet:appReady
The app's page posted ready, and queued Send messages were delivered. The built-in apps report ready immediately.
AddEventHandler('olink:client:tablet:appReady', function(appId)
-- appId: string
end)olink:client:tablet:appClosed
An app left the screen.
AddEventHandler('olink:client:tablet:appClosed', function(appId, reason)
-- appId: string, reason: 'home' | 'closed' | 'switch' | 'unregistered'
end)| Reason | When |
|---|---|
home | The player went Home: the Home control, ESC while Config.EscapeBehavior is 'home', or the page posted home |
closed | The tablet was put away, the page posted close, CloseApp was called, or the app is no longer allowed or installed |
switch | Another app opened in its place |
unregistered | UnregisterApp, the registering resource stopped, or a built-in app was switched off |
olink:client:tablet:closed
The tablet was put away for any reason: the player, the key, ESC, using the item again, death or downed state, the item leaving the inventory, Config.Enabled turned off, the admin settings editor opening, character unload, a script, or the tablet stopping. Fires after appClosed.
AddEventHandler('olink:client:tablet:closed', function()
end)olink:client:tablet:widgetReady
A frame widget's page posted ready while the tablet is open. Queued data was delivered.
AddEventHandler('olink:client:tablet:widgetReady', function(widgetId)
-- widgetId: string
end)olink:client:tablet:deviceRenamed
The player renamed the tablet they have open.
AddEventHandler('olink:client:tablet:deviceRenamed', function(device)
-- device: { imei = string, name = string, battery = number }
end)olink:client:tablet:notificationTapped
The player tapped a notification (a banner or a tray entry). By the time this fires, the tablet has switched to the app, queued { action = 'tablet:notification', data = data } for your page (only when data was set and the app isn't a built-in one), and removed the notification. If the app wasn't already on screen, its page may still be loading. Anything you Send from here is queued behind it.
AddEventHandler('olink:client:tablet:notificationTapped', function(id, appId, data)
-- id: string ('app:id'), appId: string, data: table|nil
end)Hosted Page Messages
Apps and frame widgets are iframes of your resource's own files. Building Apps and Widgets covers the web side step by step; this section is the reference.
Iframe URLs
App:
https://cfx-nui-<resource>/<url>?tabletHost=oxide-tablet&<params><params> are tabletApp=<id>, your query entries, and tabletImei=<imei> when the tablet was opened from an item. They're sorted by name and URL-encoded. The names tabletHost, tabletApp, tabletWidget, tabletSize and tabletImei belong to the tablet: it drops them from your query.
Frame widget:
https://cfx-nui-<resource>/<url>?tabletHost=oxide-tablet&<params>&tabletSize=<small|medium|large><params> are tabletWidget=<id>, your query entries (minus the same reserved names), and tabletImei=<imei> when the tablet was opened from an item. Changing the widget's size reloads the iframe with the new tabletSize.
The tablet only loads URLs that start with https://cfx-nui-<name>/, where <name> is letters, digits, _ and -. A resource whose name contains any other character (such as .) can't be hosted.
tabletImei is informational. Confirm it on the server with GetDevice(src).
Tablet to your page
Posted with postMessage in the same { action, data } shape as SendNUIMessage.
action | data | Sent to | When |
|---|---|---|---|
| Your own action | Your data | Apps | You called Send. Queued until ready |
tablet:notification | The notification's data | Apps | The player tapped a notification that had data. Queued until ready |
tablet:widgetData | table or null | Frame widgets | You called SetWidgetData, and again right after your page posts ready if data is cached |
Your page to the tablet
window.parent.postMessage({ type: 'oxide-tablet', event: 'ready', app: appId }, '*')Messages must come from the hosted iframe and have type: 'oxide-tablet'. For apps, a message whose app names a different app is ignored.
event | Apps | Frame widgets |
|---|---|---|
ready | Delivers queued messages and stops the load timer. Post it only after your message listeners exist | Delivers queued messages, resends cached data, fires widgetReady |
escape | The tablet applies its ESC rule (Config.EscapeBehavior) | Same as apps |
home | Back to the home screen (appClosed reason home) | Ignored |
close | Back to the home screen (appClosed reason closed). It doesn't put the tablet away | Ignored |
openApp | Ignored | Opens the message's app, or the widget's app if none is given, under launcher-tap rules |
If an app doesn't post ready within readyTimeoutMs, it shows that the app "didn't respond" with a Retry button. A widget shows "Unavailable". Never call SetNuiFocus from a hosted page: the tablet owns focus.
Widget Data Shapes
All shapes are Lua tables passed to SetWidgetData (client or server). Every field is optional unless marked required. Colors are six-digit hex strings.
stat
| Field | Type | Description |
|---|---|---|
value | string|number | The big number. Shows — when missing |
label | string | Line under the value |
sub | string | Small grey text |
icon | string | Font Awesome class beside the value, on medium and large only |
trend | see trend | Arrow pill |
list
| Field | Type | Description |
|---|---|---|
title | string | Heading, shown only without summary |
summary | table | Headline number: { value (required), label?, sub?, trend? }. Drawn above the rows, or as its own column on medium |
items | table[] | Rows: { text, value?, sub?, icon?, color?, trend? } |
empty | string | Text when items is empty. Defaults to "Nothing here yet" |
Rows shown per size: small 3 (2 with summary), medium 3 (3), large 8 (6). Extra rows collapse into "+N more". On small, rows are one line: no icon tile, no sub, no trend, and a colored dot when color is set. Elsewhere a row shows icon (or the first letter of text) on a tile tinted with color.
progress
| Field | Type | Description |
|---|---|---|
label | string | Text above the bar |
value | number | Current value. Non-numbers count as 0 |
max | number | Defaults to 100. Must be above 0 |
sub | string | Small text under the bar |
color | string | Bar color. Falls back to the widget's color |
The percentage is clamped to 0 to 100. medium and large also show value / max.
text
| Field | Type | Description |
|---|---|---|
title | string | Heading |
body | string | Text. Clipped at 4 lines on small, 5 on medium and 13 on large |
frame
Anything. It's posted into your iframe as { action = 'tablet:widgetData', data = data }.
trend
Used by stat, list summaries and list rows:
| Value | Shows |
|---|---|
'up', 'down', 'flat' | Arrow only |
| number | Arrow from the sign, text as a signed percentage (12 shows +12%) |
{ dir = 'up', text = '+$980' } | Arrow with your own text |
Integration Examples
App with a fallback window
Registers a work-orders app for on-duty mechanics, re-registers when the tablet restarts, and falls back to the resource's own window when the tablet isn't there.
olink = exports['o-link']:olink()
local APP_ID = 'mechanic_orders'
local registered = false
local function isTabletReady()
return GetResourceState('oxide-tablet') == 'started'
and olink.tablet.GetResourceName() == 'oxide-tablet'
end
local function registerTabletApp()
registered = false
if not isTabletReady() then return end
if olink.tablet.RegisterApp({
id = APP_ID,
label = 'Work Orders',
icon = 'fa-solid fa-wrench',
resource = GetCurrentResourceName(),
requires = { jobs = { 'mechanic' }, duty = true },
category = 'business',
color = '#f59e0b',
}) then
registered = true
end
end
-- Tablet already running when this resource starts
CreateThread(registerTabletApp)
-- Tablet started after us, or restarted
AddEventHandler('olink:client:tablet:ready', registerTabletApp)
AddEventHandler('onClientResourceStop', function(resource)
if resource == 'oxide-tablet' then registered = false end
end)
local function openOwnWindow()
SetNuiFocus(true, true)
SendNUIMessage({ action = 'open' })
end
RegisterCommand('orders', function()
if registered and olink.tablet.Open(APP_ID) then return end
openOwnWindow()
end, false)Checking access when the app opens
requires only hides the tile. This asks the server every time the app opens and backs out if the player isn't allowed.
AddEventHandler('olink:client:tablet:appOpened', function(appId)
if appId ~= APP_ID then return end
local orders = olink.callback.Trigger('mechanic:server:getOrders')
if not orders then
olink.tablet.CloseApp(APP_ID)
olink.notify.Send('You are not on duty as a mechanic.', 'error')
return
end
olink.tablet.Send(APP_ID, { action = 'setOrders', data = orders }) -- queued until the page is ready
end)A stat widget fed from the server
-- Register next to the app, from the same two places
local WIDGET_ID = 'mechanic_open_orders'
local function registerTabletWidget()
if not isTabletReady() then return end
olink.tablet.RegisterWidget({
id = WIDGET_ID,
label = 'Open Orders',
resource = GetCurrentResourceName(),
type = 'stat',
sizes = { 'small', 'medium' },
icon = 'fa-solid fa-wrench',
app = APP_ID, -- hidden unless the app is visible; tapping opens it
})
end
CreateThread(registerTabletWidget)
AddEventHandler('olink:client:tablet:ready', registerTabletWidget)olink = exports['o-link']:olink()
local function pushOpenOrders(src, count)
olink.tablet.SetWidgetData(src, 'mechanic_open_orders', {
value = count,
label = 'Open orders',
sub = 'Waiting for a mechanic',
trend = count > 5 and 'up' or 'flat',
})
endNotifications with a deep link
The server posts one notification per order. Reposting the same order replaces it. The client clears the app's notifications once the player opens it.
local function notifyNewOrder(src, orderId, plate)
return olink.tablet.Notify(src, {
app = 'mechanic_orders',
id = 'order-' .. orderId, -- same order, same notification
title = 'New work order',
message = ('Vehicle %s needs a repair'):format(plate),
data = { orderId = orderId }, -- your page receives { action = 'tablet:notification', data = ... } on tap
})
endAddEventHandler('olink:client:tablet:appOpened', function(appId)
if appId ~= APP_ID then return end
-- SetBadge(APP_ID, 0) can't hide pending notifications; clearing them does
olink.tablet.ClearNotifications(APP_ID)
end)
AddEventHandler('olink:client:tablet:notificationTapped', function(_, appId, data)
if appId ~= APP_ID or type(data) ~= 'table' then return end
olink.tablet.Send(APP_ID, { action = 'showOrder', data = { orderId = data.orderId } })
end)Next Steps
Building Apps and Widgets
Step-by-step guide with a worked example.
Settings App
The player Settings app, including per-app notification mutes.
Admin Tools
/tablet notify for testing notifications in game.
Configuration
Config.Store, Config.Notifications and the other options that shape what your app sees.
Admin Tools
Every /tablet command, the live settings editor section by section, and how to grant admin access in oxide-tablet.
Building Apps and Widgets
Developer guide for putting your own resource on oxide-tablet — registering apps and widgets, the host shim, the web side, notifications, lifecycle, and a complete example resource.