Pulse PhoneDocs
Pulse Phone docs

Pulse Phone: exports, events and state bags

This is the whole surface other resources can use. The phone resource is named phone. phone_api in the server console prints the add-on (app) API as a short summary. To add your own app to the phone, see CUSTOM_APPS.md next to this file.

1. State bags

The server writes these to Player(src).state. They are replicated, so clients read them as LocalPlayer.state.<key>, or Player(serverId).state.<key> for other players. The names match the ones other popular phone resources use, so existing HUD, dispatch and evidence scripts read them unchanged.

Key Type Meaning Set from
phoneNumber string | false number of the phone in hand (turn off with Config.Public.numberState = false) device load, provisioning, unique-phone switch
phoneOpen boolean phone out, screen shown client open/close, throttled flag event
phoneDisabled boolean disabled by SetPhoneDisabled server export
flashlight boolean torch on client torch toggle
airplaneMode boolean airplane mode client toggle
inCall boolean has a live call: dialling, ringing out or connected server call records
callAnswered boolean the call is connected server
onCallWith number | false server id of the other party (player-to-player) server
speakerphone boolean speaker on during a call server (phone:call:speaker)
mutedCall boolean this player muted their microphone on the call server (phone:call:signal)
otherMutedCall boolean the other party muted server
phoneItem boolean carries a phone item (Config.Item.required or unique phones) server inventory check
phoneBlocked boolean the player's state blocks the phone (dead, cuffed, escorted ... Config.Block) client state check, verified by the server
phoneEmergencyOnly boolean the phone opens on the emergency screen only (for example downed, Config.Block) client state check, verified by the server
phoneColour string | false unique phones only: finish id of the phone in hand item metadata / Config.Item.variants
phoneBattery number battery percent (0-100); other players' copies may lag (see below) server battery loop
phoneCharging boolean charging now server
phoneDead boolean battery empty: apps refused, calls ring out server
phone:anim, phone:anim:prop internal animation mirroring for other clients anim controller

Two limits:

2. Server events

Listen with AddEventHandler(name, fn) in a server script. Only the phone raises these. They are not net events, so a client cannot forge them.

Event Arguments
phone:numberChanged src, number \| nil, oldNumber \| nil
phone:toggled src, open
phone:callStarted callId, fromNumber, toNumber, fromSrc, toSrc (nil: not known yet), video
phone:callAnswered fromNumber, toNumber, fromSrc, toSrc, callId
phone:callEnded fromNumber, toNumber, reason, durationMs, fromSrc, toSrc, callId
phone:emergencyCall src, service ('police' \| 'ems' \| 'fire' \| ...), reason \| nil, coords (vector3) \| nil (Smart 911)
phone:emergencyText src, service, reason \| nil, body (Smart 911 emergency text)
phone:messageSent fromNumber, toNumber, body
phone:groupMessageSent fromNumber, groupId, body
phone:mailSent fromAddress, toAddress, subject
phone:wallet:transfer fromNumber, toNumber, amount, note
phone:photoSaved ownerNumber, url, kind ('photo' \| 'portrait' \| 'video')
phone:socialPost authorNumber, app, postId, caption, url
phone:mediaStored url, kind: a file the phone's upload stored
phone:mediaRefused src \| nil, url, where: a link the allow-list refused
phone:calendar:saved ownerNumber, event
phone:battery:died src

3. Client events

Listen with AddEventHandler in a client script. These are local and cannot be sent from the network.

Event Arguments
phone:toggled open, reason?
phone:openRefused reason: 'disabled', 'nui-focused' or 'check:<resource>'
phone:numberChanged number \| nil

4. Server exports (exports.phone:…)

Export Returns Notes
GetPhoneNumber(src) number | nil from cache; nil until the player has loaded
SourceOfNumber(number) src | nil online players only
FormatNumber(number) string in Config.Numbers.format
GetEmail(src) address | nil
GetEmailOfNumber(number, cb) cb(address \| nil) works for offline players too
HasPhoneItem(src, number?) boolean always true when Config.Item.required is off; with unique phones number asks for that phone
Notify(src, { app?, title, body, icon?, colour?, sticky?, actions?, deepLink? }) boolean
SetBadge(src, appId, count) boolean registered add-ons only
SendMessage(src, number, text) none as that player
SendMail(address, sender, subject, body) none
AddContact(ownerNumber, { name, number, avatar? }) boolean owner must be online
Call(src, number, video?) none places a call
IsInCall(src) boolean, callId
EndCall(src) boolean
SetPhoneDisabled(src, bool) / IsPhoneDisabled(src) boolean closes the phone if it is open
RecordTransaction(number, 'sent' \| 'received', amount, party, note?) boolean statement line only; does not move money
GetWalletBalance(number, cb) cb(balance \| nil)
UploadMedia(dataUrl, kind, cb) cb(url, reason) through the configured provider
IsMediaAllowed(url) boolean the server allow-list (Config.Media.allow)
Log(action, title, fields, src?) none writes to the phone's Discord/Fivemanage log
GetBattery(src) / SetBattery(src, percent) number server-authoritative level
IsCharging(src) / IsPhoneDead(src) boolean
ToggleCharging(src, on?, ratePerMin?) boolean an external charger (a cable item, a station script)
UsePowerbank(src) boolean single-use (reusable = false, the default): removes one Config.Battery.powerbank.item and charges amount % over time; reusable bank (reusable = true): plugs the phone in or out, charging to 100%
SaveBattery(src) / SaveAllBatteries() none levels also save every minute and on leave
RegisterApp / UnregisterApp / EmitApp / BroadcastApp / GetApps / Set / Get add-on apps (phone_api)
RegisterProvider / UnregisterProvider / GetProvider / HasCapability / BridgeReport / GetCharacterId providers (custom_provider_template.lua in this folder)
RegisterFrameworkBridge, RegisterSite, RegisterMarketsEconomy, AddDirectoryEntry see the files named in phone_api
PhoneTaken / PhoneReturned / PhoneLocked / PhoneBypass / PhoneCustody / PhoneImei stolen phones
DropPhone / DroppedPhones, PairAccessory / UnpairAccessory / SetAccessoryBattery / RegisterAccessoryKind, IsBlocked, MarketsQuotes / MarketsShock as named
ClearNotifications(src, app?) boolean empties the notification list on that player's phone (all, or one app id)
StartActivity(src, def) id | false, reason an Island activity on the Island and Lock Screen (section 14)
UpdateActivity(src, id, patch) / EndActivity(src, id) / EndAllActivities(src?) boolean / count your own resource's activities only (section 14)
PlaceScriptCall(target, opts) callId | false, reason ring a player from a script identity: "Unknown", "Dispatch" (section 15)
EndScriptCall(callId) / GetScriptCallState(callId) boolean / 'ringing' | 'connected' | nil your own script calls only (section 15)

5. Client exports (exports.phone:…)

Export Notes
Open() / Close() / IsOpen() Open respects every veto below
AddOpenCheck(fn) → id / RemoveOpenCheck(id) fn() returning false refuses the open. Checks are removed when their resource stops
SetDisabled(bool) / ToggleDisabled() / IsDisabled() a local switch; the server's SetPhoneDisabled drives the same one
GetPhoneNumber() from the state bag
FormatNumber(number)
IsInCall()
Notify({ app?, title, body, icon?, colour?, sticky? }) shown by this player's phone only
SetFlashlight(bool) / GetFlashlight() / GetAirplaneMode()
GetSignalBars() / SetAirplaneMode(bool)
RegisterCaptureProvider(p) / CaptureAvailable() camera capture backends
Rpc(name, data) the phone's own request channel; internal
useItem(data, slot) ox_inventory item hook for unique phones (client = { export = 'phone.useItem' })
usePowerbank() ox_inventory item hook for the power bank (client = { export = 'phone.usePowerbank' })
GetBattery() / IsCharging() / IsPhoneDead() this player's own level, exact; setting the level is server-only
SetRestriction(key, mode \| nil) a player state of your own for Config.Block (a jail, a cutscene, surgery): 'block', 'emergency-only' or nil to clear it. Kept per calling resource and cleared when that resource stops. Add key to Config.Block.calls to choose what happens to a live call (unlisted: the call is kept)
StartActivity(def) / UpdateActivity(id, patch) / EndActivity(id) / EndAllActivities() an Island activity on this player's phone, from a client script (section 14)

Open vetoes, in order: disabled, then another resource holding NUI focus (Config.Open.blockWhenNuiFocused), then each AddOpenCheck. A refusal raises phone:openRefused with the reason. Checks made before this one still apply: the player's state (Config.Block: dead, downed, cuffed, escorted, carried, jailed, under water, in the pause menu, or a SetRestriction of another script) and the phone item.

6. Compatibility with other phones

Many scripts (police, ambulance, boss menus, garages, deliveries) send mail, texts, notifications and job alerts using the event names other phone resources use. The phone answers those events itself, so those scripts work without edits. phone_api in the server console lists every event it answers.

A script that calls another phone's exports by that phone's resource name needs one change: call exports.phone instead. These seven server exports cover what such scripts do, and any script may use them directly:

Export What it does
ResolvePhone(target[, cb]) target is { source }, { number }, { email }, { identifier } or { imei }. Returns { number, email, name, identifier, imei, source }: with cb, online or offline; without it, online only, immediately.
DeliverMailTo(target, mail[, cb]) mail = { sender, subject, body, attachments? }. Calls cb(true, mailId) or cb(false, reason).
SendText(from, to, body[, opts][, cb]) From an online phone, a stored message. From anything else, a live service message named opts.name.
DispatchAlert(jobs, { title, body, coords? }) Notifies every on-duty member of the jobs; tapping the alert opens Maps at coords. Returns how many it reached.
CompanyMessage(src \| nil, jobOrCompany, body[, opts][, cb]) From a player, a message to the company's Services inbox, or an alert when anonymous. From a script, an alert.
PayBill(src, billId[, cb]) Pays one of the player's bills, as the Wallet does.
IsOnDuty(src[, job]) Whether the player is on duty in job, or in their own job when job is omitted.

7. Housing providers (Home app)

These are built in and chosen automatically when one of the scripts is running.

Vendor API each adapter uses (from each script's published source or documentation, cited in the adapter; not verified against live servers). "Owner's client" means the phone asks the owner's client to fire the event the housing script's own menu fires.

Script List Lock Keys
qb-houses its tables player_houses, houselocations (read only) its own lock event, from the owner's client its own give/remove events, from the owner's client
qbx_properties its properties table (read only) none (shells) list only (its key events need the owner inside)
qs-housing server export GetPlayerHouses none exposed none exposed
loaf_housing client export GetOwnedHouses server exports IsDoorLocked / SetDoorLocked client exports GiveKey, RemoveKeyHolder, GetKeyHolders
vms_housing server exports GetPlayerProperties / GetProperty none exposed its give/remove key events, from the owner's client
rtx_housing client export GetPlayerOwnedProperties its lock event + GlobalState its permission events, from the owner's client
ps-housing its properties table (read only) none (door lock script) its addAccess / removeAccess events, from the owner's client
bcs_housing server export GetOwnedHomeKeys server exports isLocked / LockHome (toggle) server exports GetKeyHolders, RemoveKeyHolder (giving a key stays in its menu)
nolag_properties server export GetAllProperties server export ToggleDoorlock (sets the state) server exports GetKeyHolders, AddKey, RemoveKey, as the player
esx_property server export GetProperties (with lock state) none exposed list from GetProperties

The loaf_housing calls could not be re-checked: its current version publishes no exports page.

8. Unique phones

Set Config.Item.unique = true. The number lives in the item's metadata:

Key Meaning
phoneNumber the number
phoneColour the finish
description tooltip text

Device rows for item phones are keyed item:<id>. Face Unlock enrols the character (Identity.CharacterKey), not the handset. The item's finish reaches the interface as bootstrap.device.colour and the phoneColour state bag. Drawing the handset in that finish (Device3D finishId) belongs to the OS layer and is not wired yet.

9. Browser sites (RegisterSite)

The Browser app cannot reach the internet. A site is registered by a resource on the server, and a page is data (a title and a list of blocks) that the phone draws in its own style. A site cannot carry script, a stylesheet or a URL outside the game. Add-on pages in the The add-on page kinds (list, detail, board, form) are accepted and converted.

exports.phone:RegisterSite({
    id       = 'pdm',                       -- [a-z0-9_], also the event namespace
    name     = 'Premium Deluxe',            -- shown on the start page tile
    host     = 'pdm.ls',                    -- optional; default '<id>.<Config.Browser.tld>'
    colour   = '#B8322F', glyph = 'car',    -- tile art (built-in glyph set)
    keywords = { 'cars', 'dealer' },        -- optional, for the search box
    pages    = { ['/'] = { title = 'Showroom', blocks = { ... } } },
    resolve  = function(src, path, cb) cb(page) end,              -- optional, dynamic pages
    submit   = function(src, path, form, values, reply) end,      -- optional, forms
})

Blocks: heading, text, stats, list, cards, links, form, directory, hero (a branded banner with an optional picture), posts (short posts with author and time) and empty (a designed "nothing here" panel). A list row, card or post may carry app + route to open a phone app (Services, Wallet, Echo...) instead of a page.

Built-in sites. Eight ship by default, each drawn from data the server really has or labelled as a brochure. Three are generated: the server's front page (name, players online, phones issued, the apps on the phone), the phone directory, and a help site built from Config. Five are in-world (<tld> is Config.Browser.tld):

Site Host Content
City Services city.<tld> Config.Services, live: who is on duty, call the line, message the company (opens Services)
City Wire citywire.<tld> public Echo posts and LS Pages adverts from the server's own tables; a designed empty page otherwise
Harbor Trust harbortrust.<tld> the caller's real balance from the bank provider, and the way into Wallet
Redline Motors redline.<tld> a showroom brochure (Config.Browser.showroom): class, seats and list price per model, a page per car
Chromeworks chromeworks.<tld> a customs price list (Config.Browser.customs) and the mechanic's line from Config.Services

Replace the brochure data in config, or turn a site off with Config.Browser.builtin = { redline = false, ... }.

10. Player data controls (requests for the interface)

Server requests the interface calls through the normal request bridge (fetchNui('phone:<name>', data) on the page, phone:rpc on the wire). Each acts on the caller's own phone only, is rate limited by the security layer, and answers { ok, reason? }.

Request Data Effect
calls:history:delete { id } (a Recents row's id from bootstrap) hides that call from this phone's Recents; the other party keeps theirs. reason = 'not-found' for an id that is not this phone's
calls:history:clear {} hides every call from this phone's Recents
messages:thread:delete { number } or { group } "Delete Conversation" for this phone only (same as messages:conversation:delete)
messages:threads:delete { threads = [ { number } \| { group }, ... ] } (at most 50) the same for several at once; answers { ok, deleted, failed }
notifications:clear { app? } empties the notification list (all, or one app id); the server relays it to the page as the NUI message notifications:clear { app? }, which the page must handle by calling clearNotifications(app)

A call row stays in the database until both sides have deleted it. Recents from bootstrap no longer include deleted calls.

11. Owner tools (console, or ace phone.admin)

<id|number> is an online server id (up to five digits) or any phone number, online or offline. Every command writes a security-log row and an activity-log entry.

Command Does
phone:resetpin <id\|number> removes the passcode and Face Unlock; the lockout clears
phone:wipe <id\|number> confirm erases the phone's own data (contacts, photos, notes, reminders, calendar, voicemail, memos, saved places, playlists, albums, blocks, mail) and hides its calls and conversations for that phone, then factory resets it. Without confirm it only says what it would erase
phone:number <id\|number> <newnumber> a new number: must fit Config.Numbers.format and be unused; the phone's own data follows it (conversations and call history stay under the old number). phone:number with no arguments shows your own number. Refused with unique phones on (the number is on the item)
phone:inspect <id\|number> a summary: number, mail, IMEI, created, online, setup, power, passcode, counts
phone:retention [now] runs Config.Retention pruning now (now is required while retention is off)
phone:health prints the start-up panel again, including voice conflicts and config problems

12. Clock and retention settings

Config.Clock.mode = 'server' (the default) publishes the server's clock in the global state bag phone:clock ({ hour, minute }), so every phone shows the same time; utcOffset pins a zone. 'game' follows the game's time of day (and so any weather/time sync script), 'real' is each player's own computer clock, 'custom' a function in config.

Config.Retention (off by default) prunes messages, social DMs, Dark Chat, call history, social activity and optionally mail older than the configured days, once a day at hour, in batches.

13. Bills, carrier, charging, store and payphones

All server side unless marked.

Server exports

Export Returns Notes
SendBill(sender, targetSrc, amount, reason, society?, cb?) id | false, reason sender: a player's server id, or a name for a script ('police', 'city_hall'). targetSrc must be online. society: optional job name ('police' or 'society_police'); payment then goes to that company's account. A script name with no society bills for the account of that name. cb(ok, idOrReason) runs once the bill is stored. Reasons: disabled, target, amount (not a whole number from 1 to Config.Bills.maxAmount), society, sender, self, rate (Config.Bills.perMinute), no-device
GetBills(src, cb?) { unpaid = {...}, paid = {...} } the phone's own bills plus esx_billing / okokBilling / qb-core phone_invoices rows, newest first; the paid list is capped at Config.Bills.paidHistory. Without cb it waits for the answer, so call it from a thread. Each bill: { id, source, from, company, number, reason, amount, at, status, paidAt }
IsWalletCardLocked(src) boolean true while that player has locked their phone's card in Wallet > Cards. A script that charges "the phone" (a shop's tap to pay) should refuse while it is locked, as the phone's own payments do
HasService(src, service?) boolean may this phone make a call / send a text / use data now ('call' default, 'text', 'data'). Always true while Config.Carrier.enabled is false
GetCarrierStatus(src) table | nil nil while the carrier is off or the line is not loaded. { service ('ok' \| 'no-plan' \| 'suspended' \| 'no-service'), bars, roaming, carrier, plan, job, line = { number, iccid, eid, status, autorenew, renewsAt, graceUntil, ... }, usage = { talkSeconds, texts, dataKB } }
UseMagPack(src, slot?) boolean as if the player used a MagPack item (ak47_inventory's onUse)
UseChargingCable(src, slot?) boolean as if the player used the cable item
DetachChargers(src) takes the MagPack and cable off the phone
GetChargingState(src) { pack, cable, source?, mode?, packLevel? } what is attached, and where the cable is plugged in
SetPowerSource(src, name \| false) boolean a power source your script provides (a house, a charging station): while set, the cable can be plugged in anywhere. false clears it and unplugs a cable using it
UseBox(src, slot?, name?) boolean as if the player used the sealed phone box (Config.Store.boxItem, or a per-finish box name)

Client exports (ox_inventory item hooks)

Export Item entry
usePack client = { export = 'phone.usePack' } on pulse_magpack
useCable client = { export = 'phone.useCable' } on pulse_cable
useBox client = { export = 'phone.useBox' } on pulse_phone_box (and the per-finish boxes)

The server re-reads the slot and the item for each of these; nothing the client sends is trusted. Ready-made item entries are in config/items/.

Server events

Event Arguments
phone:bills:sent billId, ownerNumber, amount, reason, job \| nil
phone:bills:paid fullId ('bill:<source>:<id>'), payerNumber, amount, job \| nil

State bags

Key Type Meaning
phoneCharger 'pack' | 'cable' | 'both' | nil the accessories on the phone in this player's hand; written only by the server
phone:unbox internal the unboxing scene other players see; written only by the server

Payphone calls

A payphone call is an ordinary call of the phone's call system, so phone:callStarted, phone:callAnswered and phone:callEnded fire for it. The caller's "number" is what Config.Payphone.callerId shows: Config.Payphone.callerLabel (default Payphone), Unknown, or the booth's own number. The payphone itself has no exports.

14. Island activities (Island and Lock Screen)

Another resource can show an Island activity on a player's phone: the same Island and Lock Screen card the phone's own timers and calls use, drawn by the same code. A delivery route, a heist countdown, a tow truck on its way.

Server (exports.phone:…)

local id, why = exports.phone:StartActivity(src, {
    id        = 'delivery',            -- optional, your own key (1-32 of A-Z a-z 0-9 _ -); starting it again replaces it
    title     = 'Delivery route',      -- required, 1-40 characters
    detail    = '3 stops left',        -- optional, up to 60
    compact   = '1/4',                 -- optional, up to 12: the short text beside the camera
    progress  = 0.25,                  -- optional, 0 to 1: a bar
    -- or a countdown the phone runs itself (no update per second needed):
    -- countdown = 90,                 -- seconds (1 s to 24 h), or endsAt = os.time() + 90
    -- endOnZero = true,               -- remove it when it reaches zero (else it stays at 0:00)
    glyph     = 'truck',               -- a glyph (list below), or appIcon = 'my_app' (an app's icon)
    colour    = '#FF9F0A',             -- #rrggbb, must reach 3:1 contrast on black
    link      = 'my_app/route/42',     -- optional: where a tap goes ('messages', 'my_app', 'my_app/<route>')
})
if not id then print('not shown: ' .. why) end

exports.phone:UpdateActivity(src, id, { detail = '2 stops left', progress = 0.5 })
exports.phone:UpdateActivity(src, id, { detail = false })   -- false (or '') clears a field
exports.phone:EndActivity(src, id)
exports.phone:EndAllActivities(src)     -- every activity YOUR resource shows on that player (nil = on everyone)

UpdateActivity takes any of the same fields except id; countdown = false stops a countdown, link = false removes the link. It returns true, or false, reason.

Client (exports.phone:…, this player's phone only)

local id = exports.phone:StartActivity({ title = 'Fuel', glyph = 'fuel', progress = 0.6 })
exports.phone:UpdateActivity(id, { progress = 0.55, compact = '55%' })
exports.phone:EndActivity(id)

Same definition and limits. endsAt is read against GetCloudTimeAsInt() on the client. A link or appIcon naming an add-on works once the phone has received this player's add-on list.

What the player sees

Where What
Island, one activity the glyph or app icon leading; compact trailing (else the countdown, else detail)
Island, two or three small pills either side of the camera. The phone's own call, timer and music always stay in front of yours
Island, held title, detail and the progress bar
Lock Screen the card: title, detail, progress bar and a stripe in your colour
Tap opens link (an app of the phone, or a registered add-on at the route). No link: the tap expands it

A countdown shows as m:ss (h:mm:ss over an hour) after your detail ("Drill running · 1:24") and fills the bar as it runs.

Glyphs: the Island's call, music, timer, navigation, record, camera, download, job, delivery, event, emergency, and the add-on set shield, cross, wrench, cart, bank, key, car, house, briefcase, radio, pin, bolt, book, chart, dice, fuel, gavel, heart, lock, note, plane, tag, truck. Default: event.

Limits (config/api.lua, Config.Api.activities)

Setting Default Meaning
perResource 2 activities one resource may show on one phone at once ('limit-resource')
perPlayer 4 from every script together ('limit-player'); the interface also never holds more than 6
updateMs 500 one activity updates at most this often. Faster updates are merged and the newest state is sent when the interval ends, so the last one always arrives
startsPerMinute 20 starts per resource per player per minute ('rate-limited')
enabled true false refuses every start ('disabled')

Text lengths are fixed by the screen: title 40, detail 60, compact 12 characters. Longer text is refused, not cut, with a reason such as 'title is longer than 40 characters'; an unknown field (detial = …) is refused too. Each refusal also prints one console line naming your resource (at most once a minute per reason).

Other reasons: 'player-offline', 'no-phone' (the player's phone has not loaded), 'unknown-activity' (not yours, or already ended), 'id cannot change'.

Cleanup

Your activities end when you end them, when your resource stops (the server and client halves each clear their own), when the player leaves, and - with endOnZero - when the countdown runs out. One resource can never update or end another's: activities are keyed by the calling resource.

15. Script calls (a custom or hidden caller id)

A server script can ring a player from an identity that is not a phone - a mission's "Unknown" caller, a dispatcher - through the phone's own call system: the ringtone, the call screen, answer and decline, call waiting when they are already talking, Recents, and the public call events (phone:callStarted, phone:callAnswered, phone:callEnded).

local callId, why = exports.phone:PlaceScriptCall(target, {   -- target: server id, or a phone number held by someone online
    label       = 'Unknown',           -- required, 1-32 characters: what the call screen and Recents show
    number      = '555-0142',          -- optional, 3-15 digits (+ - ( ) and spaces allowed)
    hidden      = false,               -- default: true without a number, false with one
    speaker     = dispatcherSrc,       -- optional: a player whose voice carries the call
    ringSeconds = 30,                  -- 5 to 60 (default Config.Api.scriptCalls.ringSeconds)
    onAnswer    = function(callId, src) end,
    onEnd       = function(callId, reason, durationMs) end,   -- 'ended' | 'missed' | 'declined' | 'dropped' | 'busy'
})
exports.phone:EndScriptCall(callId)          -- hang up from your side
exports.phone:GetScriptCallState(callId)     -- 'ringing' | 'connected' | nil

Audio. With speaker, that player is the calling end - their own phone shows the call - and the phone's existing voice routing puts both in one call channel (pma-voice, mumble-voip, SaltyChat, TokoVOIP, YACA), exactly as for a phone-to-phone call. Without speaker nobody is on the far end: the line connects and is silent, and your script supplies what the player hears or reads (onAnswer is where to start it).

Server only. These are server exports and no net event leads to them, so a client can never place a script call. EndScriptCall and GetScriptCallState only answer the resource that placed the call, and a player's own call can never be ended through them.

Logged. Every script call writes Script call placed and Script call ended to the phone's activity log (calls: the resource, the label, the number or withheld, the callee, the reason) and a call-history row, whose caller column is the number shown, or the label when the number is withheld. A script call never offers voicemail (there is no one to offer it to).

Refusals (false, reason): 'unavailable' (target offline, phone not loaded, dead battery or no service), 'busy' (already ringing or dialling), 'speaker-busy', 'speaker-is-target', 'limit-resource' (Config.Api.scriptCalls.perResource, default 8 at once), 'disabled', and a sentence for a bad option ('label is required', 'number must have 3 to 15 digits', 'unknown field "video"'). Your resource stopping hangs up its calls; the callbacks are not called then, because the resource is gone.

A working example of sections 14 and 15: pulse-phone-devapi-demo/ (in this folder) (/devkit_delivery, /devkit_heist, /devkit_call, /devkit_dispatch <id>, /devkit_fuel).

Still stuck?

Post in #pulse-issues (we go through it every week), or open a ticket in #open-a-ticket if it's urgent. Paste the start-up check from your server console.

Open the Discord