Exports And Integration Guide
API reference for oxide-dispatch — server exports, the olink.dispatch namespace, net events, and o-link callbacks.
API reference for oxide-dispatch. Verified against server/main.lua, the o-link adapters at o-link/modules/dispatch/oxide-dispatch/server.lua and o-link/modules/dispatch/oxide-dispatch/client.lua, and the broadcast / responder modules.
Resource Names
Primary resource name (use this for direct exports):
exports['oxide-dispatch']Recommended consumption (framework-agnostic, works across QBCore / QBX / ESX):
local olink = exports['o-link']:olink()
olink.dispatch.CreateAlert({ ... })Server Exports
CreateAlert(data)
Creates a new alert, persists it to dispatch_alerts, and broadcasts it to recipients.
local alert = exports['oxide-dispatch']:CreateAlert({
code = '10-71',
title = 'Shots Fired',
message = 'Multiple shots heard near Legion Square',
priority = 1,
jobs = 'police', -- group, raw job, or array
coords = vector3(233.18, -862.28, 30.36),
icon = 'fas fa-gun',
blipData = { sprite = 161, color = 1, scale = 1.2 },
source_type = 'system', -- 'player' | 'system' | 'manual' | 'auto-detect' | 'panic' | 'civilian' | 'civilian-anonymous'
source_name = 'Auto-detect',
expireMinutes = 15, -- optional override
soundEffect = 'Lose_1st', -- optional per-alert sound
soundSet = 'GTAO_FM_Events_Soundset',
vehicle_plate = nil, -- optional
vehicle_model = nil, -- optional
street = nil, -- optional
is_panic = false, -- internal flag, set by Panic.Trigger
})Anything you do not supply is filled from Config.GetCodeDefaults(code).
Returns: table|nil — the normalized alert table on success, nil on failure. The export does not return an error reason; the underlying failure causes ('invalid_data', 'blacklisted', 'duplicate' from auto-detect dedup, 'insert_failed') are logged internally via olink.logger.
GetActiveAlerts(jobFilter)
Returns all active (active + responded) alerts, optionally filtered by job group, raw job, or array.
local all = exports['oxide-dispatch']:GetActiveAlerts() -- everything active
local pd = exports['oxide-dispatch']:GetActiveAlerts('police') -- police group
local ems = exports['oxide-dispatch']:GetActiveAlerts({ 'ambulance', 'ems' })Returns: table[] sorted by priority (ascending) then id (descending). Empty array when nothing matches.
GetAlert(alertId)
Returns a single alert from the in-memory cache.
local alert = exports['oxide-dispatch']:GetAlert(42)Returns: alert table, or nil if the alert doesn't exist or is no longer active.
RespondToAlert(alertId, src)
Attaches a player to an alert as responding. Resolves the player's char id, name, and job through o-link and writes a row into dispatch_responders.
local ok, err = exports['oxide-dispatch']:RespondToAlert(alertId, source)Returns: boolean ok, string|nil err — possible errors: 'not_loaded', 'not_found', 'insert_failed'. Returns true (no row inserted) when the player is already attached.
StopResponding(alertId, src)
Detaches the player from the alert (sets detached_at, status cleared).
local ok = exports['oxide-dispatch']:StopResponding(alertId, source)Returns: boolean — false if the player wasn't attached or the char id couldn't be resolved.
UpdateResponderStatus(alertId, src, status)
Updates the responder's status without detaching.
exports['oxide-dispatch']:UpdateResponderStatus(alertId, source, 'on_scene')Valid status values: 'responding', 'on_scene', 'cleared'. Setting 'cleared' is equivalent to calling StopResponding.
Returns: boolean.
CloseAlert(alertId, src, reason)
Closes the alert. Sets status to closed, stamps closed_at, closed_by_char_id, and closed_reason, and notifies all recipients to remove the popup.
local ok, err = exports['oxide-dispatch']:CloseAlert(alertId, source, 'resolved')Returns: boolean ok, string|nil err — err is 'not_found' if the alert is unknown.
src may be 0 for server/console-issued closures (the closed_by_char_id will be nil).
SetCallsign(src, callsign)
Persists a callsign to the player's character metadata (callsign key) via olink.character.SetMetadata. Validation: alphanumeric and dashes only, max 16 characters.
local ok, err = exports['oxide-dispatch']:SetCallsign(source, '1A-23')Returns: boolean ok, string|nil err — possible errors: 'invalid_callsign', 'too_long', 'invalid_chars', 'unsupported', 'set_failed'.
SetOfficerStatus(src, statusId)
Sets the responder's per-session status (validated against the responder's group). Broadcasts to colleagues in the same group via olink:client:dispatch:officerStatusChanged.
exports['oxide-dispatch']:SetOfficerStatus(source, 'code4') -- police
exports['oxide-dispatch']:SetOfficerStatus(source, 'enroute') -- ems / fireReturns: boolean ok, string|nil err — possible errors: 'invalid_source', 'not_responder', 'invalid_status'.
GetOfficerStatus(src)
Reads the responder's current per-session status record.
local rec = exports['oxide-dispatch']:GetOfficerStatus(source)
-- rec = { status, callsign, group, jobName, charId, updatedAt }Returns: table|nil — nil when the player has no active responder session.
TriggerPanic(src)
Triggers a panic / responder-needs-backup alert on behalf of the given player. Subject to the standard Config.Panic.cooldownSec cooldown.
local ok, err = exports['oxide-dispatch']:TriggerPanic(source)Returns: boolean ok, string|nil err — possible errors: 'invalid_source', 'not_responder', 'cooldown', 'no_ped', plus any error from Alerts.Create.
o-link Namespace
oxide-dispatch registers itself at olink.dispatch through two adapter files:
o-link/modules/dispatch/oxide-dispatch/server.lua— server sideo-link/modules/dispatch/oxide-dispatch/client.lua— client side
The server wrappers pcall every export call; both sides guard on the resource state and return defaults when oxide-dispatch is not started, so consumers can call them during the boot window without errors.
Server side (olink.dispatch)
Mirrors the server exports plus a name lookup. This is the recommended integration surface for downstream resources running server-side (MDTs, AI systems, automation).
local olink = exports['o-link']:olink()
olink.dispatch.GetResourceName() -- 'oxide-dispatch'
olink.dispatch.CreateAlert(data) -- table|nil
olink.dispatch.GetActiveAlerts(jobFilter) -- table[]
olink.dispatch.GetAlert(alertId) -- table|nil
olink.dispatch.RespondToAlert(alertId, src) -- boolean, string|nil
olink.dispatch.StopResponding(alertId, src) -- boolean
olink.dispatch.UpdateResponderStatus(alertId, src, status) -- boolean
olink.dispatch.CloseAlert(alertId, src, reason) -- boolean, string|nil
olink.dispatch.SetCallsign(src, callsign) -- boolean, string|nil
olink.dispatch.SetOfficerStatus(src, statusId) -- boolean, string|nil
olink.dispatch.GetOfficerStatus(src) -- table|nil
olink.dispatch.TriggerPanic(src) -- boolean, string|nilClient side (olink.dispatch)
The client adapter does not expose CreateAlert (alert authoring is server-authoritative). Instead, clients submit alerts through SendAlert, which forwards the payload over oxide:dispatch:sendAlert for the server to enrich and broadcast. All read/responder operations on the client route through the rate-limited callbacks under the hood — src is the calling player and never passed by the caller.
local olink = exports['o-link']:olink()
olink.dispatch.GetResourceName() -- 'oxide-dispatch'
olink.dispatch.SendAlert(data) -- void; player-issued alert
olink.dispatch.GetActiveAlerts(jobFilter) -- table[]
olink.dispatch.GetAlert(alertId) -- table|nil
olink.dispatch.RespondToAlert(alertId) -- boolean, string|nil
olink.dispatch.StopResponding(alertId) -- boolean
olink.dispatch.UpdateResponderStatus(alertId, status) -- boolean
olink.dispatch.CloseAlert(alertId, reason) -- boolean, string|nil
olink.dispatch.SetCallsign(callsign) -- boolean, string|nil
olink.dispatch.SetOfficerStatus(statusId) -- boolean, string|nil
olink.dispatch.TriggerPanic() -- boolean, string|nilSendAlert accepts the same producer fields as CreateAlert (code, title, message, priority, icon, jobs / job, coords, vehicle_model / vehicle, vehicle_plate / plate, blipData, expireMinutes, source_type). If coords is omitted it defaults to the calling ped — but the server still rewrites coords from the player's ped, so trusted positioning is automatic.
Capability check
if olink.supports('dispatch.CreateAlert') then -- server-only
olink.dispatch.CreateAlert({ ... })
end
if olink.supports('dispatch.SendAlert') then -- client-only
olink.dispatch.SendAlert({ ... })
endNet Events
Server-bound
oxide:dispatch:sendAlert
Player-to-server relay used by the client-side olink.dispatch.SendAlert helper. Server-authoritative — the server overwrites the coords with the calling ped's coords, enriches source_char_id / source_name, and rate-limits per source by Config.Broadcast.rateLimitMs.
TriggerServerEvent('oxide:dispatch:sendAlert', {
code = '10-71',
message = 'Player-reported gunfire',
jobs = 'police',
})oxide-dispatch:server:civilianRadialFire
Fast-path radial event used by the Dispatch → Call 911 / Call 311 submenu. Carries { channel = '911'|'311', category = <id> } and skips the chat-command form.
oxide-dispatch:server:radialSetStatus
Fired by the Set Status items in the Dispatch radial menu. Payload: { statusId = '<id>' }.
oxide-dispatch:server:radialSetCallsign
Fired by the radial Set Callsign flow after the input dialog completes. Payload: the callsign string.
oxide-dispatch:server:radialPanic
Fired by the radial Panic / Backup item. No payload.
Client-bound (broadcast from server)
| Event | Payload | Fired when |
|---|---|---|
olink:client:dispatch:alertCreated | alert (table) | A new alert is broadcast to this responder |
olink:client:dispatch:alertUpdated | alert (table) | Responder list / replies change |
olink:client:dispatch:alertClosed | alertId (integer), reason ('closed' | 'expired') | Alert closed by user or auto-expired |
olink:client:dispatch:officerStatusChanged | payload (table) | A colleague in the same responder group changed their status |
olink:client:dispatch:civilianReply | alertId, callsign, message, groupLabel | A responder replied to your 911/311 call |
RegisterNetEvent('olink:client:dispatch:alertCreated', function(alert)
-- alert.id, alert.code, alert.title, alert.coords, alert.responders, alert.is_panic, alert.replies, ...
end)
RegisterNetEvent('olink:client:dispatch:alertClosed', function(alertId, reason)
-- reason: 'closed' or 'expired'
end)
RegisterNetEvent('olink:client:dispatch:officerStatusChanged', function(payload)
-- payload = { src, callsign, status, group, jobName, charId, updatedAt }
end)
RegisterNetEvent('olink:client:dispatch:civilianReply', function(alertId, callsign, message, groupLabel)
-- Surfaces in the citizen's notification toast
end)o-link Callbacks
All cross-resource read/write operations also have rate-limited callbacks (registered through olink.callback). Use these from the client when the o-link namespace isn't available (e.g. an MDT panel making a direct request).
| Callback | Args | Rate limit | Returns |
|---|---|---|---|
oxide-dispatch:server:getActiveAlerts | jobFilter? | 1000ms | alert[] (empty if rate-limited) |
oxide-dispatch:server:getActiveAlertsForOfficer | — | 1000ms | { jobName, charId, alerts } (used by the Hub on open) |
oxide-dispatch:server:getDispatchHistory | { window? } | 5000ms | { window, points } (heatmap data) |
oxide-dispatch:server:getAlert | alertId | 500ms | alert|nil |
oxide-dispatch:server:respond | alertId | 1500ms | boolean, string|nil ('rate_limited', 'not_loaded', 'not_found', 'insert_failed') |
oxide-dispatch:server:stopResponding | alertId | 1500ms | boolean |
oxide-dispatch:server:setResponderStatus | alertId, status | 1500ms | boolean |
oxide-dispatch:server:closeAlert | alertId, reason | 3000ms | boolean, string|nil |
oxide-dispatch:server:civilianCall | { channel, category?, message?, anonymous? } | matches Config.Broadcast.rateLimitMs | alertId|nil, string|nil |
oxide-dispatch:server:civilianReply | alertId, message | 1500ms | boolean, string|nil |
oxide-dispatch:server:setOfficerStatus | statusId | 1500ms | boolean, string|nil |
oxide-dispatch:server:setCallsign | callsign | 2000ms | boolean, string|nil |
oxide-dispatch:server:panic | — | 5000ms | boolean, string|nil |
oxide-dispatch:server:getOfficerStatus | — | none | table|nil |
local olink = exports['o-link']:olink()
local Callback = olink.callback
local active = Callback.Trigger('oxide-dispatch:server:getActiveAlerts', 'police')
local ok, err = Callback.Trigger('oxide-dispatch:server:respond', alertId)State Bags
| Key | Scope | Source | Description |
|---|---|---|---|
oxide-dispatch:isLEO | per-player | server | true when the player's job is an enabled department (WHERE is_enabled = 1) in oxide-police's police_departments table. Falls back to the static Config.ResponderGroups.police.jobs seed when oxide-police is missing or the DB query fails. Published by the optional PoliceLookup integration adapter for downstream consumers like oxide-police's MDT. |
oxide-dispatch:responderGroup | per-player | server | The player's responder group key ('police' / 'ems' / 'fire' / custom) or nil if their job isn't in any configured group. Generalized alternative to isLEO — prefer this for group-aware logic in third-party resources. |
Integration Examples
Custom MDT (client)
local olink = exports['o-link']:olink()
-- Live alert feed
RegisterNetEvent('olink:client:dispatch:alertCreated', function(alert)
MDT.AddRow(alert)
end)
RegisterNetEvent('olink:client:dispatch:alertUpdated', function(alert)
MDT.UpdateRow(alert)
end)
RegisterNetEvent('olink:client:dispatch:alertClosed', function(alertId)
MDT.RemoveRow(alertId)
end)
-- Initial backlog when the MDT opens (uses the client-side namespace)
local initial = olink.dispatch.GetActiveAlerts('police')
for _, alert in ipairs(initial) do MDT.AddRow(alert) end
-- User actions
function MDT.OnRespond(alertId)
olink.dispatch.RespondToAlert(alertId)
end
function MDT.OnArrive(alertId)
olink.dispatch.UpdateResponderStatus(alertId, 'on_scene')
end
function MDT.OnClose(alertId, reason)
olink.dispatch.CloseAlert(alertId, reason)
endAuto-create alert from a server-side script
local olink = exports['o-link']:olink()
RegisterNetEvent('myresource:server:bankBeingRobbed', function(coords, robberSource)
olink.dispatch.CreateAlert({
code = '10-90',
title = 'Silent Alarm',
message = 'Pacific Standard Bank vault breach',
jobs = 'police',
coords = coords,
source_type = 'system',
source_name = 'Bank Alarm',
})
end)Listen for status changes (e.g. a CAD dashboard)
RegisterNetEvent('olink:client:dispatch:officerStatusChanged', function(payload)
CAD.UpdateUnit({
id = payload.src,
callsign = payload.callsign,
status = payload.status,
group = payload.group,
job = payload.jobName,
})
end)