HTTP API overview
Every DTVSOL Super Router answers an HTTP API. It is served by the router’s daemon, dtvsold, and it is what the dtvsol CLI, the OLT monitor and your billing system use to read and change the router. This page covers what all endpoints share. The pages that follow cover one area each.
| Page | Covers |
|---|---|
| Clients | Per-MAC clients, their plans, suspension and end dates |
| Services | Subscriber services (ONT, VLAN, address and speed in one call) |
| Networks | Served networks, DHCP, IPv6 pools, prefix delegation, DNS |
| VLANs, IP addresses, routes | VLAN interfaces, addresses, static routes, NAT pools |
| Plans | Speed plans |
| OLT | OLT operations and the OLT registry |
| CGNAT and anti-spoofing | Carrier-grade NAT and IP/MAC/VLAN binding |
| Protection | Allow-list, fail2ban, port forwards, firewall view, Option 82, SNMP agent |
| Network configuration | The router’s own ports, bonds, VLANs and addresses, with a 120 s rollback |
| Alarms and health | The alarm register and each area’s readings |
| System | Status, doctor, alerts, backup, configuration versions, graphs, licence |
| MikroTik-compatible API | The RouterOS-API listener for billing systems (TCP 8728) |
Base URL
Section titled “Base URL”http://ROUTER-IP:8880The API listens on the address and port set in the router’s configuration (listen_ip and listen_port). The default port is 8880. dtvsold speaks plain HTTP. If you need TLS, put the API behind a reverse proxy or a VPN.
The API port is behind the router’s allow-list. Only networks on that list can connect to it (and to the OLT monitor on port 8881). Manage the list with dtvsol protect, /protect or the monitor’s Settings → Protection. While the list is empty, both ports are open to everyone, so add your management networks before the router goes live. Keep the list as small as you can.
Authentication
Section titled “Authentication”Every request needs the router’s API key. The key is api_key in the router’s configuration. Send it in the X-API-Key header:
curl -s -H "X-API-Key: YOUR_API_KEY" http://ROUTER-IP:8880/statusOnly when a client cannot set headers, send it as a query parameter instead:
curl -s "http://ROUTER-IP:8880/status?api_key=YOUR_API_KEY"Prefer the header. URLs end up in logs and in shell history. The router compares the key in constant time. A router with no key configured refuses every request.
A request without a valid key gets:
{ "error": "Unauthorized. Provide X-API-Key header or ?api_key= parameter."}with status 401. The router logs each failed attempt with the caller’s address in its authentication log, masking any pass, password or api_key value in the URL. fail2ban watches this log, so repeated failures ban the caller (see fail2ban).
Requests
Section titled “Requests”- Paths. The first path segment is the resource and the second is its parameter:
/clients/AA:BB:CC:DD:EE:FF,/olts/olt-1,/services/svc_1a2b3c/suspend. Trailing slashes are ignored. URL-encode the path parameters where needed. - Query parameters are used for filters on
GET(/clients?network=10.110.0.0/21). - Request bodies are JSON objects. Send them with
Content-Type: application/json. A body that is not a JSON object is treated as empty. The largest body the router reads is 4 MiB; a larger one gets413. - Methods. Reads are
GET. Changes arePOST(andPUTwhere a page says so) orDELETE. SomeDELETEendpoints take a JSON body. Each page lists the method for every endpoint. - OLT credentials never go in a URL.
POST /olt/{op}refuses any other method (see OLT).
Endpoints that change the router are marked on each page with a Changes the router box.
Responses
Section titled “Responses”Every answer is JSON, pretty-printed with a four-space indent and followed by a newline. Graphs (PNG) and the backup download (an archive) are the only exceptions.
The router writes JSON the way its original PHP API did. Every JSON parser reads it without trouble, but three habits are worth knowing:
- Forward slashes inside strings are escaped:
"10.110.0.0\/21"is the string10.110.0.0/21. - An empty object may come back as
[]. Treat an empty[]and{}the same. - A whole-number float is written as an integer (
8, not8.0).
Most answers that change something carry ok, and many carry code and message:
{ "ok": true, "code": 201, "message": "Client added successfully", "client": { "mac": "AA:BB:CC:DD:EE:FF", "ip": "10.110.0.2" }}Errors and status codes
Section titled “Errors and status codes”An error answer has an error text. Usually it also has "ok": false and the code repeated in the body:
{ "ok": false, "code": 409, "error": "MAC AA:BB:CC:DD:EE:FF already registered"}| Status | Meaning |
|---|---|
200 |
Success (reads, and most changes) |
201 |
Created (for example a client, a service, a saved configuration version) |
400 |
A parameter is missing or invalid. The error text says which. |
401 |
No API key, or the wrong one |
404 |
The client, service, OLT, plan or operation does not exist |
405 |
The method is not allowed on this path. The error text usually lists the valid forms. |
409 |
Conflict: the item already exists, or (on network configuration) your version is stale |
413 |
Request body larger than 4 MiB |
500 |
The router could not complete the change (a command failed, a file could not be written). The error text says what failed, often with a detail. |
When a 5xx answer is caused by a configuration document that does not parse, the answer also lists the damaged files in broken_data_files. dtvsol doctor reports the same problem.
A path that is not an API resource gets a 200 answer with the API’s built-in usage summary, not a 404. Check that you spelled the resource correctly if you see an answer with "api": "DTVSOL DHCP API v1.0".
The action API
Section titled “The action API”For clients that can only issue simple GET requests (a browser, a billing system with URL callbacks), most operations also exist as actions. The action name and every argument go in the query string:
curl -s -H "X-API-Key: YOUR_API_KEY" \ "http://ROUTER-IP:8880/api?action=get&mac=AA:BB:CC:DD:EE:FF"Each action calls the same code as its REST endpoint, so the effect is identical. Prefer REST for new integrations: it keeps changes out of GET requests and credentials out of URLs. For the OLT especially, use POST /olt/{op}.
The actions that change the router work with GET. The only exception is config-save, which must be POST. An unknown action gets 400 with "error": "Unknown action" and a short list of actions.
| Action | Query parameters | Same as |
|---|---|---|
action=list |
[network] |
GET /clients |
action=search |
q |
GET /clients?q= |
action=get |
mac |
GET /clients/{mac} |
action=add |
mac, ip, [hostname], [comment], [ipv6] |
POST /clients |
action=delete |
mac |
DELETE /clients/{mac} |
action=active, action=connected |
[state] |
GET /clients/active |
action=clients6 |
[iface], [routers=1] |
GET /clients6 |
action=ip-info |
[network] |
GET /ips |
action=set-plan |
mac, plan |
POST /plan |
action=suspend, action=resume |
mac |
POST /suspend, POST /resume |
action=set-expires |
mac, expires |
POST /expires |
action=status |
GET /status |
|
action=networks |
GET /networks |
|
action=reload |
POST /dhcp/reload |
|
action=interfaces |
GET /interfaces |
|
action=iface-list |
GET /iface |
|
action=add-net, action=remove-net |
iface, [label] |
POST /net, DELETE /net |
action=net6-add, action=net6-del |
iface |
POST /net6, DELETE /net6 |
action=net6-pool |
iface, [start], [end] |
POST /net6/pool |
action=net6-lease |
valid, [preferred] |
POST /net6/lease |
action=net6-dns |
servers, [domain], [clear_domain=1] |
DHCPv6 resolvers only (dns-set sets both) |
action=pd-list |
GET /pd |
|
action=pd-add |
iface, [size] |
POST /pd |
action=pd-resize |
iface, size |
POST /pd/resize |
action=pd-del |
iface |
DELETE /pd |
action=pd-assign, action=pd-unassign |
mac, [prefix] |
prefix delegation pinning |
action=pd-sync |
[dry=1] |
reconcile delegated-prefix routes now |
action=dns-list |
GET /dns |
|
action=dns-set |
[v4], [v6], [domain], [iface], [apply=all] |
POST /dns, POST /dns/{iface} |
action=dns-del |
iface or global=v4|v6|domain|all |
DELETE /dns/{iface}, DELETE /dns |
action=vlan-list |
GET /vlans |
|
action=vlan-add |
parent, vlan_id, [ip], [ipv6], [label], [protocol], [force=1], [serve=0] |
POST /vlans |
action=vlan-disable |
name, [reason] |
POST /vlans/disable |
action=vlan-enable |
name |
POST /vlans/enable |
action=vlan-del |
name |
DELETE /vlans |
action=ip-add, action=ip-del |
iface, ip, [force=1] |
POST /ip, DELETE /ip |
action=route-list, action=route-add, action=route-del |
prefix, [via], [dev], [comment] |
/routes |
action=nat-list, action=nat-add, action=nat-del |
iface, pool_start, pool_end, [exempt], [comment] |
/nat |
action=plan-list |
GET /plans |
|
action=plan-add |
name, down_mbps, up_mbps, [comment] |
POST /plans |
action=plan-del |
name |
DELETE /plans/{name} |
action=service-list |
[state], [olt], [q], [fast=1] |
GET /services |
action=service-get |
id |
GET /services/{id} |
action=service-add |
as POST /services, in the query |
POST /services |
action=service-set |
id, fields as POST /services/{id} |
POST /services/{id} |
action=service-suspend, action=service-resume |
id |
POST /services/{id}/suspend, /resume |
action=service-del |
id, [keep_ont=1] |
DELETE /services/{id} |
action=service-unregistered |
[olt] |
GET /services/unregistered |
action=service-expiry |
apply the services’ end dates now (the router’s timer does this on its own) | |
action=olts-list |
GET /olts |
|
action=olts-add, action=olts-set |
name, OLT fields |
POST /olts, POST /olts/{name} |
action=olts-del |
name |
DELETE /olts/{name} |
action=olt-sync |
[olt], [dry_run=1] |
POST /olt/sync |
action=olt-backup |
[olt] |
POST /olt/backup |
action=olt-backups |
[olt], [n] |
GET /olt/backups |
action=olt-diff |
[olt], [rev], [to] |
GET /olt/diff |
action=olt-info, action=olt-autofind, … (olt-<op>) |
olt=<name> and the operation’s arguments |
POST /olt/{op} |
action=protect-list |
GET /protect |
|
action=protect-add |
network, [comment] |
POST /protect |
action=protect-delete |
network |
DELETE /protect |
action=firewall |
[network] |
GET /firewall |
action=firewall-full |
GET /firewall/full |
|
action=fail2ban, action=fail2ban-status |
GET /fail2ban |
|
action=fail2ban-unban |
ip |
POST /fail2ban |
action=pf-list, action=pf-add, action=pf-del |
as /pf, in the query |
/pf |
action=option82 |
GET /option82 |
|
action=snmp-status, action=snmp-set |
as /snmp, in the query |
/snmp |
action=cgnat-status, action=cgnat-set |
as /cgnat, in the query |
/cgnat |
action=cgnat-lookup |
public_ip, port |
GET /cgnat/lookup |
action=antispoof-status |
GET /antispoof |
|
action=antispoof-set |
enabled, [mode], [log], [exempt] |
POST /antispoof |
action=antispoof-iface |
iface, mode |
POST /antispoof/{iface} |
action=antispoof-log |
[since], [limit] |
GET /antispoof/log |
action=antispoof-sync |
POST /antispoof/sync |
|
action=doctor |
[olt=1] |
GET /doctor |
action=alerts |
GET /alerts |
|
action=config-status, action=config-versions, action=config-diff, action=config-show |
see System | configuration versions |
action=config-save (POST) |
[comment] |
save the running configuration as a new version |
The OLT operations available as olt-<op> are: action=olt-info, action=olt-autofind, action=olt-onus, action=olt-vlans, action=olt-serviceports, action=olt-profiles, action=olt-config, action=olt-run, action=olt-exec, action=olt-vlan-add, action=olt-vlan-del, action=olt-port-vlan, action=olt-profile-add, action=olt-profile-del, action=olt-ont-add, action=olt-ont-del, action=olt-ont-reboot, action=olt-ont-desc, action=olt-ont-optical, action=olt-ntp, action=olt-sysname, action=olt-save, action=olt-plan-sync and action=olt-init. Any other operation in GET /olt’s list is accepted the same way.
The newer areas have no action form: the alarm register and health readings, the router network configuration, the graphs, the backup download and the restore.
Checking the docs against the code
Section titled “Checking the docs against the code”The site’s repository has tools/api-inventory.mjs. It reads the dtvsold sources and lists every resource, endpoint and action the API routes. With --check, it lists anything these pages do not document. It runs on every release.