# Integrations

Every integration below is optional and switches itself on when the other resource runs. They are configured on the **Integrations** and **Modules** pages of `/dispatchadmin`.

## koja-mdt

Works automatically when `koja-mdt` is started and `mdt.enabled` is on. Nothing has to be changed in the MDT. The **Integrations** page shows whether koja-mdt is not installed, running or installed but not started. The resource name is `Config.Mdt.resource`.

### Two-way sync

| Direction | What happens | Setting |
|---|---|---|
| Dispatch to MDT | Incidents appear in the MDT call list with code `911` (`Config.Mdt.code`), category, note, street, priority and the needed department types. They close in the MDT when they end here. | `mdt.pushIncidents`: `off`, `real` (from players, API, manual, sensors and the MDT), `all` (also generated calls) |
| MDT to dispatch | MDT calls (911 from the MDT, panic, ALPR, `CreateDispatch` from other scripts) ring at the console or become incidents. Only while a dispatcher is on shift; otherwise they stay MDT-only. Closing them in the MDT resolves them here. | `mdt.pullCalls`: `off`, `calls`, `incidents` |
| Assignments | Assigning a player unit attaches them to the MDT call; attaching or detaching in the MDT assigns or releases them here. | `mdt.syncStatus` |
| Unit status | Status changes sync both ways (available, busy, break, en route, on scene). | `mdt.syncStatus` |
| Duty | Player units follow MDT duty: an officer off duty in the MDT is not a unit in the CAD. | `mdt.enabled` |
| Callsigns | Player units show their MDT callsign. | `mdt.useCallsigns` |
| No dispatcher | `mode.noDispatcher = 'mdt'` sends `/911`, phone calls and `CreateCall` straight to the MDT. | `mode.noDispatcher` |

### 10-codes

Field units set their status with the field status codes (key mappings, `/tencode`, quick menu). With koja-mdt the status goes to the MDT too, `backup` also creates a `10-78` MDT call with the officer attached, and `10-99` calls the MDT panic. Officers who are on duty in the MDT can use the codes even when they are not a CAD unit. Without koja-mdt, `10-99` creates a priority 1 incident here (`mdt_status.panicFallback`). See [Commands and keybinds](/new/dispatcher/commands-and-keybinds#field-status-codes).

### BOLO

BOLOs are created in the CAD's MDT workspace (key `B`). Other resources can create and close them with the server exports `CreateBolo` and `CloseBolo` (see [Server exports](/new/dispatcher/api/server-exports)); they follow the same source rules.

| `mdt_tools.boloSource` | Behaviour |
|---|---|
| `auto` | koja-mdt when it runs, the internal list otherwise. |
| `mdt` | Always koja-mdt. New BOLOs are refused while it is down. |
| `internal` | Only the internal list in `koja_dispatch_bolos`. |

With koja-mdt, BOLOs are created and listed in the MDT, so MDT and CAD share one list; location, colour and the linked incident are kept in `koja_dispatch_bolos`. Internal BOLOs are sent to on-duty police field units as a notification (`mdt_tools.notifyUnits`). An ALPR hit in koja-mdt on a BOLO plate alerts the dispatchers (`mdt_tools.alpr`).

### MDT workspace

With koja-mdt running the workspace also has person search (flags, warrants, record, licenses), plate lookup with stolen and wanted flags (`Config.MdtTools.flags`), call notes on the linked MDT call, bulletins, mail and the list of officers on duty. Without koja-mdt only the internal BOLO list is available. The drawer is switched with `mdt_tools.monitor`.

### Settings

| Key | Default | |
|---|---|---|
| `mdt.enabled` | on | The whole integration. |
| `mdt.pushIncidents` | `real` | Incidents sent to the MDT. |
| `mdt.pullCalls` | `calls` | MDT calls taken by the CAD. |
| `mdt.syncStatus` | on | Status and assignments. |
| `mdt.useCallsigns` | on | MDT callsigns. |
| `mdt_status.*` | | Field codes, HUD chip, backup units, cooldown, panic fallback. |
| `mdt_tools.*` | | Workspace, BOLO source, ALPR alerts, unit notifications. |

## Voice

When a dispatcher answers a 911 call from a real player (`/911` or a phone call), the caller and the dispatcher are put on a private call channel. Without a voice resource, or with `voice.enabled` off, the call stays text. Generated callers are never on voice.

| Resource | How |
|---|---|
| pma-voice | `exports['pma-voice']:setPlayerCall(source, channel)`. Call channels must be enabled in pma-voice (`voice_enableCalls`, on by default). |
| mumble-voip | `SetCallChannel` on the clients. |

`voice.provider = 'auto'` uses pma-voice first, then mumble-voip. Channels are `voice.channelBase` (default `91100`) plus 0 to 899 (`Config.Voice.channelRange`); keep that range away from your phone script's call channels.

In the CAD the dispatcher can mute, hold, transfer the voice call to an on-duty player unit (`voice.allowTransfer`) and end it. With `voice.endOnDispatch` on the voice call ends when the call is dispatched; off, the caller stays on the line until the dispatcher ends it. Callers and units leave with `/hangup911`. Developers can open and close voice with the exports `ConnectVoice`, `DisconnectVoice` and `GetVoiceSession` (see [Server exports](/new/dispatcher/api/server-exports)).

## Phones

Calls and texts to the service number reach the dispatcher like `/911`: the same modes, fallbacks, follow-up messages and replies. With a supported phone, dispatcher replies are sent back as messages from the service number for 20 minutes (`Config.Phones.threadMinutes`); otherwise they arrive as normal notifications.

| Setting | Default | |
|---|---|---|
| `phones.enabled` | on | Phone 911, including the generic entry points. |
| `phones.resource` | `auto` | `auto` uses the first running phone in `Config.Phones.order`, or force one. |
| `phones.number` | `911` | The number players dial or text. |

| Phone | Status | In | Replies |
|---|---|---|---|
| lb-phone | Built-in adapter, verified | A phone call to the service number or to a company in `Config.Phones.companies` (police, sheriff, ambulance, ems, fire, lspd, bcso, safd); a company app message; a text to the service number. The phone call is ended and a text thread opens. | Messages from the service number |
| npwd | Built-in adapter, verified | A phone call or text to the service number (`exports.npwd:onCall` / `onMessage`). The call gets an automatic reply and ends. | Messages from the service number |
| qs-smartphone-pro | Built-in adapter, not verified in game | The job alert events `qs-smartphone:server:sendJobAlert` and `qs-smartphone-pro:server:sendJobAlert`. | Messages through `sendNewMessageFromApp` |
| gksphone | No adapter; use the generic entry points | | Notifications |
| yseries | No adapter; use the generic entry points | | Notifications |

**Generic entry points** for any phone or app. They only need `phones.enabled`; no supported phone has to run. Put one of them in the phone's emergency number or service app config:

```lua title="client side"
TriggerServerEvent('koja-dispatcher:phone:call', 'text of the call')
```

```lua title="server side"
exports['koja-dispatcher']:PhoneCall(source, 'text of the call')
```

## Dispatch script compatibility

Many scripts (robberies, drug sales, car theft) send alerts to a dispatch script. koja-dispatcher understands the alerts of the common ones, so those scripts work without changes. Alerts are mapped to a category, a priority (1-3) and needs (jobs containing police or sheriff, ambulance or ems, fire pick the departments; otherwise defaults per category).

| Setting | Default | |
|---|---|---|
| `compat.enabled` | on | Accept the alerts. |
| `compat.mode` | `call` | `call` rings at the console through the normal router (so modes and fallbacks apply). `incident` opens an incident straight away. |
| `compat.autoAssign` | on | Incidents only: send the nearest free units. |

The same alert at the same spot within 20 seconds is dropped (`Config.Compat.dedupeSeconds`), players are rate limited (`Config.Compat.rate`), and positions more than 600 m from the sending player are moved to the player (`Config.Compat.maxDistance`).

### Events

These events are handled only while the original resource is **not** running (a shim counts as not running), so nothing is doubled if you keep the original script.

| Script | Event |
|---|---|
| cd_dispatch | `cd_dispatch:AddNotification` |
| ps-dispatch | `ps-dispatch:server:notify` |
| rcore_dispatch | `rcore_dispatch:server:sendAlert` |
| qs-dispatch | `qs-dispatch:server:CreateDispatchCall` |
| core_dispatch | `core_dispatch:addCall` |
| linden_outlawalert | `wf-alerts:svNotify`, `wf-alerts:svNotify911` |

Client-side triggers of `ps-dispatch:client:notify`, `cd_dispatch:AddNotification`, `rcore_dispatch:server:sendAlert` and `qs-dispatch:server:CreateDispatchCall` are forwarded too.

### Shim resources

Scripts that call **exports** of a dispatch script need that resource name to exist. The release contains shims in `koja-dispatcher/compat/` that `provide` the original name and forward to koja-dispatcher:

| Shim | Exports |
|---|---|
| `ps-dispatch` | `CustomAlert`, the preset alerts (`Shooting`, `StoreRobbery`, `FleecaBankRobbery`, `OfficerDown`, `OfficerBackup`, `VehicleTheft`, `CarJacking`, `DrugSale`, `Fight`, `InjuredPerson`, `SpeedingVehicle`...), `SendTargetedAlert`, `GetVehicleData` (client); `CustomAlert` (server) |
| `cd_dispatch` | `GetPlayerInfo`, `GetVehicleData` (client) |
| `core_dispatch` | `sendAlert`, `addCall` (client and server) |
| `qs-dispatch` | `getSkinData`, `GetPlayerInfo` (client) |

Install a shim:

1. Stop and remove the original dispatch resource.
2. Copy the folder from `koja-dispatcher/compat/<name>` into your resources. Keep the folder name (for example `ps-dispatch`).
3. Add `ensure <name>` after `ensure koja-dispatcher`.

Other scripts can also send any of these formats with `exports['koja-dispatcher']:CompatAlert(name, data)`.

## Discord logs

Webhook URLs are read only from server convars, at send time. They are never sent to clients or printed. Use `set`, not `setr`. A URL must start with `https://`.

```cfg title="server.cfg"
set koja_dispatcher_webhook_shifts "https://discord.com/api/webhooks/..."
set koja_dispatcher_webhook_incidents "https://discord.com/api/webhooks/..."
set koja_dispatcher_webhook_admin "https://discord.com/api/webhooks/..."
set koja_dispatcher_webhook_default "https://discord.com/api/webhooks/..."
```

A channel without its own URL uses `default`. With no URL at all nothing is sent.

| Channel | What is logged | Setting |
|---|---|---|
| `shifts` | Shift started (dispatcher, station) and ended (duration, calls, resolved, failed, missed, score, rating, pay). | `discord.shifts` |
| `incidents` | Ended incidents: category, priority, outcome, location, response time, duration, dispatcher, units. Only priorities up to `discord.minPriority`. | `discord.incidents` |
| `incidents` | Missed calls (caller, location, how long it rang) and dropped calls (and who hung up). | `discord.calls` |
| `incidents` | Pursuit started and ended, mass casualty incident started and ended. | `discord.events` |
| `admin` | Settings changed (key, value, previous), stations, jobs and hires. | `discord.admin` |

Embeds are queued and sent in batches of up to 10 every 2 seconds per webhook (`Config.Discord.interval`); a rate limit answer from Discord pauses that webhook. Other resources can post through the same queue with `exports['koja-dispatcher']:DiscordLog(channel, title, description, fields, color)`.

## Own code and other resources

- **Inside the resource:** put your own Lua files in `server/custom/` and `client/custom/`. The manifest loads `server/custom/*.lua` and `client/custom/*.lua` after all built-in files, so you can use `KOJA.Server.*`, `Config` and the hooks there. Keep these folders when you update.
- **From other resources:** use the exports, hooks and events in the [Developer API](/new/dispatcher/api/overview). Before hooks (`callCreate`, `incidentCreate`, `unitAssign`, `shiftStart`) can return `false, 'reason'`; the reason is passed on to the caller instead of a generic `cancelled`. See [Hooks](/new/dispatcher/api/hooks).
- **Own settings:** `exports['koja-dispatcher']:RegisterSetting(key, kind, default, options)` adds a setting that is saved and edited in the admin menu's Modules page like the built-in ones. See [Server exports](/new/dispatcher/api/server-exports).

## Framework bridge

koja-dispatcher talks to the framework only through koja-lib, so ESX, QBCore and Qbox work the same. The calls are collected in `server/bridge.lua` (not escrowed) if you need to change one:

| Function | Used for |
|---|---|
| `Bridge.Job` | Job name, grade and duty of a player (koja-lib `GetJobInfo`). |
| `Bridge.Department` | Which department a player belongs to, from `Config.Departments[].jobs`, and only while on duty. |
| `Bridge.CanDispatch` | Job and minimum grade from the allowed list. Also decides who sees the station prompt. |
| `Bridge.SetDuty` | Framework duty on and off with the shift (`Config.Dispatcher.setDuty`). |
| `Bridge.Pay` | Shift pay into `Config.Pay.account`. |
| `Bridge.Notify` | Notifications through koja-lib. |
| `Bridge.Jobs`, `Bridge.SetJob` | The job list and hiring in the admin menu. |
| `Bridge.CreateJob` | Creating a job from the admin menu. |

### Creating a job

**Create job** in the admin menu works on ESX and QBCore:

- **ESX:** `ESX.CreateJob(name, label, grades)` with each grade's name, label and salary.
- **QBCore:** `exports['qb-core']:AddJob(name, job)` with `defaultDuty = true` and the highest grade as boss.

The new job is added to the jobs allowed to dispatch with the minimum grade you picked. On other frameworks the menu says creating jobs is not supported; add the job in your framework and allow it on the **Access & jobs** page.

:::hint{type="info" title="QBCore jobs"}
Depending on your qb-core version, `AddJob` may only add the job while the server runs. If the job is gone after a restart, add it to `qb-core/shared/jobs.lua` as well.
:::

### Target

The station start uses koja-lib's target bridge (`ox_target`, `koja-interactions` or `qb-target`). Without a target resource players get a text prompt and press `E`. Either way, only players whose job may dispatch see it. The startup banner shows which target was found.
