Integrations
How another resource on your server hears transmissions, transmits as a named station, answers 911 calls, manages patches and acts in your CAD through tRadio, and what you have to allow first.
tRadio has two ways in for your own code. The web API (REST and Socket.IO) is for programs that act like a radio or a dispatch panel, and is described in the API Reference. The integration hooks on this page are for another resource on the same server: a logger, a pager, an announcer, a bot that talks on a channel, anything that needs to hear finished transmissions or speak as a console without carrying a voice codec of its own.
Everything here is a Lua export or a local server event. Nothing is allowed until you say so.
Allow a resource
Name the resource in config/config.lua:
Config.integrations = { 'my-resource' }Restart tRadio after changing it. The default is an empty list, so no resource can hear, transmit, answer calls, write patches or act in your CAD. The names are resource folder names, case-sensitive.
When a resource that is not listed calls one of the hooks, the call fails and tRadio prints one line to the console, naming the resource and the exact line to add:
[audit] integration:refused hook=openStation resource=my-resource — to allow it, add 'my-resource' to Config.integrations in tRadio's config/config.lua (Config.integrations = { 'my-resource' }) and restart tRadioThe same resource and hook is reported once every ten minutes, so a resource that retries does not fill the log.
What the list covers:
| Hook | Needs the resource to be listed |
|---|---|
registerTransmissionTap | Yes |
openStation, stationTransmit | Yes |
answer911, end911 | Yes |
createPatch, removePatch | Yes |
cadOperation | Yes, and Let integrations write to your CAD switched on |
cadRequest (the CAD's own record) | Yes, and the same switch |
unregisterTransmissionTap, closeStation | No: they only act on what the caller opened |
getStations, getPatches, getAlerts, getToneKeys, cadCapabilities | No |
Let integrations write to your CAD is Settings → Advanced → CAD / MDT in the admin panel. With it off, cadOperation and cadRequest refuse every operation (reads too), and cadCapabilities reports an empty list, so a resource knows to stop offering CAD features.
How the exports behave
- JSON in, JSON out. The new exports take and return JSON strings. A failure is
"",falseor{ "ok": false, "reason": "…" }, never an error, even if the body throws. - A resource can only use what it opened. A tap or station belongs to the resource that created it. Another resource handing over its id is refused, so an id is not a key.
- Ids are 128-bit random tokens. Match them. An event carrying an id you were not given came from someone else.
- Results are events and arrive after the export returns, so you can register for the result once you have its id.
- Everything a resource opened goes when it stops. Taps unregister and stations close.
- Probes are free.
unregisterTransmissionTap,closeStation,end911andremovePatchwith an id that does not exist returnfalse, change nothing and print nothing. That is how a resource checks whether a tRadio is new enough, since calling a missing export raisesNo such export.
Hearing transmissions
registerTransmissionTap(optsJson) → tapId
opts = { "frequencies": [154.755, 460.125] }. Returns a tap id, or "" when refused. A resource holds one tap: registering again replaces it.
- With a list, the tap hears exactly those frequencies, secure channels included: naming the resource and the frequency is the consent.
- With no list, the tap hears every configured conventional channel except secure ones, so "everything" never means a channel you encrypted.
- A resource that has a station answering a 911 call also hears that call's frequency, whatever its list says: a call's frequency is made up when the call is placed and is in no configuration.
- Dispatch consoles are never heard. Neither the dispatch panel's nor a station's transmissions are delivered.
unregisterTransmissionTap(tapId) → bool
event tradio:tap:transmission(tapId, payload)
Fired once per finished transmission, when the talker lets go of the key (or about a second and a half after their last packet if the release never arrived):
{
"serverId": 7,
"name": "King 41",
"frequency": 154.755,
"startedAt": 1730000000000,
"endedAt": 1730000002100,
"codec": "imbe",
"container": "wav",
"sampleRate": 16000,
"audio": "<base64>"
}container: "wav"is a digital transmission, decoded by tRadio to mono 16-bit PCM at 16000 Hz. The rate is part of the contract.container: "webm"is an analog transmission as one Opus WebM file, still encoded (sampleRateis48000).nameis the callsign when callsigns are on, otherwise the player's name.- A transmission is cut at about 20 seconds of digital audio or 60 seconds of analog: the start is delivered and the rest dropped.
local tapId = exports['tRadio']:registerTransmissionTap(json.encode({ frequencies = { 154.755 } }))
AddEventHandler('tradio:tap:transmission', function(id, payload)
if id ~= tapId then return end
-- payload.audio is base64 of a 16 kHz mono WAV
end)Transmitting as a station
A station is a named virtual dispatch console. It appears in every unit's list under its name, covers the frequencies you give it, and may transmit on them. Its ids come from a reserved block, -890000 to -890999, so you can tell a console from a person.
openStation(optsJson) → stationId
opts = { "name": "Dispatch", "frequencies": [154.755], "answers911": false }. Returns "" when refused.
- A name is cut to 24 characters and stripped of control characters.
- A station covers up to 64 frequencies, and a resource may hold eight stations.
- Every covered frequency and every answered call uses one console id from the block of 1000. When the block is full,
openStationreturns""andanswer911answersno_console, and the console says so; close a station or cover fewer frequencies. answers911 = truemakes the station count as a dispatcher for the no-dispatcher 911 fallback, so tRadio does not announce that nobody is picking up a call the station is about to answer. It never counts as a dispatcher anywhere else: a duplex channel and a saved patch that opens when no dispatcher is on behave as if the station were not there.
closeStation(stationId) → bool
stationTransmit(stationId, optsJson) → requestId
{ "frequency": 154.755, "container": "pcm16", "sampleRate": 8000, "audio": "<base64>", "maxMs": 20000 }pcm16 is little-endian mono 16-bit samples, from 4000 to 48000 Hz. Anything other than 8000 Hz is resampled; 8000 Hz is what every digital channel runs at, so it needs no conversion. tRadio does the rest: it encodes with the channel's own codec (an analog channel still gets a vocoded station), pads the encoder's tail so the last word is not left inside it, sends the packets in real time, and shows the talking indicator and the TX log entry like any transmission.
Returns "" for a request it will not take at all: a container other than pcm16, a rate outside the range, an empty or oversized audio field. Otherwise it returns a request id and answers with an event.
- Twenty seconds at most.
maxMscan lower that and never raise it. - One voice at a time on a channel. A request waits its turn. A channel holds one airing and two waiting, and the next is refused.
- There is no cancel. Audio handed over airs to its end; closing the station stops it.
event tradio:station:done(stationId, requestId, result)
{ "ok": true, "reason": "aired", "airedMs": 1000 }reason | Meaning |
|---|---|
aired | It went out. airedMs is how long it ran. |
queue_full | The channel already had one airing and two waiting. |
too_long | More than 20 seconds, or more than maxMs. |
no_station | The station is unknown, closed, or belongs to another resource. |
not_covered | The station does not cover that frequency. |
encode_failed | The codec could not be loaded or the audio could not be encoded. |
getStations() → json
{
"stations": [{ "stationId": "…", "name": "Dispatch", "resource": "my-resource", "frequencies": [154.755], "answers911": true }],
"humans": [{ "serverId": -1001, "selected": 154.755, "listening": [460.125] }]
}humans is every person signed in to the dispatch panel, with the channel they have selected (or null) and every channel they are listening to; a station is never in it. stationId is included only for the caller's own stations, because the id is what lets a resource transmit as one. Anything that should stand down for a human dispatcher reads this.
911 calls
event tradio:911:queued(call)
Fired for every call that enters the queue, whether or not anything is listening:
{ "id": "call911_ab12cd34", "callerServerId": 9, "callerName": "Alex Vance", "message": "break in",
"frequency": -900001, "coords": { "x": 1.0, "y": 2.0, "z": 3.0 } }coords is null when unknown. frequency is the call's own line.
answer911(stationId, callId) → json
{ "ok": true } or { "ok": false, "reason": "…" }. tRadio puts the station on the call's frequency first and accepts the call second: a call whose line has nobody on it is swept as abandoned, so the other order loses the call. The station gives the line back when the call ends.
reason | Meaning |
|---|---|
caller_offline | The caller has no connection to tRadio yet. The call stays queued; try again in a moment. A caller who has not powered a radio on needs a few seconds to connect. |
no_station | The station is unknown or belongs to another resource. |
unknown_call | No such call. |
already_handled | Someone else already answered it. |
no_console | Every station console id is in use. Close a station or cover fewer frequencies. |
not_permitted | The resource is not in Config.integrations. |
end911(callId) → bool
Ends the call with the reason integration_ended and fires the usual tradio:911:ended. false for an unknown call.
event tradio:911:ended(callerServerId, reason)
Already existed. reason is one of dispatcher_ended, caller_ended, caller_offline, caller_no_answer, hangup, queue_timeout, integration_ended or system_disabled. Treat an unknown reason as "the call ended".
Alerts, tones and patches
The exports that raise an alert or play a tone (setAlertOnChannel, playToneOnChannel, getChannelAlert) are in the API Reference. These read the names your server defines:
getAlerts() → json
[{ "name": "SIGNAL 100", "toneOnly": false }], from Config.alerts. A toneOnly alert is a sound, not a state a channel is put in and lifted from.
getToneKeys() → json
["ALERT_A", "PRIORITY"]: the tone names this server has, sorted.
getPatches() → json
[{ "id": "patch_1a2b3c4d5e6f", "label": "FIRE-PD BRIDGE", "frequencies": [154.755, 460.125] }]
createPatch(label, frequenciesJson) → json
Success is { "ok": true, "id": "patch_…" }. A label is cut to 40 characters and defaults to the frequencies. A patch made this way is saved and shown on the dispatch panel like one made there. A refusal carries reason and, where it applies, the frequency that caused it:
reason | Meaning |
|---|---|
too_few | Fewer than two different frequencies. |
unknown_channel | A frequency that is not a configured channel. |
direct | A direct channel, which cannot be patched. |
trunked | A trunked channel, which cannot be patched. |
already_patched | The frequency is in a patch already. label names it. |
disabled | Channel Patching is switched off in Settings → Audio & Radio. |
not_permitted | The resource is not in Config.integrations. |
removePatch(id) → bool
CAD
The six CAD integrations stay inside tRadio. They know each system's exports, identities and status names, and they reduce every answer to a few fields, so the record a CAD sends never crosses.
cadCapabilities(provider) → json
provider is a CAD id (lacore, sonoran, imperial, wasabi, tkmdt, psmdt), or "" for the one tRadio is using. Returns:
{ "provider": "sonoran", "supports": ["plate", "createCall", "attachUnit"], "consent": true }provider is "" when that CAD is not running. consent is the Let integrations write to your CAD setting. supports is empty unless the caller is listed in Config.integrations and consent is on. The operation names are plate, bolo, person, warrants, createCall, attachUnit, detachUnit, closeCall, addNote, readStatus, writeStatus and roster. Which a CAD supports differs a great deal; see CAD / MDT.
cadOperation(provider, op, paramsJson, requestId) → bool
Starts one operation and answers with an event. true when started, false when refused: the resource is not listed, consent is off, the operation name is unknown, or the request id is empty or longer than 128 characters. params takes serverId, plate, name, about, ref, closeRef, note and status; other fields are ignored and text is cut to 200 characters. For writeStatus, status is one of the CAD's own status names, which tRadio matches against that CAD's list. It does not interpret ten-codes or phrases. A request missing something it needs answers error without reaching the CAD.
event tradio:cad:operationResult(requestId, result)
{ "kind": "ok", "ok": true, "body": { "ref": "4411" } }kind is ok, transient (try later), quota, absent, error, threw, off or unsupported. body is the reduced record: a plate (plate, owner, stolen, insured, registration, flags), a person (name, matches, flags), a call (ref, closeRef), { "warrants": bool }, a unit (serverId, callsign, status, service) or { "units": [...] }. Exactly one event per request id.
Reading a CAD's own record
cadDetect, cadStatusVocabulary, cadReadUnit, cadRoster and cadLiveCall read what is running and the status names a CAD accepts. cadRequest(provider, op, payload, requestId) runs an operation and answers on tradio:cad:result with the CAD's own record, which is the only thing a caller outside tRadio has to parse itself. Because it is public, it needs the same two things as cadOperation: the resource listed, and consent on.
The web API
The REST and Socket.IO surface is documented in the API Reference and is the other way in. It suits a program outside the game server, or one that must act as a radio or dispatcher session: tuning, keying up, dispatcher actions, tones, patches and 911 from a panel. Voice on it is codec frames (IMBE for digital channels, Opus for analog), so a program that wants to hear or speak needs a matching codec. The hooks above exist so a resource on the server does not have to carry one.
Limits
| Limit | Value |
|---|---|
| Taps per resource | 1 |
| Stations per resource | 8 |
| Frequencies per station | 64 |
| Station name | 24 characters |
| One transmission from a station | 20 seconds |
| Queue on one channel | 1 airing, 2 waiting |
| Sample rate accepted | 4000 to 48000 Hz |
| Patch label | 40 characters |
| CAD text field | 200 characters |
Third-party integrations
Separate projects that use the hooks on this page. They are not part of tRadio, are installed and updated on their own, and do nothing until you list them in Config.integrations.
| Project | What it is | Source |
|---|---|---|
| tRadioAI | A free, open-source resource that answers 911 calls and speaks on your channels as a dispatcher console | github.com/tommy141x/tRadioAI |
To use one, install it as its own resource, start it after tRadio, and add its folder name to the list:
Config.integrations = { 'tRadioAI' }Restart tRadio after the change. Listing a resource trusts it with everything in Allow a resource: it can hear the transmissions it taps, transmit as a station, answer 911 calls and edit patches. Remove the name and restart tRadio to take all of that away. Questions about a project go to its own repository, not tRadio support.
Next steps
- The REST and Socket.IO surface for programs outside the server: API Reference
- The CAD switches and the
Config.cadpin: Configuration - A hook refused, or events not arriving: Troubleshooting
Tommy's Radio
Still stuck? Support can answer it directly.
Was this page helpful?
TIMMYG Studios