# Server exports

All exports on this page are called from **server** scripts:

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

Read the [overview](/new/mdt/api/overview) first: it explains the `opts` table (`officer`, `department`, `author`), the `result, errorCode` convention, `who` parameters and the shared shapes (officer, dispatch call).

:::hint{type="info" title="Everything goes through the MDT pipeline"}
Write exports call the same action handlers as the tablet. Permissions (when `officer` is given), audit logs, Discord webhooks, [hooks](/new/mdt/api/hooks) and live tablet refresh all apply.
:::

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

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

:::hint{type="warning" title="Advanced"}
Payloads are not documented per action and can change between versions. Prefer the named exports below.
:::

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

```lua title="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).

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

```lua title="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`.

| 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](/new/mdt/api/overview) or `nil, errorCode`. **Errors** `not_found`, or the reason a `before` hook returned.

```lua title="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`.

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

```lua title="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`.

```lua title="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`.

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

### `GetCall(callId)`

**Returns** the [call](/new/mdt/api/overview) 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.

```lua title="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`.

```lua title="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.

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

```lua title="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):

```lua
{
    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`.

```lua title="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.

```lua title="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`.

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

```lua title="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`.

```lua title="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.

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

---

## Vehicles

Vehicle shape:

```lua
{
    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`.

```lua title="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`.

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

```lua title="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`.

```lua title="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).

```lua title="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):

```lua
{
    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.

```lua title="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.

```lua title="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.

```lua title="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`.

```lua title="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`.

```lua title="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`.

```lua title="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`.

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

```lua title="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?)`

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

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

---

## Warrants

Warrant shape:

```lua
{
    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.

```lua title="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).

```lua title="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).

```lua title="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.

```lua title="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).

```lua title="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).

```lua title="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).

```lua title="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`.

```lua title="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:

```lua
{
    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.

```lua title="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?)`

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

```lua title="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).

```lua title="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.

```lua title="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`.

```lua title="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`.

```lua title="server.lua"
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).

```lua title="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](/new/mdt/api/overview). 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`.

```lua title="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.

```lua title="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.

```lua title="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`).

```lua title="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](/new/mdt/api/overview).

```lua title="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:

```lua
{
    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).

```lua title="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.

```lua title="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`.

```lua title="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:

```lua
{
    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](/new/mdt/api/events)).

```lua title="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](/new/mdt/api/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 |

```lua title="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`.

```lua title="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`.

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

```lua title="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`.

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

```lua title="server.lua"
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).

```lua title="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:

```lua
{
    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).

```lua title="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 }`.

```lua title="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

| Export | Notes |
|---|---|
| `registerHook(name, fn, opts)` → `hookId` | See [hooks](/new/mdt/api/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.

```lua title="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](/new/mdt/storage)). 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.

:::hint{type="warning" title="Requirements"}
`screenshot-basic` must be started. Debug fake officers do not produce footage because no client captures their view. Recording settings live in `Config.Bodycams.recording` (see the configuration 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.

```lua title="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.

```lua title="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](/new/mdt/api/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.

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