Koja Scripts

Server exports

Every koja-mdt server export grouped by domain, with parameters, return shapes, error codes and Lua examples.

Server exports

All exports on this page are called from server scripts:

server.lua
local mdt = exports['koja-mdt']

Read the overview first: it explains the opts table (officer, department, author), the result, errorCode convention, who parameters and the shared shapes (officer, dispatch call).

Domains on this page: Generic ยท Dispatch ยท Citizens ยท Vehicles ยท Weapons ยท Incidents ยท Warrants ยท BOLOs ยท Bulletins ยท Citations ยท Evidence ยท Officers ยท Departments and permissions ยท Court ยท Custody ยท EMS ยท Notifications and mail ยท Calendar ยท ANPR ยท Charges ยท Hook management ยท Cameras and bodycams ยท Bodycam recordings ยท Storage


Generic

Invoke(action, payload, opts)

Runs any tablet action by name. Use it for actions that have no dedicated export (divisions, forms, DOJ bar, expungements, โ€ฆ).

ParamTypeNotes
actionstringAction name, for example divisions:list. See GetApiActions().
payloadtableSame payload the tablet sends.
optstable?officer, department, author, plus kind: 'reader' (cross-department read), 'ems' or 'doj' to pick the default System department type.

Returns the action result, or nil, errorCode. Errors unknown_action, not_authorized, game_only, plus the action's own codes.

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

GetApiActions()

Returns { name, perm, game, citizen }[] sorted by name. perm is the permission the action needs, game = true means it needs a live player (officer).

server.lua
for _, action in ipairs(exports['koja-mdt']:GetApiActions()) do print(action.name, action.perm) end

Dispatch

CreateDispatch(data)

Creates a live call, shows the blip and pushes it to every eligible unit (and to koja-ambulance when EMS receives it).

FieldTypeNotes
codestringUp to 16 chars. A registered code fills title, priority, types and blip when you leave them out.
titlestringUp to 96 chars, default Dispatch.
messagestringUp to 500 chars.
coordsvector3 / tablex, y, z.
streetstringOptional; clients fill it from coords when missing.
prioritynumber1 (high) to 3 (low), default 2.
departmentsstring[]Department ids that receive the call.
jobsstring[]Alternative to departments: framework jobs, resolved to departments.
typesstring[]Alternative: department types (leo, ems, fire, doj).
callertable{ name, phone }.
bliptable{ sprite, color, scale }.
sourcenumberOptional player who caused the call; passed to hooks as src.

Without departments, jobs or types the call goes to law enforcement.

Returns callId (number) or nil when dispatch is disabled or a dispatch:create hook cancelled it.

server.lua
local id = exports['koja-mdt']:CreateDispatch({
    code = '10-90', title = 'Bank alarm', message = 'Fleeca Great Ocean',
    coords = vector3(-2957.6, 481.4, 15.7), priority = 1,
    departments = { 'lspd', 'bcso' },
    blip = { sprite = 161, color = 1, scale = 1.2 },
})

UpdateDispatch(callId, changes, officer?)

Edits a call in place and pushes the update to every unit that sees it. Fires dispatch:update.

ParamTypeNotes
callIdnumber
changestableAny of title, message, priority, street, coords, code. Omitted fields keep their value.
officernumber?Server id recorded as the editor (logs, hooks).

Returns the updated call or nil, errorCode. Errors not_found, or the reason a before hook returned.

server.lua
exports['koja-mdt']:UpdateDispatch(id, { priority = 1, message = 'Hostages reported inside' })

AddDispatchNote(callId, text, author?)

Appends a note to the call (max 30 notes, oldest dropped). Fires dispatch:note.

ParamTypeNotes
callIdnumber
textstringUp to 300 chars.
authornumber / string?An officer's server id (shown as callsign name) or a display name. Default System.

Returns the updated call or nil, errorCode. Errors invalid, hook reason.

server.lua
exports['koja-mdt']:AddDispatchNote(id, 'Silent alarm re-triggered', 'Fleeca Security')

CloseDispatch(callId)

Closes an active call and frees its units. Fires dispatch:close. Returns boolean.

server.lua
exports['koja-mdt']:CloseDispatch(id)

AttachUnit(callId, src)

Assigns an on-duty field unit to an active call (the unit leaves any other call, gets the waypoint when configured). Fires dispatch:assign. Returns boolean.

DetachUnit(callId, src)

Removes a unit from a call. Fires dispatch:detach. Returns boolean.

server.lua
if exports['koja-mdt']:AttachUnit(id, source) then print('attached') end
exports['koja-mdt']:DetachUnit(id, source)

GetCall(callId)

Returns the call or nil. Calls are kept in memory for Config.Dispatch.keepCallsMinutes.

GetActiveCalls(department?)

Returns active calls, newest first. With department, only the calls that department can see.

GetDispatchCalls(filter?)

FieldTypeNotes
statusstringactive, closed or all (default).
departmentstringOnly calls this department can see. A plain string is treated as department.
limitnumber1 to 200, default 50.

Returns call[], newest first.

server.lua
local recent = exports['koja-mdt']:GetDispatchCalls({ status = 'closed', department = 'lspd', limit = 10 })

SendPanic(src)

Triggers the officer's 10-99 exactly like the panic key (law enforcement only, same cooldown). Returns callId or nil, errorCode. Errors not_authorized, no_permission, cooldown, not_in_game, cancelled, disabled.

SetUnitStatus(src, status)

Sets the unit status. status must be one of the department's statuses (GetUnitStatuses(department)). Fires unit:status. Returns true or false, errorCode (invalid, hook reason).

GetUnitStatus(src)

Returns status, callId or nil.

server.lua
local status, callId = exports['koja-mdt']:GetUnitStatus(source)

RegisterDispatchCode(code, data)

Adds a call type at runtime. Registered codes behave like Config.Dispatch.callCodes: CreateDispatch({ code = 'ROB-STORE' }) picks up the title, priority, receiving department types and blip.

FieldTypeNotes
titlestringDefault title.
prioritynumber1 to 3.
typesstring[]Department types that receive the call (leo, ems, fire). Omit for law enforcement.
bliptable{ sprite, color, scale }.
publicbooleantrue lets clients raise this code through the guarded 911 path (client SendDispatch). Server-side coords, rate limits and Config.Dispatch.publicCooldown apply.

Registering an existing code overrides it; the original comes back when your resource stops. Returns boolean.

server.lua
exports['koja-mdt']:RegisterDispatchCode('ROB-STORE', {
    title = 'Store robbery', priority = 1, types = { 'leo' },
    blip = { sprite = 52, color = 1, scale = 1.1 },
})

RemoveDispatchCode(code)

Only the resource that registered the code can remove it. Returns boolean.

GetDispatchCodes()

Returns every code: { code, title, priority, types, blip, public, custom, command }[].


Citizens

Citizen shape (framework data plus MDT profile):

{
    identifier = 'ABC12345', firstname = 'John', lastname = 'Doe', name = 'John Doe',
    dob = '1990-01-01', gender = 'male', phone = '555-0100',
    image = 'https://...', tags = { 'armed' }, warrants = 1, online = true,
}

GetCitizen(identifier)

Returns the citizen plus licenses and active bolos, or nil.

server.lua
local citizen = exports['koja-mdt']:GetCitizen(identifier)
if citizen and citizen.warrants > 0 then print(citizen.name .. ' is wanted') end

GetCitizenProfile(identifier, opts?)

The full record the tablet shows: citizen plus notes, tags, gallery, fingerprint, dna, licenses, vehicles, weapons, incidents, record (criminal summary), warrants (all statuses), bolos and court cases. Runs as a cross-department reader unless opts.officer is given (then the officer's visibility rules apply).

Returns the profile or nil, errorCode. Errors invalid, not_found.

SearchCitizens(query, limit?)

Name, phone or id search (2+ characters). limit 1 to 50. Returns citizen[].

GetCitizenNames(identifiers)

Returns { [identifier] = 'Full Name' } for up to 50 identifiers.

GetCriminalRecord(identifier)

Returns { incidents, summary } or nil. Expunged records are excluded. summary = { charges = { { code, label, class, count } }, totals = { fine, months, points, convictions, felonies } }; totals count processed suspect entries only.

server.lua
local record = exports['koja-mdt']:GetCriminalRecord(identifier)
if record and record.summary.totals.felonies > 0 then print('felon') end

UpdateCitizenProfile(identifier, changes, opts?)

Edits the MDT profile. Omitted fields keep their current value. Fires citizen:update.

FieldTypeNotes
imagestringPhoto URL.
notesstringUp to 8000 chars, replaces the notes. Use AddCitizenNote to append.
tagsstring[]Profile flags (Config.ProfileTags ids).
gallerystring[]Image URLs.
fingerprint, dnastringUp to 32 chars.

Returns true or nil, errorCode. Errors not_found, no_permission, hook reason.

server.lua
exports['koja-mdt']:UpdateCitizenProfile(identifier, { fingerprint = 'FP-88213' })

AddCitizenNote(identifier, text, author?)

Appends a timestamped line to the profile notes (kept to the last 8000 chars). Fires citizen:update. Returns boolean.

SetCitizenFlag(identifier, tag, state)

Adds (state ~= false) or removes a profile flag. tag must be an id from Config.ProfileTags (armed, gang, parole, probation, violent, officer_safety, mental, informant, flight_risk). Fires citizen:update. Returns boolean.

server.lua
exports['koja-mdt']:SetCitizenFlag(identifier, 'parole', true)

GetCitizenLicenses(who)

Returns the framework licenses of the citizen, as provided by the framework bridge.

SetCitizenLicense(who, licenseType, granted, opts?)

Grants (granted ~= false) or revokes a license through the framework bridge. Fires citizen:license.

Returns the updated license list or nil, errorCode. Errors invalid, failed (bridge refused), hook reason.

server.lua
exports['koja-mdt']:SetCitizenLicense(identifier, 'driver', false, { author = 'DMV' })

Vehicles

Vehicle shape:

{
    plate = 'ABC123', model = 'sultan', owner = 'ABC12345', ownerName = 'John Doe',
    flags = { 'stolen' }, points = 3, image = nil, bolo = false, unregistered = nil,
}

GetVehicle(plate)

Returns the vehicle with bolos and active or pending warrants. Unknown plates return { plate, unregistered = true }.

SearchVehicles(query, limit?)

Plate search (2+ characters). Returns vehicle[].

GetCitizenVehicles(who)

Returns every vehicle the citizen owns, decorated with MDT flags and points.

GetVehicleFlags(plate) / HasVehicleFlag(plate, flag)

Returns string[] / boolean. Flags: stolen, wanted, impounded, noinsurance, noregistration, seized.

SetVehicleFlag(plate, flag, state)

Adds or removes one flag. Fires vehicle:flags. Returns boolean.

server.lua
exports['koja-mdt']:SetVehicleFlag(plate, 'stolen', true)

UpdateVehicle(plate, changes, opts?)

Edits the MDT vehicle record. Omitted fields keep their value. Fires vehicle:flags.

FieldTypeNotes
flagsstring[]Replaces the flag list. Unknown flags are dropped.
notesstringUp to 4000 chars.
pointsnumber0 to 99. Reaching Config.Records.vehicleStrikeLimit warns units.
imagestringURL.

Returns true or nil, errorCode.

server.lua
exports['koja-mdt']:UpdateVehicle('ABC123', { points = 4, notes = 'Speed camera strike' })

Weapons

Weapon shape: { serial, identifier, owner, model, label, status, notes, image, registeredBy, createdAt }. Statuses: registered, stolen, seized, destroyed, evidence.

RegisterWeapon(data)

Registers or updates a serial (use it from gun shops). data = { serial, model, identifier, owner, label, status, notes, image }; serial and model are required, owner is looked up from identifier when missing. Fires weapon:save. Returns boolean.

server.lua
exports['koja-mdt']:RegisterWeapon({ serial = 'WPN-55213', model = 'WEAPON_PISTOL', identifier = identifier, label = 'Pistol' })

GetWeapon(serial)

Returns the weapon plus evidence ({ id, label, incidentId, createdAt }[]) or nil, errorCode (invalid, not_found).

SearchWeapons(query?)

Serial, owner, model or label search; without a query the 50 newest. Returns weapon[].

GetCitizenWeapons(who)

Returns weapon[] registered to the citizen.

SetWeaponStatus(serial, status, opts?)

Changes the status of a registered weapon. opts.notes replaces the notes. Fires weapon:save. Returns true or nil, errorCode (invalid, not_found, hook reason).

server.lua
exports['koja-mdt']:SetWeaponStatus('WPN-55213', 'stolen', { notes = 'Reported stolen by owner' })

Incidents

Incident shape (GetIncident, and the result of every incident write):

{
    id = 42, title = 'Armed robbery at Fleeca', type = 'robbery', status = 'open', department = 'lspd',
    location = 'Great Ocean Hwy', restricted = false, author = 'John Doe', authorIdentifier = 'ABC12345',
    narrative = '...', tags = { 'armed' }, images = {},
    officers = { { identifier, name, callsign, department } },
    people = { {
        id = 7, identifier = 'XYZ999', name = 'Jane Roe', role = 'suspect',
        charges = { { code = 'P.C. 2005', label = 'Robbery', class = 'felony', count = 1, modifier = nil, fine = 3000, months = 25, points = 0 } },
        fine = 3000, months = 25, points = 0, reduction = 0, plea = nil, notes = '',
        processed = false, processedAt = nil, processedBy = nil,
    } },
    vehicles = { { plate, model, notes } },
    evidence = { evidence... }, warrants = { ... }, bolos = { ... }, cases = { { id, title, status, verdict } },
    activity = { { identifier, name, action, at } }, createdAt = ..., updatedAt = ...,
}

people[].role: suspect, victim, witness, other. status: open, pending, closed, archived. type: an id from Config.IncidentTypes.

CreateIncident(data)

Same payload as the tablet report editor. Fires incident:create.

FieldTypeNotes
titlestringRequired, 160 chars.
typestringConfig.IncidentTypes id, default other.
statusstringDefault open.
location, narrativestring
tagsstring[]Up to 12.
imagesstring[]URLs.
restrictedbooleanNeeds incidents.restricted.
peopletable[]{ identifier, name, role, charges = { { code, count, modifier } }, reduction, plea, notes }, up to 30. Fine, months and points are calculated from the penal code.
vehiclestable[]{ plate, model, notes }, up to 15.
officerstable[]{ identifier, name, callsign, department }. A real acting officer is always listed first; System is never listed.
evidencenumber[]Evidence ids to link.
officer, department, authorActing identity.

Returns the incident or nil, errorCode. Errors title_required, no_permission, not_authorized, hook reason.

server.lua
local incident = exports['koja-mdt']:CreateIncident({
    title = 'Store robbery - 24/7 Innocence Blvd', type = 'robbery', author = 'Auto Report',
    people = { { identifier = suspectId, name = 'Unknown', role = 'suspect', charges = { { code = 'P.C. 2005' } } } },
})

UpdateIncident(id, changes, opts?)

Loads the incident, applies changes and saves it through the editor action. Any field of CreateIncident can be changed; lists (people, vehicles, officers, tags) replace the current list. Processed people are locked: only their notes and plea change. Fires incident:update.

Returns the incident or nil, errorCode. Errors invalid, not_found, no_permission, hook reason.

server.lua
exports['koja-mdt']:UpdateIncident(42, { status = 'closed' })

AddPersonToIncident(id, person, opts?)

Appends a person. person is an identifier string or { identifier, name, role, charges, reduction, plea, notes }; name is looked up from the identifier when missing. Fires incident:update.

Returns the incident or nil, errorCode. Errors invalid, not_found, too_many (30 people), hook reason.

server.lua
exports['koja-mdt']:AddPersonToIncident(42, { identifier = witnessId, role = 'witness', notes = 'Clerk' })

AddChargeToIncident(incidentId, person, charges)

Merges charges into the person's unprocessed suspect entry (created when missing) and recalculates fine, months and points. person is an identifier or { identifier, name }, charges is { { code, count, modifier } } or plain codes. Fires incident:update (after stage only).

Returns the incident or nil, errorCode. Errors invalid, not_found.

server.lua
exports['koja-mdt']:AddChargeToIncident(42, suspectId, { { code = 'P.C. 2005', count = 1 } })

AddVehicleToIncident(id, vehicle, opts?)

vehicle is a plate or { plate, model, notes }. Already linked plates are ignored. Returns the incident or nil, errorCode.

server.lua
exports['koja-mdt']:AddVehicleToIncident(42, 'ABC123')

LinkEvidenceToIncident(id, evidenceIds, opts?)

Links one evidence id or a list of ids. Returns the incident or nil, errorCode.

server.lua
exports['koja-mdt']:LinkEvidenceToIncident(42, { 101, 102 })

DeleteIncident(id, opts?)

Deletes the report, its people, officers and vehicles; evidence is unlinked, not deleted. Reports with processed suspects need the command rank (System has it). Fires incident:delete. Returns true or nil, errorCode (not_found, has_processed, hook reason).

ProcessSuspect(personId, opts)

Applies the fine and jail time of an incident suspect exactly like the tablet process button: money is taken through the fine integration, the jail adapter runs, arrest warrants are served and points can revoke the license. Fires incident:process.

FieldTypeNotes
personIdnumberincident.people[].id.
opts.officernumberRequired. The processing officer (distance checks use their position).
opts.finebooleanfalse skips the fine.
opts.jailbooleanfalse skips jail.

Returns the incident or nil, errorCode. Errors game_only (no officer), disabled, not_found, already_processed, not_suspect, target_offline, target_far, fine_failed, hook reason.

server.lua
local incident, err = exports['koja-mdt']:ProcessSuspect(personId, { officer = source, jail = true })

GetIncident(id)

Returns the incident or nil, 'not_found'.

SearchIncidents(filter?)

FieldTypeNotes
querystringTitle, location, author or person name; #42 matches the id.
status, typestring
unprocessedbooleanOnly reports with unprocessed suspects.
pagenumber25 per page.
officer, mineWith officer and mine = true, only that officer's reports.

Returns { items, page, hasMore }. Items are summaries with involved, charges, fine, months, unprocessed and evidenceCount.

server.lua
local page = exports['koja-mdt']:SearchIncidents({ unprocessed = true })

Warrants

Warrant shape:

{
    id = 9, type = 'arrest', identifier = 'XYZ999', name = 'Jane Roe', plate = nil, address = nil,
    incidentId = 42, incidentTitle = '...', reason = '...', charges = { ... }, status = 'active',
    department = 'lspd', requestedBy = 'John Doe', requestedIdentifier = 'ABC12345',
    reviewedBy = 'Judge Smith', reviewNote = nil, reviewedAt = ..., closedBy = nil, closedAt = nil,
    expiresAt = ..., createdAt = ..., updatedAt = ...,
}

Types: arrest, search, vehicle, bench. Statuses: pending, active, served, denied, expired, cancelled.

CreateWarrant(data)

FieldTypeNotes
typestringDefault arrest.
namestringRequired.
reasonstringRequired, 4000 chars.
identifier, plate, addressstringplate is required for vehicle.
chargestable[]Penal code entries.
hoursnumberExpiry, default Config.Records.defaultWarrantHours, max 720.
incidentIdnumber

As System the warrant is active immediately. With officer = src it follows Config.Records.warrantsNeedApproval (pending until a judge approves). Fires warrant:create.

Returns the warrant or nil, errorCode. Errors invalid, plate_required, hook reason.

server.lua
exports['koja-mdt']:CreateWarrant({ type = 'bench', identifier = id, name = 'Jane Roe', reason = 'Failure to appear', hours = 168, department = 'doj' })

ApproveWarrant(id, note?, opts?) / DenyWarrant(id, note?, opts?)

Reviews a pending warrant (note up to 512 chars). The requester gets a toast. Fires warrant:approve / warrant:deny. Returns the warrant or nil, errorCode (invalid, not_found, not_pending, hook reason).

server.lua
exports['koja-mdt']:ApproveWarrant(9, 'Probable cause established', { officer = judgeSrc })

ServeWarrant(id, note?, opts?) / CancelWarrant(id, note?, opts?)

Closes a warrant as served or cancelled. Fires warrant:serve / warrant:cancel. Returns the warrant or nil, errorCode (invalid, not_found, no_permission, hook reason).

server.lua
for _, warrant in ipairs(exports['koja-mdt']:GetActiveWarrants(identifier)) do
    if warrant.type == 'arrest' then exports['koja-mdt']:ServeWarrant(warrant.id, 'Arrested at border checkpoint') end
end

GetWarrant(id)

Returns the warrant or nil.

GetActiveWarrants(identifier)

Returns { id, type, reason, expiresAt }[] of active warrants.

HasActiveWarrant(identifier)

Returns boolean.

GetCitizenWarrants(who)

Returns the 50 newest warrants of the citizen in every status.

GetPlateWarrants(plate)

Returns the 50 newest warrants issued for the plate in every status.

server.lua
for _, warrant in ipairs(exports['koja-mdt']:GetPlateWarrants('ABC123')) do
    if warrant.status == 'active' then print('vehicle warrant #' .. warrant.id) end
end

SearchWarrants(filter?)

filter = { status, type, query }. status also accepts closed (denied, expired, cancelled) and all; the default is pending and active. Returns up to 100 warrants.


BOLOs

BOLO shape: { id, type, title, description, identifier, plate, model, image, priority, status, department, createdBy, createdAt }. Types: person, vehicle, general. Priority 1 (high) to 3.

CreateBolo(data)

data = { type, title, description, identifier, plate, image, priority } plus acting identity. Broadcasts the BOLO toast to law enforcement. Fires bolo:create. Returns the BOLO or nil, errorCode (invalid, hook reason).

server.lua
exports['koja-mdt']:CreateBolo({ type = 'vehicle', title = 'Black Sultan, armed occupants', plate = 'ABC123', priority = 1 })

UpdateBolo(id, changes, opts?)

Changes any field of CreateBolo. Fires bolo:update. Returns the BOLO or nil, errorCode (not_found, hook reason).

server.lua
exports['koja-mdt']:UpdateBolo(5, { priority = 3, description = 'Last seen heading to Paleto' })

CloseBolo(id, opts?)

Fires bolo:close. Returns true or nil, errorCode (not_found, no_permission, hook reason).

GetBolo(id) / GetActiveBolos(limit?) / GetCitizenBolos(who) / GetPlateBolos(plate)

Returns a BOLO / bolo[]. The list variants return active BOLOs only (limit 1 to 100, default 50).

server.lua
if #exports['koja-mdt']:GetPlateBolos(plate) > 0 then print('BOLO on ' .. plate) end

Bulletins

Bulletin shape: { id, title, content, department, pinned, author, createdAt }. department = nil means global.

PostBulletin(data)

data = { title, content, department, pinned, author }. No department posts a global bulletin. Fires bulletin:create. Returns true or false, errorCode.

server.lua
exports['koja-mdt']:PostBulletin({ title = 'Road closure', content = 'Route 68 closed until 22h', department = 'sahp' })

UpdateBulletin(id, changes, opts?)

changes = { title, content, pinned }. As System it runs in the bulletin's own department. Fires bulletin:update. Returns the bulletin or nil, errorCode (not_found, no_permission, hook reason).

DeleteBulletin(id, opts?)

Fires bulletin:delete. Returns true or nil, errorCode.

GetBulletins(department?, limit?)

Returns the department's and the global bulletins, pinned first (limit 1 to 100, default 30). Without department, only global bulletins.


Citations

Citation shape:

{
    id = 12, number = 'SAHP-2026-000012', department = 'sahp', departmentInfo = { id, label, short, motto, color, logo },
    citizenIdentifier = 'XYZ999', citizenName = 'Jane Roe',
    officer = { identifier, name, callsign, badge, rank },
    violations = { { code, label, count, fine, points } }, total = 750, lateFee = 0, amountDue = 750, points = 2,
    location = 'Route 68', coords = { x, y, z }, plate = 'ABC123', vehicleModel = 'sultan', notes = '',
    status = 'unpaid', dueAt = ..., paidAt = nil, paidBy = nil,
    contestReason = nil, contestedAt = nil, courtCaseId = nil,
    voidedBy = nil, voidReason = nil, voidedAt = nil, printed = 0, incidentId = nil, createdAt = ...,
}

Statuses: unpaid, paid, contested, void, overdue. Results of write actions also carry viewer (isViolator, isOfficer, canPay, canContest) and ui.

IssueCitation(data)

FieldTypeNotes
violationstable[] / string[]Required. Codes of charges whose class is allowed for citations (Config.Citations.classes), as { code, count } or plain codes.
identifierstringViolator; the name is looked up.
namestringUsed when there is no identifier.
plate, vehicleModel, location, notesstring
coordstableDefaults to the officer's position when officer is given.
incidentIdnumber
notifybooleanfalse skips the violator notification.
print, handToOnly with officer: print = true gives the paper citation item, handTo = 'target' hands it to the nearby violator.
officer, author, departmentdepartment sets the number prefix.

Points are applied and can revoke the license (Config.Processing.pointsRevokeAt). Fires citation:create and the koja-mdt:server:citationIssued event.

Returns the citation or nil, errorCode. Errors disabled, no_violations, invalid, hook reason.

server.lua
local citation, err = exports['koja-mdt']:IssueCitation({
    identifier = ownerId, plate = 'ABC123', violations = { 'V.C. 7001' },
    location = 'Route 68 speed camera', department = 'sahp', author = 'Speed Camera 4',
})

PayCitation(id, opts?)

FieldTypeNotes
opts.chargebooleantrue takes the amount due from the violator (online, or offline when Config.Citations.allowWebPay is on and the framework bridge supports offline charges). Default false: only marks the citation paid, use it when your script already took the money.

Works on unpaid, overdue and contested citations. Fires citation:pay. Returns the citation or nil, errorCode (not_found, not_payable, already_processed, target_offline, game_only, fine_failed, hook reason).

server.lua
if exports.my_bank:RemoveMoney(source, citation.amountDue) then
    exports['koja-mdt']:PayCitation(citation.id, { author = 'Maze Bank' })
end

VoidCitation(id, reason, opts?)

reason is required (255 chars). Paid or void citations cannot be voided. Fires citation:void. Returns the citation or nil, errorCode (invalid, not_found, not_payable, no_permission, hook reason).

server.lua
exports['koja-mdt']:VoidCitation(12, 'Camera calibration error')

ResolveCitation(id, uphold, note?, opts?)

Rules on a contested citation: uphold = true makes it payable again (with the contest grace period), false voids it. Returns the citation or nil, errorCode (not_found, not_payable).

GetCitation(id) / GetCitationByNumber(number)

Returns the citation or nil.

GetCitations(identifier, status?)

Returns the 100 newest citations of the citizen, optionally with one status.

GetUnpaidCitations(who)

Returns open (unpaid and overdue) citations, oldest due date first.

GetOutstandingTotal(who)

Returns amount, count: the sum of total + lateFee of open citations and how many there are.

server.lua
local owed, count = exports['koja-mdt']:GetOutstandingTotal(source)
if owed > 0 then print(('You owe $%d on %d citations'):format(owed, count)) end

SearchCitations(filter?)

filter = { status, identifier, query, page, officer, mine }. status also accepts open. Returns { items, page, hasMore } (25 per page).


Evidence

Evidence shape: { id, incidentId, incidentTitle, label, type, serial, description, images, location, status, custody, createdBy, createdAt }. custody is the chain of custody: { at, by, identifier, callsign, action, note, to, location }[]. Statuses: stored, checked_out, released, destroyed, lab. Types come from Config.EvidenceTypes.

AddEvidence(data)

data = { label, type, serial, description, location, incidentId, createdBy } (label required). Fires evidence:create. Returns the evidence id or nil.

server.lua
local evidenceId = exports['koja-mdt']:AddEvidence({ label = '9mm casing', type = 'ballistic', serial = 'WPN-55213', incidentId = 42, createdBy = 'Forensics' })

UpdateEvidence(id, changes, opts?)

Changes incidentId, label, type, serial, description, images, location or status. A status change adds a custody entry (custodyNote sets its note). Fires evidence:update. Returns the evidence or nil, errorCode.

server.lua
exports['koja-mdt']:UpdateEvidence(evidenceId, { status = 'released', custodyNote = 'Returned to owner' })

TransferEvidence(id, data, opts?)

FieldTypeNotes
actionstringtransferred, checked_out, returned (back to stored), lab, released, destroyed.
locationstringNew storage location.
tostringNew holder. transferred needs to or a new location.
notestring255 chars.

Released or destroyed evidence cannot move. Fires evidence:transfer. Returns the evidence or nil, errorCode (invalid, not_found, hook reason).

server.lua
exports['koja-mdt']:TransferEvidence(evidenceId, { action = 'lab', note = 'Ballistics comparison' })

DeleteEvidence(id, opts?)

Fires evidence:delete. Returns true or nil, errorCode.

GetEvidence(id) / GetIncidentEvidence(incidentId) / SearchEvidence(filter?)

filter = { query, type, status }. Returns evidence / evidence[] (search returns the 100 newest).


Officers

See the officer shape. Only players whose job maps to an MDT department are officers.

GetOfficer(src)

Returns the officer or nil.

GetOfficerByIdentifier(identifier)

Works for offline members too (resolved from the framework job; source is nil and onDuty is false when offline). Returns the officer or nil.

GetOnDutyOfficers(filter?)

filter is a department id or { department, type, field }. Returns on-duty officer[], sorted by callsign.

GetOfficers(filter?)

Every online member. filter is a department id or { department, type, field, onDuty, status }. Returns officer[].

CountOnDuty(filter?)

Same filter as GetOfficers (onDuty is forced to true). Returns a number. Cheaper than counting GetOnDutyOfficers.

server.lua
if exports['koja-mdt']:CountOnDuty({ type = 'leo' }) < 3 then return end
local lspd = exports['koja-mdt']:GetOfficers({ department = 'lspd', status = 'available' })

IsOnDuty(src)

Returns boolean.

SetDuty(src, state)

Clocks the officer in (true) or out (false). Respects framework duty sync, koja-ambulance duty mode and the officer:duty hook. Returns boolean: whether the officer is now in the requested state.

server.lua
exports['koja-mdt']:SetDuty(source, true)

GetCallsign(src) / SetCallsign(src, callsign)

callsign up to 16 chars, saved to the officer record and pushed to every tablet. Returns the callsign or nil / boolean.

GetDutyTime(who)

Returns totalSeconds, weekSeconds (including the running shift).

GetRoster(department)

Returns members, departmentConfig or nil, 'invalid'. Member: { identifier, name, job, grade, gradeLabel, rank, callsign, badge, image, phone, certifications, divisions, dutySeconds, weekSeconds, lastSeen, joinedAt, suspended, online, onDuty, status }, sorted by grade.

server.lua
local members = exports['koja-mdt']:GetRoster('bcso')
for _, member in ipairs(members or {}) do print(member.callsign, member.name, member.gradeLabel) end

HasCertification(who, certId)

Returns boolean: the officer holds the active certificate certId (for example fto, swat).

GetOfficerCertifications(who)

Returns { id, label, color, icon, status, division, assignedAt, assignedBy }[].

AssignCertification(who, certId, opts?) / RevokeCertification(who, certId, opts?)

Certificates belong to a department; as System the officer's own department is used unless opts.department is set. The officer gets a notification on assignment. Returns true or nil, errorCode (invalid, not_found, cert_disabled).

server.lua
exports['koja-mdt']:AssignCertification(identifier, 'k9', { author = 'Training Academy' })

Departments and permissions

Department info shape: { id, label, short, type, field, color, jobs }.

GetDepartment(src)

Returns departmentId, info where info also has rank, grade, gradeLabel and isBoss, or nil.

GetDepartmentByJob(job)

Returns departmentId, info or nil.

GetDepartmentInfo(departmentId)

Returns info plus statuses (unit statuses) and leo (boolean), or nil.

GetDepartments()

Returns Config.Departments keyed by id, including admin panel overrides.

ReloadDepartments()

Reloads the admin panel department overrides from the database and pushes them to every client. Returns true.

GetUnitStatuses(departmentId?)

Returns { id, code, color }[]: the statuses the department's units can use (SetUnitStatus).

HasPermission(src, perm) / GetPermissions(src) / GetPermissionList()

See permissions.

server.lua
local department, info = exports['koja-mdt']:GetDepartment(source)
if department and info.type == 'leo' and exports['koja-mdt']:HasPermission(source, 'citations.create') then
    -- allow the radar gun
end

Court

Court exports run as a DOJ System session by default. Case shape:

{
    id = 3, number = 'SA-00003', title = 'State v. Roe', caseType = 'criminal', incidentId = 42,
    defendantIdentifier = 'XYZ999', defendantName = 'Jane Roe',
    judge = 'Judge Smith', judgeIdentifier = '...', prosecutor = '...', prosecutorIdentifier = '...', defense = '...', defenseIdentifier = '...',
    status = 'scheduled', verdict = nil, plea = 'not_guilty', sentence = nil, sentenceFine = nil, sentenceMonths = nil,
    sentenceAppliedAt = nil, sentenceAppliedBy = nil, hearingAt = ..., courtroom = 'Courtroom 1', sealed = false, notes = '',
    chargeCount = 2, createdBy = '...', createdAt = ..., updatedAt = ..., closedAt = nil,
}

Detailed results (GetCourtCase and every court write) add charges ({ id, code, label, class, count, modifier, fine, months, points, plea, verdict }[]), totals, hearings, docket, links, citations and access.

Statuses: filed, arraignment, scheduled, trial, verdict, sentencing, closed, dismissed, appeal. Verdicts: guilty, not_guilty, plea_deal, dismissed, mistrial.

CreateCourtCase(data)

FieldTypeNotes
titlestringRequired.
defendantNamestringRequired.
defendantIdentifierstring
caseTypestringConfig.Doj.caseTypes, default criminal.
incidentIdnumber
judge, prosecutor, defensestringNames; add judgeIdentifier, prosecutorIdentifier, defenseIdentifier to notify them.
courtroom, notesstring
sealedboolean

Fires court:create. Returns the detailed case or nil, errorCode (invalid, hook reason).

server.lua
local case = exports['koja-mdt']:CreateCourtCase({ title = 'State v. Roe', defendantName = 'Jane Roe', defendantIdentifier = id, incidentId = 42 })

SetCourtCaseStatus(id, status, note?, opts?)

Fires court:status. Returns the case or nil, errorCode (invalid, not_found, no_permission, hook reason).

SetCourtVerdict(id, verdict, note?, opts?)

Sets the verdict, fills open charge verdicts and moves the case (guilty โ†’ sentencing, not_guilty โ†’ closed, dismissed โ†’ dismissed, mistrial โ†’ scheduled). Fires court:verdict.

SetCourtSentence(id, sentence, opts?)

sentence = { fine, months, notes }. Stores the sentence without applying it. Returns the case or nil, errorCode (already_processed).

ApplyCourtSentence(id, opts?)

Applies the stored sentence through the fine and jail integrations (the defendant must be online unless Config.Doj.sentence.requireOnline = false), records the conviction on the linked incident and closes the case. Fires court:sentence and koja-mdt:server:sentenced. Errors disabled, invalid, already_processed, target_offline, fine_failed, hook reason.

server.lua
exports['koja-mdt']:SetCourtSentence(3, { fine = 2500, months = 15, notes = 'Plea agreement' })
exports['koja-mdt']:ApplyCourtSentence(3)

ScheduleHearing(caseId, data, opts?)

data = { startsAt = 'YYYY-MM-DDTHH:MM', kind, courtroom, judge, notes, status, id }. Pass id to edit an existing hearing. Parties are notified. Errors invalid (bad startsAt), not_found.

server.lua
exports['koja-mdt']:ScheduleHearing(3, { startsAt = '2026-11-02T18:30', kind = 'trial', courtroom = 'Courtroom 1' })

AddDocketEntry(caseId, kind, body, opts?)

kind: note, filing, motion, ruling, order (configurable). Returns the case or nil, errorCode.

GetCourtCase(id) / SearchCourtCases(filter?) / GetDefendantCases(who)

filter = { status, caseType, query, officer, mine }; status also accepts closed and all (default: open cases). Returns a detailed case / up to 150 cases / up to 30 cases of the defendant.


Custody

Jail record shape:

{
    id = 5, identifier = 'XYZ999', name = 'Jane Roe', months = 20, secondsTotal = 1200, secondsServed = 300, secondsLeft = 900,
    releaseDue = ..., reason = 'MDT incident #42', charges = { ... }, incidentId = 42, incidentTitle = '...', caseId = nil,
    officer = 'John Doe', officerIdentifier = 'ABC12345', department = 'lspd', system = 'koja-police', facility = 'Bolingbroke',
    status = 'active', releaseReason = nil, releasedBy = nil, createdAt = ..., releasedAt = nil, online = true,
}

Impound shape: { id, plate, model, owner, ownerName, reason, fee, feeStatus, holdUntil, holdLeft, lot, officer, officerIdentifier, department, system, status, releasedBy, releaseNote, createdAt, releasedAt }. Jail statuses: active, released, escaped, transferred. Impound statuses: held, released.

Reporting from a jail or impound script

These record what your script already did in the game. They never jail or impound anyone themselves.

ExportReturns
ReportJail(who, months, data)jail record or nil. data = { name, reason, charges, incidentId, caseId, officer, officerIdentifier, department, facility, system }; officer is a server id, a name or { name, identifier, department }. A new sentence supersedes the active one.
ReportRelease(who, reason, data)closed record or nil. reason: released, escaped (units get a toast), transferred; data = { by }.
ReportImpound(plate, data)impound record or nil, errorCode. data = { reason, fee, hours, lot, officer, owner, ownerName, model, department }. Flags the vehicle impounded.
ReportImpoundRelease(plate, data)closed record or nil. data = { feeStatus = 'paid' / 'waived', note, by }.
GetActiveSentence(who)active jail record or nil.
IsImpounded(plate)boolean.

The same four reports can be sent as server events: koja-mdt:custody:jail, koja-mdt:custody:release, koja-mdt:custody:impound and koja-mdt:custody:impoundRelease (server side only, see events).

server.lua
exports['koja-mdt']:ReportJail(source, 25, { reason = 'Armed robbery', officer = officerSrc, facility = 'Bolingbroke' })
exports['koja-mdt']:ReportRelease(source, 'escaped', { by = 'Bolingbroke' })

Driving custody from the MDT

These run the MDT action, which calls the configured jail or impound adapter (see integrations).

ExportNotes
ReleaseInmate(recordId, reason?, opts?)Early release: runs the jail adapter's release, closes the record. Fires jail:release. Errors not_found, not_active.
UpdateSentence(recordId, months, reason?, opts?)Changes the length of an active sentence.
ImpoundVehicle(plate, data, opts?)data = { reason, fee, hours, lot } (reason required). Runs the impound adapter, records the impound, despawns the vehicle near opts.officer. Fires impound:create. Errors disabled, plate_required, already_impounded.
ReleaseImpound(idOrPlate, data?, opts?)data = { feeStatus = 'paid' / 'waived' / 'charge', note }. charge takes the fee from the online owner. Fires impound:release. Errors not_found, not_active, target_offline, fine_failed.

Lookups

ExportReturns
GetJailRecord(id)jail record or nil
GetJailHistory(who)the 50 newest jail records
GetImpound(id)impound or nil
GetHeldImpound(plate)the current held impound or nil
GetImpoundHistory(plate)the 50 newest impounds of the plate
GetOwnerImpounds(who)the 50 newest impounds of the owner's vehicles
server.lua
local held = exports['koja-mdt']:GetHeldImpound(plate)
if held then print(('Impounded at %s, fee $%d'):format(held.lot, held.fee)) end

EMS

Medical exports run as an EMS System session by default.

GetMedicalProfile(who) / UpdateMedicalProfile(who, changes, opts?)

Profile: { bloodType, allergies, conditions, medications, emergency = { name, phone, relation }, dnr, donor, notes, updatedBy, updatedAt }. UpdateMedicalProfile changes any of those fields (lists replace). bloodType must be in Config.Ems.bloodTypes. Returns the profile or nil, errorCode.

server.lua
exports['koja-mdt']:UpdateMedicalProfile(identifier, { allergies = { 'Penicillin' }, dnr = false })

GetPatient(who)

Returns the patient chart (identifier, name, dob, gender, phone, height, online, profile, history, bills, admissions) or nil, errorCode.

CreateTreatment(data)

Creates an ePCR. Fires treatment:create.

FieldTypeNotes
namestringRequired, patient name.
identifierstringPatient.
statusstringdraft or final.
typestringConfig.Ems.incidentTypes id.
location, complaint, narrativestring
vitalstable[]{ at, hr, sys, dia, spo2, rr, temp, gcs, pain }.
injuriestable[]{ side = 'front' / 'back', region, kind, severity, note }.
treatmentstable[]{ id } from Config.Ems.catalog or { label, kind, dose, count }.
crewtable[]{ identifier, name, callsign }.
transport, hospital, priority, outcomestringtransport: none, ground, air, police, self.
callIdnumberDispatch call.

Returns the treatment or nil, errorCode (patient_required, no_permission, hook reason).

GetTreatment(id)

Returns the full ePCR with vitals, injuries, treatments, crew, bills, admission, patient.

CreateMedicalBill(data)

data = { treatmentId, identifier, name, items = { { id, count } } }. Items come from Config.Ems.catalog / Config.Ems.fees; custom { label, price, count } items need Config.Ems.billing.allowCustomItems. Fires bill:create. Errors disabled, patient_required, bill_empty, bill_too_high.

ChargeMedicalBill(id, opts?)

Takes the bill from the online patient (or sends a koja-ambulance invoice in invoice mode, which needs opts.officer). Fires bill:status. Errors not_found, not_payable, target_offline, target_far, not_in_game, bill_failed.

SetMedicalBillStatus(id, status, reason?, opts?)

status: paid (mark as paid) or void. Fires bill:status. Errors invalid, not_found, not_payable.

GetMedicalBills(who, status?)

Returns the 50 newest bills: { id, treatmentId, identifier, name, department, items, total, status, issuedBy, issuedIdentifier, paidAt, paidBy, voidedBy, voidReason, external, createdAt }[].

AdmitPatient(data) / DischargePatient(admissionId, note?, opts?)

data = { hospital, bed, identifier, name, treatmentId, state, reason, notes }. hospital is an id from Config.Ems.hospitals; state: stable, observation, serious, critical. Fires hospital:admit / hospital:discharge and the koja-mdt:server:admission / koja-mdt:server:discharge events. Errors invalid, bed_taken, already_admitted, patient_required, not_found.

server.lua
local admission = exports['koja-mdt']:AdmitPatient({ hospital = 'pillbox', bed = 'ER-1', identifier = id, name = 'Jane Roe', state = 'serious' })

GetHospitalBoard()

Returns { hospitals = { { id, label, short, beds = { { id, admission } }, waiting, live } }, totals = { beds, occupied, critical, waiting } }.


Notifications and mail

Notifications land in the MDT notification center (stored and shown live). data = { title, body, kind, actor, link = { page, id } }; title is required (160 chars), body 500 chars, link.page is an MDT page such as citizens, vehicles, incidents, warrants, dispatch. Every notification fires notification:send.

ExportWho gets it
NotifyOfficer(who, data)One officer (server id or identifier, online or not) or a list of identifiers.
NotifyDepartment(departmentId, data)Everyone in the department.
NotifyAll(data)Everyone.
SendMdtNotification(target, data)Any of the above: server id, department id, identifier, string[] or nil (everyone).

All return boolean.

server.lua
exports['koja-mdt']:NotifyDepartment('lspd', { title = 'Prison transport arriving', body = 'Mission Row, gate 2 in 5 minutes', kind = 'system' })

SendMail(data)

FieldTypeNotes
fromstringSender name, default System.
tostring / tableIdentifier, department id, an array of either, or { identifiers = {}, departments = {}, all = true }.
subjectstring160 chars.
bodystringUp to Config.Mail.maxBodyLength.
departmentstringSender department.

Fires mail:send. Returns mailId or nil, errorCode (invalid, mail_no_recipients, mail_too_many, hook reason).

GetUnreadMailCount(who)

Returns the number of unread inbox messages.


Calendar

Calendar times are milliseconds since epoch.

CreateCalendarEvent(data)

FieldTypeNotes
titlestringRequired, 120 chars.
start, endnumberMilliseconds. end defaults to start.
allDayboolean
description, locationstring
categorystringtraining, meeting, operation, patrol, court, social, other.
coordstable{ x, y, z }.
audiencestringdepartment (default), division (with divisionId) or all.
visibilitystringeveryone or managers.
maxAttendees, requireRsvp
recurrence, repeatUntilnone, daily, weekly, biweekly, monthly.
officer, department, authorActing identity.

Fires calendar:create. Returns { id, start } or nil, errorCode (invalid, no_permission, hook reason).

server.lua
local now = os.time() * 1000
exports['koja-mdt']:CreateCalendarEvent({ title = 'Firearms qualification', category = 'training', start = now + 86400000, department = 'lspd' })

GetCalendarEvents(from, to, opts?)

opts.filters = { categories = {}, cancelled = false }. The range is capped at Config.Calendar.maxRangeDays. Returns { events, court, from, to }.

CancelCalendarEvent(id, reason?, opts?) / DeleteCalendarEvent(id, opts?)

opts.restore = true restores a cancelled event. Attendees are notified. Fires calendar:cancel / calendar:delete. Returns true or nil, errorCode. As System, pass the event's department when it belongs to another department.


ANPR

ANPR result shape:

{
    plate = 'ABC123', known = true, model = 'sultan', owner = { identifier = 'XYZ999', name = 'Jane Roe' },
    flags = { stolen = true, bolo = false, warrant = false, unknown = false },
    recordFlags = { 'stolen' }, bolos = 0, warrants = 0, hit = true,
}

CheckPlate(plate) / CheckPlates(plates)

Looks plates up with the same cache the in-car ALPR uses (Config.Anpr.cacheSeconds). Nothing is logged. Returns a result / { [plate] = result } (up to 50 plates).

ReportAnprRead(data)

Reports a read from your own camera or reader. When the plate is a hit it is written to the ANPR log, the reporting officer (if any) gets the hit on the tablet, and Config.Anpr.autoDispatch can raise a call.

FieldTypeNotes
platestringRequired.
sourcestringcamera (default) or vehicle.
cameraId, cameraLabelYour camera id and label.
coords, street, model
officernumberThe officer whose reader saw the plate.
departmentstringOwning department when there is no officer. Needed for auto dispatch.

Hits are rate limited per camera and plate (Config.Anpr.hitCooldown). Fires anpr:hit. Returns result, hit where hit is the log entry ({ id, plate, model, owner, flags, bolos, warrants, source, cameraId, cameraLabel, officer, department, street, coords, dispatchId, createdAt }) or false (no hit, cooldown or cancelled).

server.lua
local result, hit = exports['koja-mdt']:ReportAnprRead({ plate = plate, cameraLabel = 'Del Perro Pier gate', coords = coords, department = 'lspd' })

GetAnprLog(filter?)

filter = { page, perPage, source, flag, query, officer, mine }. Returns { items, total, page, perPage, stats }.


Charges

Charge shape: { id, code, label, category, class, fine, months, points, description }. Classes: infraction, misdemeanor, felony.

GetCharges() / GetCharge(code)

Returns the active penal code / one charge or nil.

CalculateCharges(list)

list = { { code, count, modifier } } or plain codes. Modifiers accessory, attempted, conspiracy, aiding halve fine and months. Returns resolved, totals with totals = { fine, months, points }.

server.lua
local _, totals = exports['koja-mdt']:CalculateCharges({ 'P.C. 2005', { code = 'P.C. 1001', modifier = 'attempted' } })
print(totals.fine, totals.months)

SaveCharge(data, opts?) / RemoveCharge(code, opts?)

data = { code, label, category, class, fine, months, points, description } (code, label, category required). Saving an existing code updates it; removing deactivates it. Both reload the penal code on every tablet. Returns the new charge list or nil, errorCode.


Hook management

ExportNotes
registerHook(name, fn, opts) โ†’ hookIdSee hooks. Returns nil, 'unknown_hook' / nil, 'invalid_function'.
removeHook(hookId) โ†’ booleanOnly the registering resource can remove its hook.
GetHookNames() โ†’ string[]Every hook name.

Cameras and bodycams

ExportReturns
GetCameras()Every CCTV camera row.
SetCameraBroken(cameraId, state)boolean. state = false repairs it.
GetBodycam(src){ source, identifier, name, callsign, department, serial, state, since, reason, street, coords, sessionId, netId, watchers } or nil.
StartBodycam(src, reason?)Truthy on success, false, errorCode when the player cannot record.
StopBodycam(src, reason?)true.
PoliceBridge(){ dispatch, duty, custody } when koja-police is running, otherwise nil.

TunnelSettings() and TunnelState() are used internally by the web portal tunnel (server/tunnel.js). Do not call them.

server.lua
AddEventHandler('my-heists:alarm', function(src)
    exports['koja-mdt']:StartBodycam(src, 'alarm')
end)

Bodycam recordings

While an officer's own body camera is recording, the wearer's client grabs a low resolution frame of their game view with screenshot-basic every intervalMs. Frames are uploaded in batches and packed into segment files of Config.Storage.segmentSeconds (about 80 frames). Segments are written to recordings/<sessionId>/s/<n>.kseg inside the resource folder and, when Config.Storage.footage = 'storage', uploaded to S3 or Fivemanage (see Storage and CDN). The database keeps a per-frame index with position, street, segment number and offset. Older recordings saved one file per frame stay as recordings/<sessionId>/<seq>.<jpg|webp> and still play. Officers with bodycams.view play the footage back on the Recordings tab of the Body cameras page.

GetBodycamRecordings(filter?)

Every filter field is optional.

FieldTypeNotes
identifierstringWearer.
departmentstring
incidentIdnumberSessions linked to the incident.
from, tostringDates, YYYY-MM-DD.
shotsbooleanOnly sessions with shots fired.
limit, offsetnumberPaging.

Returns sessions that have footage. Each entry has id, sessionId, identifier, name, callsign, department, serial, startedAt, endedAt, startReason, endReason, incidentId, incidentTitle, shots, events, live, plus frames, bytes, format, intervalMs, firstAt, lastAt and finalized. Timestamps are milliseconds.

server.lua
local list = exports['koja-mdt']:GetBodycamRecordings({ department = 'lspd', incidentId = 1042, shots = true, limit = 50 })

GetBodycamRecording(sessionId)

Returns the same fields as one list entry plus index ({ seq, at, x, y, z, heading, street, segment }[], segment is nil for old per-frame recordings), segments ({ n, bytes, frames, url? }[], url is a short-lived presigned or public link for segments on remote storage) and path (recordings/<sessionId> inside the resource), or nil when the session has no footage.

GetBodycamRecordingFrame(sessionId, seq)

Returns one frame as a data:image/...;base64, string, or nil. Frames in remote segments are downloaded from the bucket first, so call it from a thread.

server.lua
local recording = exports['koja-mdt']:GetBodycamRecording(sessionId)
if recording then
    local first = exports['koja-mdt']:GetBodycamRecordingFrame(sessionId, recording.index[1].seq)
end

Playback over HTTP

GET /recordings/<sessionId>/s/<n>?t=<token> on the resource HTTP handler serves one local segment, and GET /recordings/<sessionId>/<seq>?t=<token> serves one frame of an old per-frame recording. Both work even when the web portal is disabled. Tokens come from the footage:get MDT action (checks bodycams.view and department visibility), are tied to one session and expire after Config.Bodycams.recording.tokenMinutes. Segments on remote storage are fetched straight from the bucket with the URL in footage:get. When the route cannot be reached the player falls back to footage:segment, which returns one segment as base64 (or a fresh remote URL), and footage:read for old frames.

The segment file is a small container: the 4 ASCII bytes KSG1, a big-endian uint32 header length, a UTF-8 JSON header { v, sessionId, n, format, frames: [{ seq, at, offset, length }] }, then the frame images back to back. offset counts from the first byte after the header.

The bodycam:recordingSaved and bodycam:recordingDeleted hooks are listed on the hooks page.

Storage

GetStorageStatus()

Returns { driver, requested, footage, images, ok, message, checkedAt, pending, failures, lastError, warnings }. driver is the active driver (local, s3 or fivemanage), requested is what Config.Storage.driver asked for, footage is true when segments go to remote storage, images is inline, storage or webhook. ok and message come from the startup self-test, pending is the number of queued uploads and deletes.

server.lua
local storage = exports['koja-mdt']:GetStorageStatus()
if not storage.ok then print('MDT storage problem: ' .. tostring(storage.message)) end