Server exports
All exports on this page are called from server scripts:
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, โฆ).
| Param | Type | Notes |
|---|---|---|
action | string | Action name, for example divisions:list. See GetApiActions(). |
payload | table | Same payload the tablet sends. |
opts | table? | 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.
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).
for _, action in ipairs(exports['koja-mdt']:GetApiActions()) do print(action.name, action.perm) endDispatch
CreateDispatch(data)
Creates a live call, shows the blip and pushes it to every eligible unit (and to koja-ambulance when EMS receives it).
| Field | Type | Notes |
|---|---|---|
code | string | Up to 16 chars. A registered code fills title, priority, types and blip when you leave them out. |
title | string | Up to 96 chars, default Dispatch. |
message | string | Up to 500 chars. |
coords | vector3 / table | x, y, z. |
street | string | Optional; clients fill it from coords when missing. |
priority | number | 1 (high) to 3 (low), default 2. |
departments | string[] | Department ids that receive the call. |
jobs | string[] | Alternative to departments: framework jobs, resolved to departments. |
types | string[] | Alternative: department types (leo, ems, fire, doj). |
caller | table | { name, phone }. |
blip | table | { sprite, color, scale }. |
source | number | Optional 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.
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.
| Param | Type | Notes |
|---|---|---|
callId | number | |
changes | table | Any of title, message, priority, street, coords, code. Omitted fields keep their value. |
officer | number? | 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.
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.
| Param | Type | Notes |
|---|---|---|
callId | number | |
text | string | Up to 300 chars. |
author | number / 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.
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.
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.
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?)
| Field | Type | Notes |
|---|---|---|
status | string | active, closed or all (default). |
department | string | Only calls this department can see. A plain string is treated as department. |
limit | number | 1 to 200, default 50. |
Returns call[], newest first.
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.
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.
| Field | Type | Notes |
|---|---|---|
title | string | Default title. |
priority | number | 1 to 3. |
types | string[] | Department types that receive the call (leo, ems, fire). Omit for law enforcement. |
blip | table | { sprite, color, scale }. |
public | boolean | true 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.
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.
local citizen = exports['koja-mdt']:GetCitizen(identifier)
if citizen and citizen.warrants > 0 then print(citizen.name .. ' is wanted') endGetCitizenProfile(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.
local record = exports['koja-mdt']:GetCriminalRecord(identifier)
if record and record.summary.totals.felonies > 0 then print('felon') endUpdateCitizenProfile(identifier, changes, opts?)
Edits the MDT profile. Omitted fields keep their current value. Fires citizen:update.
| Field | Type | Notes |
|---|---|---|
image | string | Photo URL. |
notes | string | Up to 8000 chars, replaces the notes. Use AddCitizenNote to append. |
tags | string[] | Profile flags (Config.ProfileTags ids). |
gallery | string[] | Image URLs. |
fingerprint, dna | string | Up to 32 chars. |
Returns true or nil, errorCode. Errors not_found, no_permission, hook reason.
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.
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.
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.
exports['koja-mdt']:SetVehicleFlag(plate, 'stolen', true)UpdateVehicle(plate, changes, opts?)
Edits the MDT vehicle record. Omitted fields keep their value. Fires vehicle:flags.
| Field | Type | Notes |
|---|---|---|
flags | string[] | Replaces the flag list. Unknown flags are dropped. |
notes | string | Up to 4000 chars. |
points | number | 0 to 99. Reaching Config.Records.vehicleStrikeLimit warns units. |
image | string | URL. |
Returns true or nil, errorCode.
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.
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).
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.
| Field | Type | Notes |
|---|---|---|
title | string | Required, 160 chars. |
type | string | Config.IncidentTypes id, default other. |
status | string | Default open. |
location, narrative | string | |
tags | string[] | Up to 12. |
images | string[] | URLs. |
restricted | boolean | Needs incidents.restricted. |
people | table[] | { identifier, name, role, charges = { { code, count, modifier } }, reduction, plea, notes }, up to 30. Fine, months and points are calculated from the penal code. |
vehicles | table[] | { plate, model, notes }, up to 15. |
officers | table[] | { identifier, name, callsign, department }. A real acting officer is always listed first; System is never listed. |
evidence | number[] | Evidence ids to link. |
officer, department, author | Acting identity. |
Returns the incident or nil, errorCode. Errors title_required, no_permission, not_authorized, hook reason.
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.
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.
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.
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.
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.
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.
| Field | Type | Notes |
|---|---|---|
personId | number | incident.people[].id. |
opts.officer | number | Required. The processing officer (distance checks use their position). |
opts.fine | boolean | false skips the fine. |
opts.jail | boolean | false 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.
local incident, err = exports['koja-mdt']:ProcessSuspect(personId, { officer = source, jail = true })GetIncident(id)
Returns the incident or nil, 'not_found'.
SearchIncidents(filter?)
| Field | Type | Notes |
|---|---|---|
query | string | Title, location, author or person name; #42 matches the id. |
status, type | string | |
unprocessed | boolean | Only reports with unprocessed suspects. |
page | number | 25 per page. |
officer, mine | With officer and mine = true, only that officer's reports. |
Returns { items, page, hasMore }. Items are summaries with involved, charges, fine, months, unprocessed and evidenceCount.
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)
| Field | Type | Notes |
|---|---|---|
type | string | Default arrest. |
name | string | Required. |
reason | string | Required, 4000 chars. |
identifier, plate, address | string | plate is required for vehicle. |
charges | table[] | Penal code entries. |
hours | number | Expiry, default Config.Records.defaultWarrantHours, max 720. |
incidentId | number |
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.
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).
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).
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
endGetWarrant(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.
for _, warrant in ipairs(exports['koja-mdt']:GetPlateWarrants('ABC123')) do
if warrant.status == 'active' then print('vehicle warrant #' .. warrant.id) end
endSearchWarrants(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).
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).
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).
if #exports['koja-mdt']:GetPlateBolos(plate) > 0 then print('BOLO on ' .. plate) endBulletins
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.
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)
| Field | Type | Notes |
|---|---|---|
violations | table[] / string[] | Required. Codes of charges whose class is allowed for citations (Config.Citations.classes), as { code, count } or plain codes. |
identifier | string | Violator; the name is looked up. |
name | string | Used when there is no identifier. |
plate, vehicleModel, location, notes | string | |
coords | table | Defaults to the officer's position when officer is given. |
incidentId | number | |
notify | boolean | false skips the violator notification. |
print, handTo | Only with officer: print = true gives the paper citation item, handTo = 'target' hands it to the nearby violator. | |
officer, author, department | department 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.
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?)
| Field | Type | Notes |
|---|---|---|
opts.charge | boolean | true 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).
if exports.my_bank:RemoveMoney(source, citation.amountDue) then
exports['koja-mdt']:PayCitation(citation.id, { author = 'Maze Bank' })
endVoidCitation(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).
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.
local owed, count = exports['koja-mdt']:GetOutstandingTotal(source)
if owed > 0 then print(('You owe $%d on %d citations'):format(owed, count)) endSearchCitations(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.
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.
exports['koja-mdt']:UpdateEvidence(evidenceId, { status = 'released', custodyNote = 'Returned to owner' })TransferEvidence(id, data, opts?)
| Field | Type | Notes |
|---|---|---|
action | string | transferred, checked_out, returned (back to stored), lab, released, destroyed. |
location | string | New storage location. |
to | string | New holder. transferred needs to or a new location. |
note | string | 255 chars. |
Released or destroyed evidence cannot move. Fires evidence:transfer. Returns the evidence or nil, errorCode (invalid, not_found, hook reason).
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.
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.
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.
local members = exports['koja-mdt']:GetRoster('bcso')
for _, member in ipairs(members or {}) do print(member.callsign, member.name, member.gradeLabel) endHasCertification(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).
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.
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
endCourt
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)
| Field | Type | Notes |
|---|---|---|
title | string | Required. |
defendantName | string | Required. |
defendantIdentifier | string | |
caseType | string | Config.Doj.caseTypes, default criminal. |
incidentId | number | |
judge, prosecutor, defense | string | Names; add judgeIdentifier, prosecutorIdentifier, defenseIdentifier to notify them. |
courtroom, notes | string | |
sealed | boolean |
Fires court:create. Returns the detailed case or nil, errorCode (invalid, hook reason).
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.
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.
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.
| Export | Returns |
|---|---|
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).
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).
| Export | Notes |
|---|---|
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
| Export | Returns |
|---|---|
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 |
local held = exports['koja-mdt']:GetHeldImpound(plate)
if held then print(('Impounded at %s, fee $%d'):format(held.lot, held.fee)) endEMS
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.
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.
| Field | Type | Notes |
|---|---|---|
name | string | Required, patient name. |
identifier | string | Patient. |
status | string | draft or final. |
type | string | Config.Ems.incidentTypes id. |
location, complaint, narrative | string | |
vitals | table[] | { at, hr, sys, dia, spo2, rr, temp, gcs, pain }. |
injuries | table[] | { side = 'front' / 'back', region, kind, severity, note }. |
treatments | table[] | { id } from Config.Ems.catalog or { label, kind, dose, count }. |
crew | table[] | { identifier, name, callsign }. |
transport, hospital, priority, outcome | string | transport: none, ground, air, police, self. |
callId | number | Dispatch 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.
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.
| Export | Who 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.
exports['koja-mdt']:NotifyDepartment('lspd', { title = 'Prison transport arriving', body = 'Mission Row, gate 2 in 5 minutes', kind = 'system' })SendMail(data)
| Field | Type | Notes |
|---|---|---|
from | string | Sender name, default System. |
to | string / table | Identifier, department id, an array of either, or { identifiers = {}, departments = {}, all = true }. |
subject | string | 160 chars. |
body | string | Up to Config.Mail.maxBodyLength. |
department | string | Sender 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)
| Field | Type | Notes |
|---|---|---|
title | string | Required, 120 chars. |
start, end | number | Milliseconds. end defaults to start. |
allDay | boolean | |
description, location | string | |
category | string | training, meeting, operation, patrol, court, social, other. |
coords | table | { x, y, z }. |
audience | string | department (default), division (with divisionId) or all. |
visibility | string | everyone or managers. |
maxAttendees, requireRsvp | ||
recurrence, repeatUntil | none, daily, weekly, biweekly, monthly. | |
officer, department, author | Acting identity. |
Fires calendar:create. Returns { id, start } or nil, errorCode (invalid, no_permission, hook reason).
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.
| Field | Type | Notes |
|---|---|---|
plate | string | Required. |
source | string | camera (default) or vehicle. |
cameraId, cameraLabel | Your camera id and label. | |
coords, street, model | ||
officer | number | The officer whose reader saw the plate. |
department | string | Owning 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).
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 }.
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
| Export | Notes |
|---|---|
registerHook(name, fn, opts) โ hookId | See hooks. Returns nil, 'unknown_hook' / nil, 'invalid_function'. |
removeHook(hookId) โ boolean | Only the registering resource can remove its hook. |
GetHookNames() โ string[] | Every hook name. |
Cameras and bodycams
| Export | Returns |
|---|---|
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.
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.
| Field | Type | Notes |
|---|---|---|
identifier | string | Wearer. |
department | string | |
incidentId | number | Sessions linked to the incident. |
from, to | string | Dates, YYYY-MM-DD. |
shots | boolean | Only sessions with shots fired. |
limit, offset | number | Paging. |
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.
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.
local recording = exports['koja-mdt']:GetBodycamRecording(sessionId)
if recording then
local first = exports['koja-mdt']:GetBodycamRecordingFrame(sessionId, recording.index[1].seq)
endPlayback 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.
local storage = exports['koja-mdt']:GetStorageStatus()
if not storage.ok then print('MDT storage problem: ' .. tostring(storage.message)) end