MCPTT Mission Critical Push-to-Talk Service sealMCPTT.comMission Critical Push-to-Talk ServiceDeveloper documentation

Dispatcher developer guide

For a developer who wants to build a dispatcher console: a desktop or web program that talks and listens on talkgroups, shows who is talking, handles emergencies, shows locations, makes private calls and call alerts, sends messages, monitors radios remotely and plays recordings. Read README.md first for the system overview and glossary.

Status: MCPTT does not ship a dispatcher console yet. Radios and phones run in production on the voice protocol a console uses. This guide says what you can build on today and where you will hit gaps. Anything marked "unverified" could not be confirmed and may change.

Embed the MCPTT client SDK (the Rust client core every radio and phone app uses) in a desktop app, for example with Tauri. Use the event stream (events-websocket.md) for the fleet-wide view and for dispatcher actions (clear an emergency, request a location, send a call alert, remote monitoring). Use the management REST API (management-rest.md) for lookups and remote monitoring.

Do not implement the voice protocol yourself unless you must. It is large (two sites at once, de-duplication, forward error correction, gap repair, catch-up hold), and every radio and phone already uses the SDK's implementation. A browser-only console that runs the SDK is not possible yet: there is no WebAssembly or WebTransport build. A web UI with a small local service that runs the SDK and serves it on localhost works the same way as a desktop shell.

2. Architecture

+------------------------------- desktop app ------------------------------+
|  UI (any framework)  <------------------------->  native backend         |
|                                                   client SDK (per seat)  |
|                                                   audio I/O              |
|                                                   event stream client    |
|                                                   REST client            |
+--------------------------------------------------------------------------+
      | QUIC, both sites                   | wss events.locN    | https /api/v1
      v                                    v                    v
   loc1 site  <------------------------->  loc2 site
  • From Rust you link the SDK as a normal library. Kotlin and Swift bindings exist for mobile apps.
  • The SDK does not own audio devices. The app captures PCM and calls RadioCore::push_capture(pcm), and pulls playout with RadioCore::pull_playout() on its audio clock (16 kHz mono, 20 ms frames, PCM16). Device selection, outputs and mixing across seats are yours.

3. Voice

3.1 Connecting

RadioCore::new(settings, listener, device_key) starts the runtime and connects to both sites. RadioSettings carries the server addresses, the pinned server certificate SHA-256 (clients pin the MCPTT certificate authority, not the public web roots), the device identity, and one of the sign-in forms: badge and PIN, account email and password, or a one-time enrollment code. device_key is the TLS client key. Events reach you through the RadioListener you pass in, plus optional listeners for calls, emergencies, messaging, transcripts and pictures (set_*_listener).

On the wire this is QUIC v1, TLS 1.3, a hello exchange that agrees optional features (capability bits), then a login.

3.2 Codec

Opus, wideband, 16 kHz mono, 20 ms frames on normal links (40 or 60 ms on constrained links). Every frame arrives over both sites and the SDK de-duplicates them.

3.3 Floor

StepSDK
Choose the talkgroupset_talkgroup(id)
Press PTTptt_down(); capture starts at the press and is buffered until the grant
Granted, denied or queuedlistener callback (a denial carries a cause; a queue carries a position)
Releaseptt_up()
Someone else talkslistener callback with the talker's callsign and unit number, then idle
Pre-emptedlistener callback
Catch-up hold ("Rx pending")listener callback

Priority: an emergency talker outranks everyone; otherwise the callsign's talk level (-10 to +10) decides, and a higher priority pre-empts after a short grace period (200 ms by default, none for an emergency). Console-only floor rights (all-call, broadcast, emergency override) need a dispatcher sign-in, which is not offered yet, so a console today has a radio's floor rights.

3.4 Emergency

  • A voice session that supports it receives emergency alerts for its talkgroups through set_emergency_listener and emergency_alerts().
  • Fleet-wide, the event stream publishes emergency.* events (scope events:emergency; see the event stream guide, section 5).
  • A console clears an emergency with the event-stream command emergency.clear (scope emergency:clear, a reason of at least 10 characters). It is cleared as dispatcher, as from the portal. There is no acknowledge step yet; emergency.ack is refused as unavailable.
  • The SDK's clear_emergency clears the session's own callsign's emergency, which is the radio case, not the dispatcher case.
  • For the position of the unit in emergency, use the event stream's location.position (section 5).

3.5 Private calls and call alerts

  • SDK: start_private_call(target, confirmed), answer_private_call(accept), end_private_call(), private_call(), send_call_alert(target, text), ack_call_alert(id).
  • The SDK holds one private call at a time, as a radio does. Several calls at once, calls to dispatch positions, radio check and break-in are not offered.
  • A console or CAD can also send a call alert without a voice session: the event-stream command call_alert.send (scope calls:alert).
  • A call or call alert to a unit in a covert or silent emergency is refused.

3.6 Listening to many talkgroups

The SDK models one radio: one selected talkgroup plus scan (set_scan, set_scan_overlap(Mix)), with one playout stream. Until there is a console core, the options are:

  1. One RadioCore per audio "seat" (for example per talkgroup or per output), each a separate device identity with its own callsign sign-in, mixed by your app. Costs one device record and two connections per seat (unverified at 20 seats).
  2. One RadioCore with scan in mix mode, accepting one mixed output and no per-group routing.

3.7 Patch, multi-select, all-call, tone-out

Not offered to consoles.

3.8 Messages

The SDK implements messaging (conversations, send_message, attachments) for sessions that support it. There is no event-stream command to send a message.

3.9 Recordings and replay

  • Every burst is recorded on the server; the raw stream is the record.
  • From an SDK session: download(transmission_id) and transcript(transmission_id), plus local history of what the session heard (list_history, play_history, seek_history).
  • Instant recall of recent bursts is yours to build on top of pull_playout or history.
  • Recording search, playback links and export are not in the REST API yet. The portal's transmissions page is the current way to find and play a recording.

4. Who is talking, fleet-wide

Inside a voice session you only learn about talkgroups the session is affiliated to. For the whole agency, subscribe to the event stream: floor.granted (talkgroup, callsign, device, transmission_id), floor.released, floor.idle, with last_value: true to get the current talker per talkgroup on connect.

5. Locations

Event stream topic location (location.position, the newest fix per batch), with a token that holds both events:location and locations:read. Each connection that can receive positions is audited. To ask units for a position now, send the event-stream command location.request (scope locations:request, one to 16 callsigns, a reason); the fixes arrive as location.position. Location history and geofences are not offered to integrations.

6. Remote monitoring

A console can listen to, and with a camera watch, a radio without its user pressing PTT:

  1. Start a session with POST /api/v1/monitoring/sessions (scope monitoring:control, a reason of at least 10 characters) or the event-stream command monitoring.start.
  2. Watch it: ask for a viewer token (POST /api/v1/monitoring/sessions/{id}/viewer-token, scope monitoring:view, or the command monitoring.viewer_token) and open the viewer WebSocket from your backend; or embed the view_url page that the REST start answer returns.
  3. Follow its state with the monitoring.* events (scope events:monitoring).
  4. Stop it with POST /api/v1/monitoring/sessions/{id}/stop or the command monitoring.stop. Recorded segments stay available through GET /api/v1/monitoring/sessions/{id}/segments.

Every start, stop and viewer is audited. Details and the viewer protocol: management-rest.md, Monitoring.

7. Authentication for a console

What exists today:

  • Voice sessions authenticate as devices: an enrolled device key, then a login with a badge and PIN or an account email and password. A console today is enrolled like a phone or radio and signs in to a callsign. It then has a radio's rights, not a dispatcher's.
  • Event stream: per-site event tokens created by an agency admin in the portal.
  • Management API: OAuth 2.0 client credentials at /oauth/token. An agency administrator or the operator creates an API client and gives you its client id and secret.

Planned, not offered: dispatcher sign-in with OpenID Connect and MFA, with dispatcher scopes and floor rights.

Never log tokens, PINs, passwords, audio, or location and recording content.

8. Available today and planned

"Today" means a console can use it now through the SDK, the event stream or the REST API.

FeatureTodayNotes
Group PTT, grant, deny, queue, pre-emptionYes, through the SDK (radio rights)Dispatcher floor rights not offered
Talker IDYesSDK for affiliated talkgroups; event stream floor.granted for all
Catch-up hold indicatorYes, through the SDK
Multi-select PTT, all-call, announcement, tone-outNo
20 talkgroups audible with per-group routingNoThe SDK has one output; see section 3.6
Emergency list and alertsYesEvent stream emergency.*; SDK emergency alerts for the session's talkgroups
Emergency clear by a dispatcherYesEvent-stream command emergency.clear
Emergency acknowledgeNo
Private call (one at a time), call alertYes, through the SDKCall alert also by event-stream command
Several private calls, dispatch positions, radio check, break-inNo
Remote monitoring (audio, camera, covert)YesREST /monitoring/sessions and event-stream commands; section 6
Presence: battery, accessory, channel, signalYesEvent stream presence.*, network.signal
Lock a userYesREST PUT /users/{id}/lock
Set a statusNo
Stun and revive a devicePortal only
Live call listBuild itFrom floor.* events
Instant recallBuild itFrom SDK history or your own buffer
Recording download, transcript of a burstYes, through the SDKLive captions through set_caption_listener
Recording search, playback link, exportNoPortal only
Live positions on a mapYesEvent stream location.position
Location requestYesEvent-stream command location.request
Track playback, geofencesNoPortal map meanwhile
Patches, dynamic regroup, force selectNo
MessagingYes, through the SDKNo event-stream command
Dispatcher sign-in with OIDC and MFANo
Two sites at onceYes, through the SDK
Browser-only buildNo
Keyboard, USB or footswitch PTTYours to buildCall ptt_down / ptt_up

9. First console in a day

Goal: a desktop window that signs in, talks and listens on one talkgroup, shows who is talking across the agency, shows units on a map, and sends a call alert.

  1. Get access (before the day). From the agency admin: a device record for your console with a callsign that is a member of a test talkgroup (with talk right); its sign-in credentials or an enrollment code; the pinned server certificate hash and the voice address of both sites; an event token from each site with events:floor, events:presence, events:location, locations:read, events:emergency, emergency:clear, calls:alert; an API client with talkgroups:read users:read.
  2. Scaffold. A desktop shell (for example Tauri), the client SDK, an audio library, a WebSocket client for the event stream and an HTTP client for REST.
  3. Audio loop. Open a 16 kHz mono input and output (or 48 kHz and resample). Every 20 ms: feed captured PCM16 to push_capture while is_capturing(); write pull_playout() to the output.
  4. Voice session. Build RadioSettings (both sites, pin, device identity, sign-in), implement RadioListener to forward state to the UI, call RadioCore::new, then set_talkgroup. Map a key and an on-screen button to ptt_down / ptt_up. Show grant, deny, and the current talker.
  5. Talkgroup and callsign names. GET /api/v1/talkgroups and GET /api/v1/users?kind=callsign with the client-credentials token; cache them.
  6. Who is talking, everywhere. Connect to wss://events.loc1.server.mcptt.com/v1/stream, send hello with the token, subscribe to floor with last_value: true; answer heartbeats. Render a talkgroup list with the current talker. On disconnect, reconnect with resume_from_seq; on system.resync_required, take the snapshot again.
  7. Map. Subscribe to location; draw location.position markers; grey out fixes older than 60 s.
  8. Call alert and private call. Add a callsign picker (search_contacts / lookup_directory from the SDK) and buttons for send_call_alert and start_private_call(target, false); show call state from the call listener.
  9. Emergencies. Subscribe to emergency (scope events:emergency) and set the SDK's emergency listener; show emergencies in a list with a repeating tone. Clear them with the emergency.clear command (token scope emergency:clear).
  10. Recordings. For a burst in your call list, download(transmission_id) and play the file; transcript(transmission_id) for the text.

What you will not have at the end of the day: dispatcher floor rights, multi-group audio routing, emergency acknowledge, patches, location history. See section 8.