πŸ”Œ DVPX Public API

Every endpoint lives at a single address and returns JSON. No authentication is required and CORS is open to any origin β€” you can feed your own dashboard, bot or widget straight from the browser.

Base URL
https://dvpx.algsoft.net.tr/api.php
No API key is required on this server β€” the endpoints are public.
GET Last Heard api.php?action=lastheard&limit=5

Call history reported by the reflectors. A row is opened when a transmission STARTS (active=true, duration_ms=0 at that point) and updated with the final length when it ends β€” use the active field for a "currently talking" indicator.

Parameter Default Description
tg 0 talkgroup number. 0/omitted = all talkgroups.
limit 50 number of records, 1-200.
since β€” only records NEWER than this. ISO-8601 or unix seconds. For incremental polling, send back the last started_at you saw.

    
GET Online Stations api.php?action=online

Stations currently connected to the reflectors. Apps pick call targets from this list. Reflectors refresh it about every 10 seconds; entries older than 90 seconds disappear. The talkgroup field returns 0 when the station is not on a talkgroup.


    
GET Talkgroups api.php?action=talkgroups

Talkgroups that are active on the panel. When dmr_id is given the list is personalised for that identity; without the parameter you get the public list.

Parameter Default Description
dmr_id β€” supply your identity to receive the talkgroup list assigned to you (an ESSID-suffixed id is accepted too).

    
GET Reflectors api.php?action=servers

Reflectors you can connect to, with their TCP signalling and UDP voice ports. To appear here a reflector must be online, approved by the administrator AND have contacted the panel within the last 2 minutes β€” so every server listed is genuinely alive.


    
GET Everything (default) api.php

Servers + talkgroups + DVPX Voice Engine settings. This is what the app fetches on startup.


    
GET Health Check api.php?action=ping

Responds without touching the database; suitable for uptime monitoring.


    
lastheard response fields
Field Type Description
id int unique id of the call record
source_id int DMR ID of the transmitting station
callsign string callsign (empty if unknown)
target_id int target: TG number, or the peer DMR ID for a private call
tg_name string talkgroup name (empty for private calls)
call_type int 0 = talkgroup, 1 = private call
private bool convenience flag for call_type == 1
frames int number of voice frames sent (20 ms per frame)
duration_ms int transmission length in milliseconds. It is 0 while the transmission is still running.
active bool true means this transmission is STILL RUNNING β€” use it for a "currently talking" indicator.
server_id int|null panel id of the reflector that logged it
server_name string reflector name
started_at string start of transmission, ISO-8601 UTC
started_at_unix int same instant as unix seconds β€” easier to parse
Records are returned by started_at descending (newest first). A station that transmitted several times appears several times β€” de-duplicate by source_id if your list shows one row per station.
Examples
curl β€” last 10 calls on TG 90
curl -s "https://dvpx.algsoft.net.tr/api.php?action=lastheard&tg=90&limit=10"
JavaScript β€” straight from the browser (CORS is open)
const r = await fetch("https://dvpx.algsoft.net.tr/api.php?action=lastheard&tg=90");
const { calls } = await r.json();
calls.forEach(c => console.log(c.callsign, c.tg_name, c.duration_ms));
Python β€” incremental polling (new records only)
import requests, time

url, since = "https://dvpx.algsoft.net.tr/api.php", None
while True:
    p = {"action": "lastheard", "limit": 200}
    if since:
        p["since"] = since
    calls = requests.get(url, params=p, timeout=10).json()["calls"]
    for c in reversed(calls):          # eskiden yeniye
        print(c["started_at"], c["callsign"], "->", c["target_id"])
    if calls:
        since = calls[0]["started_at"] # en yeni kayΔ±t
    time.sleep(5)
πŸ“ž Full-Duplex Calling TCP signalling

Full duplex is a private call where two stations talk at the same time without holding PTT. Nothing changes in AUDIO TRANSPORT β€” the reflector already forwards packets regardless of direction, so both ways flow at once. The only addition is CALL SETUP: who is calling whom, whether the other side accepted, and when the call ended.

These lines travel on the TCP signalling channel between client and reflector; they are unrelated to api.php.

→ FDX INVITE <hedefDmrId>
← FDX RING <kaynakId> <cagriIsareti>
→ FDX ACCEPT | REJECT | END
← FDX ACCEPTED | REJECTED | ENDED
← FDX FAIL  NO_TARGET | BUSY | SELF | NO_CALL
ACCEPTED goes to BOTH sides β€” if one waited for PTT the conversation would stay one-way. A reflector that has not been updated returns ERR UNKNOWN_COMMAND and the client shows "this reflector does not support full duplex". Older clients never send FDX, so nothing changes for them.
πŸ“‘ Reflector Control API token required

This section is NOT for app developers; it is for reflector operators. Reflectors never touch the database: they fetch the talkgroup list, the block list and the user grants from this endpoint, and report what happens (online list, call records) back to it.

Address reflectors connect to β€” config.json β†’ dashboard.url
https://dvpx.algsoft.net.tr/reflector.php
Method Endpoint Description
GET ?action=ping No token needed. A "did I get the address right?" test β€” you can open it in a browser.
GET ?action=snapshot Policy only: talkgroup list, blocked ids, user grants. For debugging.
POST ?action=heartbeat The real endpoint. Sends telemetry and receives the policy (only if it CHANGED) in one request.
Authentication β€” any one of these
Authorization: Bearer dvpx_xxxxxxxx
X-DVPX-Token: dvpx_xxxxxxxx
{"token": "dvpx_xxxxxxxx", ...}

Both headers are sent because some shared hosts never pass Authorization through to PHP. The token is shown once in the panel; only its SHA-256 digest is stored in the database.

Testing the connection
# Is the address right? (no token)
curl -sS "https://dvpx.algsoft.net.tr/reflector.php?action=ping"

# Is the token valid? It should return your talkgroup list
curl -sS -H "X-DVPX-Token: dvpx_xxxx" "https://dvpx.algsoft.net.tr/reflector.php?action=snapshot"
Error contract β€” this drives the reflector's behaviour
HTTP error fatal What the reflector does
401unauthorizedtrue LOCKS: refuses new logins and drops connected users.
403pending_approvaltrue Waits locked; enters service AUTOMATICALLY the moment the admin approves.
503server_errorfalse Treats it as TEMPORARY: keeps carrying voice and accepting logins from its cached policy, queues its reports.

This distinction is deliberate: a temporary database or network fault NEVER closes the network to logins. Only an explicit rejection (401/403) locks a reflector. Reflector setup: dvpx-reflector/KURULUM.md

Usage notes
  • Responses are cached for 5 seconds (Cache-Control: max-age=5). Polling faster than that gains nothing.
  • If you track a busy feed, use the since parameter instead of pulling 200 rows every time β€” it spares both the server and your bandwidth.
  • Timestamps are UTC. Convert to local time on the client side.
  • A record appears the moment a transmission STARTS, with active=true; when it ends the same record is updated with the final length. Seeing the same id twice is an update, not a duplicate.
  • On failure the body is {"ok": false, "error": "...", "message": "..."} with a 4xx/5xx status code.