π 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.
https://dvpx.algsoft.net.tr/api.php
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. |
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.
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). |
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.
api.php
Servers + talkgroups + DVPX Voice Engine settings. This is what the app fetches on startup.
api.php?action=ping
Responds without touching the database; suitable for uptime monitoring.
| 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 |
curl -s "https://dvpx.algsoft.net.tr/api.php?action=lastheard&tg=90&limit=10"
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));
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 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
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.
config.json β dashboard.urlhttps://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. |
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.
# 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"
| HTTP | error |
fatal |
What the reflector does |
|---|---|---|---|
| 401 | unauthorized | true |
LOCKS: refuses new logins and drops connected users. |
| 403 | pending_approval | true |
Waits locked; enters service AUTOMATICALLY the moment the admin approves. |
| 503 | server_error | false |
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
- 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.