Skip to content

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)
http://ROUTER-IP:8880

The 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.

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:

Terminal window
curl -s -H "X-API-Key: YOUR_API_KEY" http://ROUTER-IP:8880/status

Only when a client cannot set headers, send it as a query parameter instead:

Terminal window
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).

  • 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 gets 413.
  • Methods. Reads are GET. Changes are POST (and PUT where a page says so) or DELETE. Some DELETE endpoints 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.

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 string 10.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, not 8.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" }
}

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".

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:

Terminal window
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.

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.