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: booleanfalse 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 errerr 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 / fire

Returns: 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|nilnil 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.

oxide-dispatch registers itself at olink.dispatch through two adapter files:

  • o-link/modules/dispatch/oxide-dispatch/server.lua — server side
  • o-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|nil

Client 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|nil

SendAlert 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({ ... })
end

Net 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)

EventPayloadFired when
olink:client:dispatch:alertCreatedalert (table)A new alert is broadcast to this responder
olink:client:dispatch:alertUpdatedalert (table)Responder list / replies change
olink:client:dispatch:alertClosedalertId (integer), reason ('closed' | 'expired')Alert closed by user or auto-expired
olink:client:dispatch:officerStatusChangedpayload (table)A colleague in the same responder group changed their status
olink:client:dispatch:civilianReplyalertId, callsign, message, groupLabelA 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)

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).

CallbackArgsRate limitReturns
oxide-dispatch:server:getActiveAlertsjobFilter?1000msalert[] (empty if rate-limited)
oxide-dispatch:server:getActiveAlertsForOfficer1000ms{ jobName, charId, alerts } (used by the Hub on open)
oxide-dispatch:server:getDispatchHistory{ window? }5000ms{ window, points } (heatmap data)
oxide-dispatch:server:getAlertalertId500msalert|nil
oxide-dispatch:server:respondalertId1500msboolean, string|nil ('rate_limited', 'not_loaded', 'not_found', 'insert_failed')
oxide-dispatch:server:stopRespondingalertId1500msboolean
oxide-dispatch:server:setResponderStatusalertId, status1500msboolean
oxide-dispatch:server:closeAlertalertId, reason3000msboolean, string|nil
oxide-dispatch:server:civilianCall{ channel, category?, message?, anonymous? }matches Config.Broadcast.rateLimitMsalertId|nil, string|nil
oxide-dispatch:server:civilianReplyalertId, message1500msboolean, string|nil
oxide-dispatch:server:setOfficerStatusstatusId1500msboolean, string|nil
oxide-dispatch:server:setCallsigncallsign2000msboolean, string|nil
oxide-dispatch:server:panic5000msboolean, string|nil
oxide-dispatch:server:getOfficerStatusnonetable|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

KeyScopeSourceDescription
oxide-dispatch:isLEOper-playerservertrue 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:responderGroupper-playerserverThe 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)
end

Auto-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)

Next Steps