API Reference
Lua exports, REST API, and Socket.IO events for integrating with Tommy's Radio.
Server Exports
Channel Information
| Export | Parameters | Returns |
|---|---|---|
getSpeakersInChannel | frequency: string|number | table — server IDs connected as speakers |
getListenersInChannel | frequency: string|number | table — server IDs scanning (listen-only) |
getAllUsersInChannel | frequency: string|number | table — speakers + listeners combined |
getActiveTalkersInChannel | frequency: string|number | table — server IDs with PTT held right now |
getActiveChannels | — | string[] — frequencies with at least one active user |
getChannelInfo | frequency: string|number | { speakers, listeners, activeTalkers } or nil |
getAllChannels | — | table[] — full channel list from config (zones, names, types, etc.) |
local speakers = exports['tRadio']:getSpeakersInChannel("154.755")
local info = exports['tRadio']:getChannelInfo("154.755")
-- info = { speakers = {...}, listeners = {...}, activeTalkers = {...} }Alerts & Panic
| Export | Parameters | Returns |
|---|---|---|
getChannelAlert | frequency | alert config object or nil |
setAlertOnChannel | frequency, enabled: boolean|nil, alertIndexOrName: number|string|nil | — |
getChannelPanic | frequency | { [serverId] = true, ... } |
setChannelPanic | frequency, serverId, enabled: boolean | — |
getUserPanicState | serverId, frequency | boolean |
setAlertOnChannel resolution rules:
alertIndexOrNameomitted ornil→ first alert in config- number → 1-based index into the alerts array
- string → case-insensitive name match
enabled = nil→ toggle current state
exports['tRadio']:setAlertOnChannel("154.755", true) -- first alert, activate
exports['tRadio']:setAlertOnChannel("154.755", true, "SIGNAL 100") -- by name
exports['tRadio']:setAlertOnChannel("154.755", nil, 2) -- toggle second alert
exports['tRadio']:setChannelPanic("154.755", 5, true)User Management
| Export | Parameters | Returns |
|---|---|---|
setUserChannel | serverId: number, frequency: string|number | boolean |
disconnectUser | serverId: number | boolean |
isUserTalking | serverId: number, frequency: string|number | boolean |
setUserChannel handles trunked channel routing automatically.
local ok = exports['tRadio']:setUserChannel(5, "460.125")
exports['tRadio']:disconnectUser(5)User Information
| Export | Parameters | Returns |
|---|---|---|
getPlayerName | serverId | string |
getPlayerNacId | serverId | string or nil |
getUserInfo | serverId | { name, nacId } |
hasRadioAccess | serverId | boolean |
setUserNacId | serverId, nacId: string | boolean — overrides until next refreshNacId |
refreshNacId | serverId | boolean — re-fetches NAC ID from source; call after job changes |
refreshPlayerInfo | — | number — count of players refreshed |
getAcePermissions | serverId | { connect, scan, gps, zones, dispatch } |
local perms = exports['tRadio']:getAcePermissions(5)
-- { connect = true, scan = true, gps = false, zones = true, dispatch = false }Codeplug / Subscriber
| Export | Parameters | Returns |
|---|---|---|
getSubscriberInfo | serverId | subscriber record or nil |
getSubscriberZones | serverId | table[] of zone objects or nil |
refreshSubscriber | serverId | boolean — rebuilds full subscriber record; more thorough than refreshNacId |
getSubscriberInfo returns:
{
codeplugId = "leo",
unitId = "141",
nac = 0x293,
radioModel = "APX8000",
features = { ... },
tones = { ... },
emergency = { ... },
announcementVolume = 0.8,
}Callsigns
Requires useCallsignSystem = true in the admin panel.
| Export | Parameters | Returns |
|---|---|---|
setCallsign | serverId, callsign: string|nil — pass "" or nil to clear | — |
getCallsign | serverId | string or nil |
exports['tRadio']:setCallsign(5, "L-141")
local cs = exports['tRadio']:getCallsign(5) -- "L-141"Audio
| Export | Parameters | Returns |
|---|---|---|
playToneOnChannel | frequency, tone: string | — |
playToneOnSource | serverId, tone: string | — |
exports['tRadio']:playToneOnChannel("154.755", "ALERT")
exports['tRadio']:playToneOnSource(5, "BEEP")Client Exports
All volume exports use 0.0–1.0. The admin panel and config.lua use 0–100 — divide by 100 before passing those values to any volume export.
Radio Control
| Export | Parameters | Returns |
|---|---|---|
openRadio | focus: boolean (default true) | — |
closeRadio | — | — |
connectToFrequency | frequency: string|number | boolean |
getCurrentFrequency | — | frequency string/number, or -1 if not connected |
getCurrentChannel | — | channel object |
addListeningChannel | channel | — |
removeListeningChannel | channel | — |
getListeningChannels | — | string[] — active scan frequencies |
exports['tRadio']:openRadio() -- open with NUI focus
exports['tRadio']:openRadio(false) -- open without focus
local ok = exports['tRadio']:connectToFrequency("154.755")Transmission
| Export | Parameters | Returns |
|---|---|---|
startTransmitting | — | — |
stopTransmitting | — | — |
isTransmitting | — | boolean |
getActiveTalker | — | { serverId, name, frequency } or nil |
setTalking | talking: boolean | — — UI feedback only, does not affect voice |
exports['tRadio']:startTransmitting()
local talker = exports['tRadio']:getActiveTalker()
-- { serverId = 5, name = "John Smith", frequency = "154.755" }Radio State
| Export | Parameters | Returns |
|---|---|---|
isConnected | — | boolean |
isPowerOn | — | boolean |
setPower | state: boolean|nil (omit to toggle) | — |
isRadioOpen | — | boolean |
isRadioFocused | — | boolean |
Volume & Audio
| Export | Parameters | Returns |
|---|---|---|
setVolume | volume: number (0.0–1.0) | — |
getVolume | — | number (0.0–1.0) |
setToneVolume | volume: number (0.0–1.0) | — |
getToneVolume | — | number (0.0–1.0) |
set3DVolume | volume: number (0.0–1.0) | — |
get3DVolume | — | number (0.0–1.0) |
playTone | tone: string | — — plays locally |
NotifySirenChanged | — | — — forces immediate re-poll of bgModeCheck; call after siren state changes in custom ELS |
exports['tRadio']:setVolume(0.75)
exports['tRadio']:playTone("ALERT")
exports['tRadio']:NotifySirenChanged()Appearance & Settings
| Export | Parameters | Returns |
|---|---|---|
setRadioLayout | layout: string | — |
getRadioLayout | — | string |
setRadioTheme | theme: string | — |
getRadioTheme | — | string |
setAnimationId | animId: string | — |
getAnimationId | — | string |
setEarbudsEnabled | enabled: boolean | — |
getEarbudsEnabled | — | boolean |
setGPSEnabled | enabled: boolean | — |
getGPSEnabled | — | boolean |
Alerts & Panic (Client)
| Export | Parameters | Returns |
|---|---|---|
triggerAlertOnChannel | frequency, enabled: boolean | — |
panicButton | frequency, enabled: boolean | — |
User Information (Client)
| Export | Parameters | Returns |
|---|---|---|
getCurrentName | — | string |
getCurrentNacId | — | string |
refreshMyNacId | — | — — re-fetches from server |
getMyCallsign | — | string or nil |
setMyCallsign | callsign: string — pass "" to clear | — |
getZone | — | string — current zone name |
System
| Export | Parameters | Returns |
|---|---|---|
getBatteryLevel | — | number (0–100) |
getSignalStrength | — | signal strength value |
getConnectionDiagnostics | — | diagnostic snapshot table |
Web API
Authentication
All protected endpoints require a per-session authToken obtained at runtime. This is not Config.authToken from config.lua.
Step 1 — create a session:
curl -X POST http://192.0.2.100:7777/radio/dispatch/auth \
-H "Content-Type: application/json" \
-d '{"nacId": "141", "callsign": "Dispatcher-01"}'Response:
{
"success": true,
"sessionId": "dispatch_abc123",
"authToken": "550e8400-e29b-41d4-a716-446655440000"
}nacIdmust matchdispatchNacIdinconfig.lua- Sessions expire after 24 hours
- To re-issue a session after a server restart:
POST /radio/dispatch/reauthwithAuthorizationheader only (nox-session-id)
Step 2 — authenticate all protected requests:
Authorization: Bearer 550e8400-e29b-41d4-a716-446655440000
x-session-id: dispatch_abc123Endpoint Prefix Guide
| Prefix | Purpose |
|---|---|
/api/ | General server integrations — bots, CAD, external monitoring |
/radio/dispatch/ | Full dispatcher presence and real-time state |
/dispatch/ | Unit management — controlling individual in-game players |
Prefer GET /radio/dispatch/status over GET /api/status for all external integrations — it is a superset and actively maintained.
Public Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/health | Server status, version, auth method |
GET | /radio/dispatch/installer | Windows desktop app installer (.exe) |
GET | /radio/dispatch/update.json | Auto-updater manifest |
Protected Endpoints
Status & Config
| Method | Path | Body | Description |
|---|---|---|---|
GET | /radio/dispatch/status | — | Users, channels, panic, alerts, patches (preferred) |
GET | /api/status | — | Subset of /radio/dispatch/status |
GET | /radio/dispatch/config | — | Zones, alerts, volumes, FX settings, installed themes |
GET | /radio/dispatch/tones | — | Available tone list |
Broadcast & Tones
| Method | Path | Body | Description |
|---|---|---|---|
POST | /radio/dispatch/broadcast | { message, frequency?, type?, tone? } | Send broadcast to channel or all |
POST | /api/trigger-broadcast | { message, frequency?, type? } | Broadcast (no tone field) |
POST | /radio/dispatch/tone | { frequency, tone } | Play tone on a channel |
POST | /api/play-tone | { frequency, tone } | Play tone (alias) |
Alerts
| Method | Path | Body | Description |
|---|---|---|---|
POST | /radio/dispatch/alert/trigger | { frequency, alertType, alertConfig } | Activate a named alert |
POST | /radio/dispatch/alert/clear | { frequency } | Clear active alert |
POST | /radio/dispatch/alert/oneshot | { frequency, alertConfig } | Fire alert once without persisting |
Channel & User Control
| Method | Path | Body | Description |
|---|---|---|---|
POST | /radio/dispatch/switchChannel | { serverId, frequency, oldFrequency } | Move in-game player to a channel |
POST | /dispatch/user/alert | { userId, message, frequency } | Send alert message to a player |
POST | /dispatch/user/disconnect | { userId } | Disconnect a player from the radio |
POST | /dispatch/user/set-player-callsign | { userId, callsign } | Set in-game player callsign (requires useCallsignSystem) |
POST | /dispatch/user/update-callsign | { callsign, userId } | Update callsign for a dispatch user (userId must be negative) |
Patches
| Method | Path | Body | Description |
|---|---|---|---|
GET | /api/patches | — | List active patches |
POST | /api/patch/create | { label, frequencies: ["154.755", "460.125"] } | Create a cross-channel patch |
POST | /api/patch/remove | { id } | Remove a patch by ID |
Session
| Method | Path | Headers | Description |
|---|---|---|---|
POST | /radio/dispatch/reauth | Authorization only | Re-issue session after server restart |
SESSION_TOKEN="550e8400-e29b-41d4-a716-446655440000"
SESSION_ID="dispatch_abc123"
# Status
curl http://192.0.2.100:7777/radio/dispatch/status \
-H "Authorization: Bearer $SESSION_TOKEN" \
-H "x-session-id: $SESSION_ID"
# Create patch
curl -X POST http://192.0.2.100:7777/api/patch/create \
-H "Authorization: Bearer $SESSION_TOKEN" \
-H "x-session-id: $SESSION_ID" \
-H "Content-Type: application/json" \
-d '{"label": "TAC1-FIRE", "frequencies": ["154.755", "460.125"]}'
# Play tone
curl -X POST http://192.0.2.100:7777/radio/dispatch/tone \
-H "Authorization: Bearer $SESSION_TOKEN" \
-H "x-session-id: $SESSION_ID" \
-H "Content-Type: application/json" \
-d '{"frequency": "154.755", "tone": "ALERT"}'Socket.IO
Connect to appear as a dispatcher and receive real-time channel events.
const res = await fetch('http://192.0.2.100:7777/radio/dispatch/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nacId: '141', callsign: 'Bot-01' })
}).then(r => r.json());
const socket = io('http://192.0.2.100:7777', {
auth: {
authToken: res.authToken, // per-session UUID — not Config.authToken
sessionId: res.sessionId,
serverId: -(1000 + Math.floor(Math.random() * 10000)) // must be negative
}
});Emit (client → server)
| Event | Payload | Description |
|---|---|---|
setDispatchSession | sessionId: string | Link socket to auth session |
updateUserInfo | { name: string, nacId: string } | Set dispatcher identity |
setSpeakerChannel | frequency: string | Join channel to speak |
addListeningChannel | frequency: string | Start scanning a channel |
removeListeningChannel | frequency: string | Stop scanning a channel |
listenToUser | targetServerId: number | Follow a specific user across any frequency |
stopListeningToUser | targetServerId: number | Stop following a specific user |
setTalking | { state: boolean, frequency?: string, override?: boolean } | PTT. override: true bypasses blockTransmission (audit-logged) |
voice | { data: string, encoding?: string, frequency?: string } | Base64 Opus packet; encoding: "imbe" for P25 |
heartbeat | Date.now() | Keep-alive |
Listen (server → client)
| Event | Payload | Description |
|---|---|---|
voice | { serverId, frequency, data, encoding?, receiveType, patchId? } | Relayed voice packet |
talkingState | { serverId, frequency, state: boolean } | PTT start/stop |
speakerJoined | { serverId, frequency, name?, nacId? } | Speaker joined channel |
speakerLeft | { serverId, frequency, name?, nacId? } | Speaker left channel |
listenerJoined | { serverId, frequency } | Listener joined channel |
listenerLeft | { serverId, frequency } | Listener left channel |
channelState | { frequency, speakers, listeners, activeTalkers, empty } | Full channel state snapshot |
patchStatus | { patchId, active: boolean, label, frequencies } | Patch created or removed |
serverTone | { tone: string, frequency: string } | Tone played on channel |
dispatchNotification | { type: string, ...payload } | Cross-dispatcher notification |
playerInfoUpdate | { serverId, name?, nacId?, unitId? } | Player identity changed |
configUpdate | { ... } | Slim config re-broadcast — no zones/alerts inline |
configRefresh | {} | Re-fetch /radio/dispatch/config |
ptt:denied | { frequency, reason: string, currentTalker? } | PTT denied — channel busy (blockTransmission = true) |
Dispatch Theme JavaScript API
JS themes run directly in the dispatch panel with full DOM access. Dispatchers are shown the complete source code and must explicitly accept a warning before installation.
Theme JavaScript receives three injected globals:
| Global | Purpose |
|---|---|
dispatch | Read state, subscribe to events, call actions |
theme | CSS helpers, DOM control |
onUnmount(fn) | Register cleanup for when the theme changes or unmounts |
Reading State
dispatch.getState() returns a plain-object snapshot of the current store. Does not set up reactive subscriptions.
const s = dispatch.getState();| Field | Type | Description |
|---|---|---|
channel | string | Currently monitored frequency |
scannedChannels | string[] | Additional scanned frequencies |
zones | Zone[] | Full zone → channel → user tree |
pttActive | boolean | Global PTT active |
channelPttActive | boolean | Per-channel PTT active |
channelPttFrequency | string | Frequency for active channel PTT |
voiceStatus | string | MUTED | LISTENING | TRANSMITTING | TX (IMBE) | TX (Opus) |
micReady | boolean | null | Microphone availability |
activeAlerts | Record<string, string> | { [frequency]: alertName } |
alertTypes | AlertType[] | Available alert configs |
tones | { id, name }[] | Available tones for broadcast alerts |
patches | Patch[] | Active frequency patches |
transmissions | TransmissionEntry[] | Last 50 transmission log entries |
announcementGroups | AnnouncementGroup[] | Group definitions |
activeAnnouncementGroup | string | null | Currently armed group ID |
settings | DispatchSettings | PTT bindings, volumes, callsign |
callsign | string | Dispatcher callsign |
serverName | string | Connected server name |
connected | boolean | Socket connection state |
health | string | connected | stale | disconnected |
authenticated | boolean | Auth state |
Zone shape:
{
id: string, name: string,
channels: [{
id: string, frequency: string, name: string,
type: string, // "conventional" | "trunked" | "digital"
canTransmit: boolean,
listeners: number,
users: [{
id: string, name: string,
nacId: string | null, unitId: string | null,
transmitting: boolean, panic: boolean
}]
}]
}DispatchSettings shape:
{
callsign: string,
voiceVolume: number, // 0–1
sfxVolume: number, // 0–1
globalPTT: { pttKey, pttType, pttMouseButton },
channelPTT: { [frequency]: PTTSettings },
announcementGroupPTT: { [groupId]: PTTSettings }
}Events
Subscribe with dispatch.on(event, callback). All events fire immediately on subscribe with the current state — no separate initialisation call needed.
dispatch.on('dispatch:zones', ({ zones }) => render(zones));
// Unsubscribe
const handler = ({ active }) => { /* ... */ };
dispatch.on('dispatch:ptt', handler);
onUnmount(() => dispatch.off('dispatch:ptt', handler));| Event | Payload | Fires when |
|---|---|---|
dispatch:zones | { zones } | Any channel, user, panic, or listener-count change |
dispatch:ptt | { active } | Global PTT pressed or released |
dispatch:channel | { freq } | Main monitored channel changes |
dispatch:channel-ptt | { active, freq } | Per-channel PTT starts or stops |
dispatch:voice-status | { status } | Voice status changes |
dispatch:connection | { connected, health } | Connection state changes |
dispatch:alerts | { alerts: [{id, name}] } | Active channel alerts change |
dispatch:alert-types | { alertTypes } | Alert config reloads |
dispatch:scans | { scanned: string[] } | Scanned channel list changes |
dispatch:patches | { patches } | Patches created or removed |
dispatch:transmissions | { transmissions } | TX log entry added or completed |
dispatch:callsign | { callsign } | Dispatcher callsign changes |
dispatch:announcement-group | { groupId: string | null } | Active announcement group changes |
dispatch:settings | { settings } | Any PTT binding or volume setting changes |
Actions
Channel:
dispatch.joinChannel('460.0625')
dispatch.leaveChannel()
dispatch.addScan('154.755')
dispatch.removeScan('154.755')PTT:
dispatch.startPTT() // global PTT — requires microphone + active channel
dispatch.stopPTT()
dispatch.startChannelPTT('460.0625') // per-channel PTT
dispatch.stopChannelPTT()PTT silently no-ops if the dispatcher has no active channel, the channel has canTransmit: false, or another PTT is already active.
Announcement groups:
dispatch.activateAnnouncementGroup('group-id') // arm
dispatch.activateAnnouncementGroup(null) // disarmAlerts:
dispatch.triggerAlert('460.0625', 'SIGNAL 100')
dispatch.sendBroadcastAlert({
frequency: '460.0625',
type: 'Priority Alert', // "General Alert" | "Information Alert" | "Priority Alert" | "Emergency Alert"
message: 'All units respond to 123 Main St',
tone: 'PRIORITY'
})
dispatch.acknowledgePanic(serverId, unitId) // unitId optionalPatches:
const patchId = await dispatch.createPatch(['460.0625', '154.755'])
await dispatch.removePatch(patchId)Settings & user management:
dispatch.updateSettings({ voiceVolume: 0.8 })
dispatch.updateSettings({ callsign: 'DISP-1' })
dispatch.openSettings()
dispatch.closeSettings()
// All return Promise<boolean>
dispatch.kickUser(serverId)
dispatch.alertUser(serverId, 'Message text', frequency)
dispatch.setUserCallsign(serverId, 'UNIT-5')
dispatch.switchUserChannel(serverId, '462.000', '460.000') // (id, newFreq, oldFreq)DOM Control
All CSS changes through the theme API are automatically reversed when the theme changes or the panel unmounts.
theme.setVar('--accent-raw', '#ff6600') // set a CSS variable on <html>
theme.removeVar('--accent-raw')
// Adds "tj-ptt-active" to <html> (auto-prefixed)
theme.addClass('ptt-active')
theme.removeClass('ptt-active')
theme.hideDefaultUI() // hides Header, ZoneList, and announcement bar
theme.showDefaultUI() // restores them
// Persistent <div> inside .dispatch-app — mount custom HTML here
const root = theme.getContainer()Variable names: --[a-zA-Z][a-zA-Z0-9-]* (max 60 chars). Class names: [a-zA-Z][a-zA-Z0-9-_]* (max 40 chars, without the tj- prefix). hideDefaultUI() does not affect Settings, broadcast, or patch modals — they render in overlay portals.
Security Model
| What | How it's enforced |
|---|---|
| CSS injection | @import and external url() calls are stripped at install time. Relative URLs and data: URIs are permitted. |
| JS execution | Runs directly in the dispatch panel — no iframe sandbox. Full DOM access by design. |
| Install review | Any theme containing jsText shows a mandatory source-review dialog. Cannot be bypassed. |
| JS badge | Marketplace listing cards display a visible JS badge on themes that include JavaScript. |
| Auto-cleanup | CSS vars, classes, canvas contents, and hideDefaultUI state are all reversed automatically on theme change or panel close. |
Full Panel Replacement Example
theme.hideDefaultUI();
const root = theme.getContainer();
root.style.cssText = 'flex:1;overflow:auto;display:flex;flex-direction:column';
function renderChannel(c, activeFreq) {
const panicking = c.users.filter(u => u.panic).length;
const active = c.frequency === activeFreq;
return `
<div data-freq="${c.frequency}" style="
padding:10px 14px; border-bottom:1px solid var(--border); cursor:pointer;
background:${active ? 'var(--my-dispatch-bg)' : 'transparent'};
border-left:3px solid ${active ? 'var(--accent-raw)' : 'transparent'}">
<div style="display:flex;align-items:center;gap:8px">
<span style="font-weight:600;color:var(--text-primary)">${c.name}</span>
<span style="font-size:11px;color:var(--text-secondary)">${c.frequency}</span>
${panicking ? `<span style="color:var(--danger);font-size:11px;font-weight:700">⚠ PANIC</span>` : ''}
${c.users.some(u=>u.transmitting) ? `<span style="color:var(--accent-raw);font-size:11px">● TX</span>` : ''}
</div>
<div style="font-size:11px;color:var(--text-secondary);margin-top:2px">
${c.users.map(u =>
`<span style="color:${u.transmitting ? 'var(--accent-raw)' : u.panic ? 'var(--danger)' : 'var(--text-secondary)'}">${u.name}</span>`
).join(', ') || 'No users'}
</div>
</div>`;
}
function render() {
const s = dispatch.getState();
const channels = s.zones.flatMap(z => z.channels);
root.innerHTML = `
<div style="padding:10px 14px;border-bottom:1px solid var(--border);
display:flex;align-items:center;gap:12px;background:var(--bg-secondary)">
<span style="font-weight:700;color:var(--text-primary)">${s.serverName || 'Dispatch'}</span>
<span style="font-size:11px;color:${s.connected ? 'var(--success)' : 'var(--danger)'}">● ${s.health}</span>
<span style="font-size:11px;color:var(--text-secondary);margin-left:auto">${s.callsign}</span>
</div>
<div style="flex:1;overflow-y:auto">
${channels.map(c => renderChannel(c, s.channel)).join('')}
</div>
<div style="padding:10px 14px;border-top:1px solid var(--border);
display:flex;gap:8px;background:var(--bg-secondary)">
<button id="ptt-btn" style="flex:1;padding:8px;border-radius:6px;border:none;cursor:pointer;
font-weight:700;color:var(--text-primary);
background:${s.pttActive ? 'var(--accent-raw)' : 'var(--bg-hover)'}">
${s.pttActive ? '● TRANSMITTING' : 'PTT'}
</button>
<button onclick="dispatch.openSettings()" style="padding:8px 12px;border-radius:6px;
border:1px solid var(--border);background:transparent;color:var(--text-secondary);cursor:pointer">⚙</button>
</div>`;
root.querySelectorAll('[data-freq]').forEach(el => {
el.onclick = () => dispatch.joinChannel(el.dataset.freq);
});
const pttBtn = root.querySelector('#ptt-btn');
if (pttBtn) {
pttBtn.onmousedown = () => dispatch.startPTT();
pttBtn.onmouseup = () => dispatch.stopPTT();
pttBtn.onmouseleave = () => dispatch.stopPTT();
}
}
dispatch.on('dispatch:zones', render);
dispatch.on('dispatch:channel', render);
dispatch.on('dispatch:ptt', render);
dispatch.on('dispatch:connection', render);
dispatch.on('dispatch:callsign', render);Error Responses
All error responses use Content-Type: application/json.
| Status | Body | Cause |
|---|---|---|
400 | { "error": "..." } | Missing or invalid parameters |
401 | { "error": "Authentication required" } | Missing or invalid auth headers |
404 | { "error": "Channel not found" } | Invalid frequency |
500 | { "error": "..." } | Server error |
Type notes:
- Frequencies accept strings (
"154.755") or numbers (154.755) on all endpoints and exports. - Dispatcher
serverIdvalues must be negative; in-game player IDs are positive. - CORS is enabled for all origins.
- Volume: exports use 0.0–1.0; admin panel and
config.luause 0–100 — divide by 100 when passing config values to exports.
Usage & Customization
In-game controls, admin panel, dispatch panel, alerts, animations, layouts, sounds, and dispatch theming for Tommy's Radio.
Troubleshooting
Symptom-based fixes for connection failures, audio problems, microphone issues, dispatch panel errors, and configuration mistakes in Tommy's Radio.
