API overview
koja-mdt exposes four integration surfaces:
| Surface | Side | Use it to |
|---|---|---|
| Server exports | server | Read and write MDT records from your own resources (dispatch, records, warrants, citations, custody, EMS, court…). |
| Client exports | client | Open the tablet, read the local officer state, send 911 calls, drive the bodycam. |
| Hooks | server | Block, rewrite or react to MDT actions, whether they come from the tablet, the web portal or an export. |
| Events | both | Listen 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
-
Start order
Start your resource afterkoja-mdt(and its dependenciesoxmysqlandkoja-lib).server.cfgensure oxmysql ensure koja-lib ensure koja-mdt ensure my-robberies -
Call an export
server.lualocal mdt = exports['koja-mdt'] local medics = mdt:CountOnDuty({ type = 'ems' }) -
Handle errors
Most write exports returnresult, errorCode. Always check the first value.server.lualocal 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
| Topic | Rule |
|---|---|
| Naming | Exports are PascalCase (GetCitizen, CreateWarrant). The only exceptions are the hook helpers registerHook and removeHook. |
| Input | Domain exports take one input table (CreateIncident(data)) or a primary key plus an optional change table (UpdateBolo(id, changes, opts)). |
| Output | result on success. nil, errorCode (or false, errorCode) on failure. Lookups return nil or an empty table instead of an error. |
| Validation | Every input is type-checked and clamped with the same Guard helpers the tablet uses (lengths, enums, ranges). Bad input never throws. |
| Side | Server exports are callable from server scripts only. Clients can never reach them. |
| Yielding | Exports 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. |
| Copies | Tables cross the resource boundary as copies. Changing a returned table does not change the MDT. |
Sources and identifiers
| Term | Meaning |
|---|---|
src / server id | The player's server id (a number). Exports that need a live player accept it. |
identifier | The 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. |
who | Parameters named who accept either a server id (number) or an identifier (string). A number is always treated as a server id. |
plate | Trimmed and upper-cased by the MDT. ' abc 123 ' and 'ABC 123' refer to the same vehicle. |
department | A 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):
| Field | Type | Effect |
|---|---|---|
officer | number | Run 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'. |
department | string | When no officer is given: the department that owns the record. |
author | string | When 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.
if exports['koja-mdt']:HasPermission(source, 'warrants.approve') then
-- the player can approve warrants in the MDT
end| Export | Returns |
|---|---|
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:
| Code | Meaning |
|---|---|
invalid | Missing or malformed input. |
not_found | The record, call or player does not exist. |
not_authorized | officer was given but the player is not an MDT member. |
no_permission | The acting officer lacks the permission, or the record belongs to another department. |
cancelled | A before hook returned false without a reason. Hooks can also return their own code. |
disabled | The feature is turned off in shared/config.lua. |
game_only / not_in_game | The action needs a live player; pass officer. |
target_offline / target_far | The citizen must be online / nearby (processing, billing, impound fees). |
already_processed | The suspect, sentence or payment was already applied. |
not_pending / not_active / not_payable | The record is in the wrong state for the action. |
fine_failed / bill_failed | Taking money from the player failed (not enough money). |
unknown_action | Invoke was called with an action name that does not exist. |
failed | Unexpected 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.
local result, err = exports['koja-mdt']:Invoke('divisions:list', {}, { department = 'lspd' })See Invoke and GetApiActions in the server exports for details.