Koja Scripts

API overview

Conventions, acting identity, permissions, error codes and return shapes shared by every koja-mdt export, hook and event.

API overview

koja-mdt exposes four integration surfaces:

SurfaceSideUse it to
Server exportsserverRead and write MDT records from your own resources (dispatch, records, warrants, citations, custody, EMS, court…).
Client exportsclientOpen the tablet, read the local officer state, send 911 calls, drive the bodycam.
HooksserverBlock, rewrite or react to MDT actions, whether they come from the tablet, the web portal or an export.
EventsbothListen to MDT activity with plain AddEventHandler.

The integrations page explains the editable/ bridges for jails, fines, impounds and frameworks, and the examples page has complete recipes.

Getting started

  1. Start order

    Start your resource after koja-mdt (and its dependencies oxmysql and koja-lib).
    server.cfg
    ensure oxmysql
    ensure koja-lib
    ensure koja-mdt
    ensure my-robberies
  2. Call an export

    server.lua
    local mdt = exports['koja-mdt']
    local medics = mdt:CountOnDuty({ type = 'ems' })
  3. Handle errors

    Most write exports return result, errorCode. Always check the first value.
    server.lua
    local warrant, err = exports['koja-mdt']:CreateWarrant({ type = 'arrest', name = 'John Doe', reason = 'Armed robbery' })
    if not warrant then
        print('warrant failed: ' .. tostring(err))
    end

Conventions

TopicRule
NamingExports are PascalCase (GetCitizen, CreateWarrant). The only exceptions are the hook helpers registerHook and removeHook.
InputDomain exports take one input table (CreateIncident(data)) or a primary key plus an optional change table (UpdateBolo(id, changes, opts)).
Outputresult on success. nil, errorCode (or false, errorCode) on failure. Lookups return nil or an empty table instead of an error.
ValidationEvery input is type-checked and clamped with the same Guard helpers the tablet uses (lengths, enums, ranges). Bad input never throws.
SideServer exports are callable from server scripts only. Clients can never reach them.
YieldingExports that touch the database or the network yield. Call them from a thread, an event handler or a callback, never from the main chunk of a file.
CopiesTables cross the resource boundary as copies. Changing a returned table does not change the MDT.

Sources and identifiers

TermMeaning
src / server idThe player's server id (a number). Exports that need a live player accept it.
identifierThe framework character id: citizenid on QBCore and Qbox, identifier on ESX. Records (citizens, warrants, citations, jail records) are keyed by it, so it works for offline players.
whoParameters named who accept either a server id (number) or an identifier (string). A number is always treated as a server id.
plateTrimmed and upper-cased by the MDT. ' abc 123 ' and 'ABC 123' refer to the same vehicle.
departmentA department id from shared/departments.lua (lspd, bcso, sahp, doj, ems, lsfd, …).

Acting identity

Write exports run through the same internal action handlers as the tablet (Api.Invoke), so permissions, audit logs, hooks, Discord webhooks and live sync behave exactly as if an officer did it. Who the action runs as is controlled by an options table, passed either inside the input table (CreateIncident(data)) or as the last argument (opts):

FieldTypeEffect
officernumberRun as this on-duty or off-duty MDT member (server id). Their name goes on the record and their permissions are enforced. If the player is not an MDT member the export returns nil, 'not_authorized'.
departmentstringWhen no officer is given: the department that owns the record.
authorstringWhen no officer is given: the display name used on the record and in logs (default System).

Without officer, the action runs as System: a virtual session with every permission and the command rank. Server resources are trusted, so System bypasses the permission checks but never the input validation, the hooks or the state rules (for example you still cannot pay a void citation).

Permissions

Permission names are listed in shared/permissions.lua and returned by GetPermissionList(). Ranks map to permission sets per department.

server.lua
if exports['koja-mdt']:HasPermission(source, 'warrants.approve') then
    -- the player can approve warrants in the MDT
end
ExportReturns
HasPermission(src, perm)boolean
GetPermissions(src)sorted string[] of the player's permissions
GetPermissionList()every permission name the MDT knows

Error codes

Errors are short machine-readable strings. The ones you will meet most often:

CodeMeaning
invalidMissing or malformed input.
not_foundThe record, call or player does not exist.
not_authorizedofficer was given but the player is not an MDT member.
no_permissionThe acting officer lacks the permission, or the record belongs to another department.
cancelledA before hook returned false without a reason. Hooks can also return their own code.
disabledThe feature is turned off in shared/config.lua.
game_only / not_in_gameThe action needs a live player; pass officer.
target_offline / target_farThe citizen must be online / nearby (processing, billing, impound fees).
already_processedThe suspect, sentence or payment was already applied.
not_pending / not_active / not_payableThe record is in the wrong state for the action.
fine_failed / bill_failedTaking money from the player failed (not enough money).
unknown_actionInvoke was called with an action name that does not exist.
failedUnexpected internal error. It is printed to the server console.

Domain-specific codes (plate_required, no_violations, bed_taken, already_impounded, bill_too_high, cooldown, …) are listed next to each export.

Common shapes

These shapes are returned by many exports. Fields can be nil when the value is unknown.

Officer

{
    source = 4, identifier = 'ABC12345', name = 'John Doe', callsign = '1-ADAM-12', badge = '1042',
    department = 'lspd', rank = 'officer', gradeLabel = 'Officer II',
    status = 'available', onDuty = true, call = 12, image = 'https://...',
}

Dispatch call

{
    id = 12, code = '10-90', title = 'Bank alarm', message = 'Fleeca Great Ocean', street = 'Great Ocean Hwy',
    coords = { x = 0.0, y = 0.0, z = 0.0 }, priority = 1, status = 'active',
    caller = { name = 'Alarm company', phone = nil },
    units = { { source = 4, callsign = '1-ADAM-12', name = 'John Doe', department = 'lspd' } },
    notes = { { by = '1-ADAM-12 John Doe', text = 'Suspects left north', at = 1730000000000 } },
    blip = { sprite = 161, color = 1, scale = 1.2 }, createdAt = 1730000000000,
}

Timestamps

Database timestamps come back the way oxmysql returns them (usually a number of milliseconds). Live dispatch data (createdAt, note at) is always milliseconds since epoch.

Generic action bridge

Every tablet action can also be called directly. Use the named exports when one exists; Invoke is the escape hatch for anything else.

server.lua
local result, err = exports['koja-mdt']:Invoke('divisions:list', {}, { department = 'lspd' })

See Invoke and GetApiActions in the server exports for details.