# Examples

Complete recipes built only from the documented [server exports](/new/mdt/api/server-exports), [client exports](/new/mdt/api/client-exports), [hooks](/new/mdt/api/hooks) and [events](/new/mdt/api/events).

:::hint{type="danger" title="Validate on the server"}
Every recipe that reacts to a client event checks the player's position and rate limits on the server before it touches the MDT. Never let a client decide the content of a call, a citation or a sentence.
:::

## Robbery script creating a dispatch call

:::steps
### Register the call type

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

CreateThread(function()
    mdt:RegisterDispatchCode('ROB-STORE', {
        title = 'Store robbery', priority = 1, types = { 'leo' },
        blip = { sprite = 52, color = 1, scale = 1.1 },
    })
end)
```

### Create the call when a validated robbery starts

```lua title="my-robberies/server.lua"
local active = {}

RegisterNetEvent('my-robberies:server:start', function(storeId)
    local src = source
    local store = Config.Stores[storeId]
    if not store or active[storeId] then return end
    if #(GetEntityCoords(GetPlayerPed(src)) - store.coords) > 15.0 then return end
    if mdt:CountOnDuty({ type = 'leo' }) < store.minPolice then return end
    active[storeId] = mdt:CreateDispatch({
        code = 'ROB-STORE',
        message = ('Silent alarm at %s'):format(store.label),
        coords = store.coords,
        caller = { name = 'Alarm company' },
        source = src,
    })
end)
```

### Update and close it

```lua title="my-robberies/server.lua"
RegisterNetEvent('my-robberies:server:safeOpened', function(storeId)
    local callId = active[storeId]
    if not callId then return end
    mdt:UpdateDispatch(callId, { message = 'Safe opened, suspects leaving' })
    mdt:AddDispatchNote(callId, 'Camera shows 2 suspects, masks', 'Store CCTV')
end)

RegisterNetEvent('my-robberies:server:finished', function(storeId)
    local callId = active[storeId]
    active[storeId] = nil
    SetTimeout(10 * 60 * 1000, function()
        local call = callId and mdt:GetCall(callId)
        if call and call.status == 'active' and #call.units == 0 then mdt:CloseDispatch(callId) end
    end)
end)
```
:::

## Speed camera issuing citations

```lua title="my-speedcams/server.lua"
local mdt = exports['koja-mdt']
local flashed = {}

RegisterNetEvent('my-speedcams:server:flash', function(cameraId, plate, speed)
    local src = source
    local camera = Config.Cameras[cameraId]
    if not camera or type(plate) ~= 'string' or type(speed) ~= 'number' then return end
    if #(GetEntityCoords(GetPlayerPed(src)) - camera.coords) > 60.0 then return end
    local key = plate .. ':' .. cameraId
    if flashed[key] and os.time() - flashed[key] < 60 then return end
    flashed[key] = os.time()

    local vehicle = mdt:GetVehicle(plate)
    mdt:ReportAnprRead({ plate = plate, cameraLabel = camera.label, coords = camera.coords, department = 'sahp' })
    if not vehicle or vehicle.unregistered then return end

    local citation, err = mdt:IssueCitation({
        identifier = vehicle.owner,
        plate = vehicle.plate,
        vehicleModel = vehicle.model,
        violations = { speed - camera.limit > 35 and 'V.C. 7002' or 'V.C. 7001' },
        location = camera.label,
        coords = camera.coords,
        notes = ('Speed camera: %d in a %d zone'):format(math.floor(speed), camera.limit),
        department = 'sahp',
        author = 'Speed Camera ' .. cameraId,
    })
    if not citation then print('citation failed: ' .. tostring(err)) return end

    mdt:UpdateVehicle(vehicle.plate, { points = (vehicle.points or 0) + 1 })
end)
```

`ReportAnprRead` logs the plate only when it is a hit (stolen, BOLO, warrant), and with `Config.Anpr.autoDispatch` raises a call for SAHP. The citation reaches the owner's notification center and citation list; license points are applied by the MDT.

## Jail script reporting sentences

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

AddEventHandler('koja-mdt:server:jailPlayer', function(target, months, reason, officerSrc)
    StartSentence(target, months, reason)
end)

AddEventHandler('koja-mdt:server:releasePlayer', function(target, identifier, reason)
    if target then EndSentence(target) end
end)

function JailFromJudgeNpc(src, months, reason)
    StartSentence(src, months, reason)
    mdt:ReportJail(src, months, { reason = reason, facility = 'Bolingbroke', officer = 'Judge NPC' })
end

function OnSentenceServed(src)
    mdt:ReportRelease(src, 'released', { by = 'Bolingbroke' })
end

function OnEscape(src)
    mdt:ReportRelease(src, 'escaped', { by = 'Bolingbroke' })
    local coords = GetEntityCoords(GetPlayerPed(src))
    mdt:CreateDispatch({ code = '10-98', title = 'Prison escape', coords = coords, priority = 1, types = { 'leo' } })
end

RegisterNetEvent('my-prison:server:remaining', function()
    local src = source
    local sentence = mdt:GetActiveSentence(src)
    TriggerClientEvent('my-prison:client:remaining', src, sentence and sentence.secondsLeft or 0)
end)
```

Sentences from MDT processing already create the jail record; only report the ones your script starts on its own, and always report the end of a sentence. See [integrations](/new/mdt/api/integrations) for writing a jail adapter instead.

## Phone app reading warrants and citations

```lua title="my-phone/server.lua"
local mdt = exports['koja-mdt']
local lastRequest = {}

RegisterNetEvent('my-phone:server:records', function()
    local src = source
    if lastRequest[src] and os.time() - lastRequest[src] < 5 then return end
    lastRequest[src] = os.time()

    local warrants = {}
    for _, warrant in ipairs(mdt:GetCitizenWarrants(src)) do
        if warrant.status == 'active' then
            warrants[#warrants + 1] = { id = warrant.id, type = warrant.type, reason = warrant.reason, expiresAt = warrant.expiresAt }
        end
    end
    local owed, count = mdt:GetOutstandingTotal(src)

    TriggerClientEvent('my-phone:client:records', src, {
        warrants = warrants,
        citations = mdt:GetUnpaidCitations(src),
        owed = owed,
        openCitations = count,
    })
end)

RegisterNetEvent('my-phone:server:payCitation', function(citationId)
    local src = source
    if type(citationId) ~= 'number' then return end
    local own
    for _, citation in ipairs(mdt:GetUnpaidCitations(src)) do
        if citation.id == citationId then own = citation end
    end
    if not own then return end
    local paid, err = mdt:PayCitation(own.id, { charge = true })
    TriggerClientEvent('my-phone:client:paid', src, paid ~= nil, err)
end)

AddEventHandler('playerDropped', function() lastRequest[source] = nil end)
```

`PayCitation` with `charge = true` takes the money through the MDT fine integration. Only act on a player's own records: the pay handler only accepts a citation id from the caller's own unpaid list.

## Garage checking impounds

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

RegisterNetEvent('my-garage:server:takeOut', function(plate)
    local src = source
    if type(plate) ~= 'string' then return end
    local held = mdt:GetHeldImpound(plate)
    if held then
        TriggerClientEvent('my-garage:client:notify', src, ('Impounded at %s. Fee $%d.'):format(held.lot, held.fee))
        return
    end
    if mdt:HasVehicleFlag(plate, 'stolen') then
        TriggerClientEvent('my-garage:client:notify', src, 'This vehicle is reported stolen.')
        return
    end
    SpawnVehicle(src, plate)
end)

RegisterNetEvent('my-garage:server:payImpound', function(plate)
    local src = source
    local held = type(plate) == 'string' and mdt:GetHeldImpound(plate)
    if not held or held.holdLeft > 0 then return end
    local record, err = mdt:ReleaseImpound(plate, { feeStatus = 'charge', note = 'Paid at impound lot' })
    TriggerClientEvent('my-garage:client:notify', src, record and 'Vehicle released' or ('Release failed: ' .. tostring(err)))
end)

AddEventHandler('my-tow:server:towed', function(plate, reason)
    mdt:ReportImpound(plate, { reason = reason, fee = 350, hours = 0, lot = 'davis' })
end)
```

`ReleaseImpound` with `feeStatus = 'charge'` takes the fee from the online owner; `ReportImpound` records a tow your script already performed.

## Rules as hooks

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

mdt:registerHook('citation:create', function(payload)
    if payload.total > 5000 then return false, 'over_limit' end
end, { stage = 'before' })

mdt:registerHook('incident:process', function(payload)
    if payload.months > 60 then return { months = 60 } end
end, { stage = 'before' })

mdt:registerHook('mdt:open', function(payload)
    return exports.ox_inventory:Search(payload.source, 'count', 'mdt_tablet') > 0
end, { stage = 'before' })

AddEventHandler('koja-mdt:server:dutyChanged', function(src, onDuty, department)
    if src then exports['my-hud']:SetBadge(src, onDuty and department or nil) end
end)
```
