RouterOS API
RouterOS API
Section titled “RouterOS API”The RouterOS API provides a binary TCP interface for programmatic device management. Unlike the REST API (which uses HTTP/JSON), the RouterOS API communicates over a dedicated TCP connection using a length-prefixed binary protocol. It is available on every RouterOS device and supports the full range of RouterOS commands — reading, writing, subscribing to real-time updates, and running system operations.
| Service | Port | Encryption |
|---|---|---|
api | 8728 | None (plaintext) |
api-ssl | 8729 | TLS |
Overview
Section titled “Overview”Sub-menu
Section titled “Sub-menu”/ip/service
Key Capabilities
Section titled “Key Capabilities”- Full read/write access to all RouterOS menus
- Real-time event streaming (listen/monitor)
- Concurrent request multiplexing via tags
- TLS-encrypted transport on port 8729
- Well-supported Python libraries (
librouteros,routeros_api)
Enabling the API Service
Section titled “Enabling the API Service”By default, the API service is enabled on port 8728. For production use, enable only the SSL variant.
Check current status
Section titled “Check current status”/ip service printEnable API-SSL (recommended)
Section titled “Enable API-SSL (recommended)”/ip service set api-ssl disabled=no/ip service set api disabled=yesAssign a certificate for TLS:
/ip service set api-ssl certificate=your-server-certRestrict access by source address
Section titled “Restrict access by source address”/ip service set api-ssl address=192.168.88.0/24Create a dedicated API user
Section titled “Create a dedicated API user”/user group add name=api-user policy=read,write,api,!local,!telnet,!ssh,!ftp,!reboot,!policy,!winbox,!web,!sniff,!sensitive,!romon/user add name=apiuser group=api-user password=StrongPasswordAvoid using the unencrypted API (port 8728) in production — credentials and all data are transmitted in cleartext. Use api-ssl (port 8729) with a valid certificate.
Protocol Basics
Section titled “Protocol Basics”The RouterOS API communicates using sentences. Each sentence is a sequence of words terminated by an empty word (\x00). Words are length-prefixed using a variable-length encoding.
Word length encoding
Section titled “Word length encoding”| First byte value | Length field size | Max word length |
|---|---|---|
0x00–0x7F | 1 byte | 127 bytes |
0x80–0xBF | 2 bytes | 16 383 bytes |
0xC0–0xDF | 3 bytes | 2 097 151 bytes |
0xE0–0xEF | 4 bytes | 268 435 455 bytes |
Sentence structure
Section titled “Sentence structure”A complete API exchange consists of command sentences (sent by client) and reply sentences (sent by router).
Command sentence format:
/ip/address/print ← command word (API path)=.proplist=address,network ← attribute word (name=value)?address=192.168.1.0/24 ← query word.tag=42 ← tag word (for async correlation) ← empty word (end of sentence)Reply types:
| Reply word | Meaning |
|---|---|
!re | One result record |
!done | Command completed successfully |
!trap | Recoverable error |
!fatal | Fatal error (connection closed) |
Example exchange
Section titled “Example exchange”Client sends (reading IP addresses):
/ip/address/print ← empty word ends sentenceRouter replies:
!re=.id=*1=address=192.168.88.1/24=network=192.168.88.0=interface=bridge
!doneAuthentication
Section titled “Authentication”RouterOS API supports two login methods depending on the RouterOS version.
Plaintext login (RouterOS v6.43+ and v7)
Section titled “Plaintext login (RouterOS v6.43+ and v7)”Send the username and password directly:
/login=name=apiuser=password=StrongPasswordRouter responds with !done on success.
Challenge-response login (legacy, pre-v6.43)
Section titled “Challenge-response login (legacy, pre-v6.43)”- Send
/loginwith no credentials. - Router returns
!donewith=ret=<hex-challenge>. - Compute MD5 of
\x00+ password + challenge bytes (as raw bytes). - Send
/loginwith=name=and=response=00+ MD5 hex digest.
Most modern libraries handle this automatically. If you are targeting a specific RouterOS version, check which method applies.
Python Integration with librouteros
Section titled “Python Integration with librouteros”librouteros is the officially recommended Python library for RouterOS API access.
Installation
Section titled “Installation”pip install librouterosConnecting and authenticating
Section titled “Connecting and authenticating”import librouteros
api = librouteros.connect( host='192.168.88.1', username='apiuser', password='StrongPassword', port=8728,)For SSL (port 8729):
import sslimport librouteros
ctx = ssl.create_default_context()ctx.check_hostname = Falsectx.verify_mode = ssl.CERT_NONE # use CERT_REQUIRED with a valid CA in production
api = librouteros.connect( host='192.168.88.1', username='apiuser', password='StrongPassword', port=8729, ssl_wrapper=ctx.wrap_socket,)Running commands and reading responses
Section titled “Running commands and reading responses”Use api.path() to navigate the RouterOS menu tree:
# List all IP addressesaddresses = api.path('/ip/address')for addr in addresses: print(addr['address'], addr['interface'])Fetch specific properties using .proplist to reduce response size:
ifaces = api.path('/interface')for iface in ifaces.select('name', 'running', 'tx-byte', 'rx-byte'): print(iface)Managing firewall rules
Section titled “Managing firewall rules”fw = api.path('/ip/firewall/filter')
# List all rulesfor rule in fw: print(rule['.id'], rule['chain'], rule['action'])
# Add a rulefw.add( chain='input', protocol='tcp', **{'dst-port': '22'}, src_address='10.0.0.0/8', action='accept', comment='allow-ssh-from-management',)
# Remove a rule by IDfw.remove(id='*5')Managing DHCP leases
Section titled “Managing DHCP leases”leases = api.path('/ip/dhcp-server/lease')
# Check if a static lease exists; create or update ittarget_mac = 'AA:BB:CC:DD:EE:FF'target_ip = '192.168.88.100'
existing = [x for x in leases if x.get('mac-address') == target_mac]
if existing: leases.update(id=existing[0]['.id'], address=target_ip)else: leases.add(**{ 'mac-address': target_mac, 'address': target_ip, 'comment': 'managed-by-api', })Interface monitoring loop
Section titled “Interface monitoring loop”import time
ifaces = api.path('/interface')
while True: for iface in ifaces.select('name', 'running', 'rx-byte', 'tx-byte'): status = 'up' if iface.get('running') else 'down' print(f"{iface['name']:20s} {status} rx={iface.get('rx-byte', 0)} tx={iface.get('tx-byte', 0)}") print('---') time.sleep(30)Subscribing to Real-Time Updates
Section titled “Subscribing to Real-Time Updates”Some RouterOS commands stream continuous updates until cancelled. This is useful for monitoring traffic, watching log events, or receiving ARP table changes as they happen.
How streaming works
Section titled “How streaming works”Long-running commands (e.g., /interface/monitor-traffic) send repeated !re sentences. The client continues receiving results until it sends a /cancel command referencing the original .tag.
Raw protocol example
Section titled “Raw protocol example”Start a traffic stream on ether1 (tag 1):
/interface/monitor-traffic=interface=ether1.tag=1Router sends repeated replies:
!re=name=ether1=rx-bits-per-second=4096=tx-bits-per-second=2048.tag=1
!re...Cancel the stream:
/cancel=tag=1Python streaming with librouteros
Section titled “Python streaming with librouteros”librouteros exposes streaming via the lower-level api() call:
import threading
def monitor_traffic(api, interface, duration=10): """Monitor interface traffic for a set duration.""" results = [] tag = api( '/interface/monitor-traffic', **{'=interface': interface, '=once': 'no'}, ) import time deadline = time.time() + duration for sentence in tag: if time.time() >= deadline: break rx = sentence.get('rx-bits-per-second', 0) tx = sentence.get('tx-bits-per-second', 0) print(f" rx={int(rx)//1000} kbps tx={int(tx)//1000} kbps")
monitor_traffic(api, 'ether1', duration=10)Practical Automation Scripts
Section titled “Practical Automation Scripts”Automated backup
Section titled “Automated backup”Trigger a binary backup and a text export via API, then download the files:
import librouterosimport ftplibimport datetime
api = librouteros.connect('192.168.88.1', username='apiuser', password='StrongPassword')
date_str = datetime.date.today().isoformat()backup_name = f"auto-{date_str}"
# Create binary backupapi.path('/system/backup').call('save', {'name': backup_name})
# Create text exportapi.path('/').call('export', {'file': backup_name})
print(f"Backups saved: {backup_name}.backup and {backup_name}.rsc")api.disconnect()To retrieve the files off-device, use /tool fetch via API or retrieve via FTP/SFTP:
# RouterOS side: push backup to remote server/tool fetch address=192.168.1.10 src-path=/auto-2026-03-22.backup \ dst-path=/backups/ mode=ftp user=ftpuser password=ftppass upload=yesConfiguration management (desired state)
Section titled “Configuration management (desired state)”Apply a set of desired firewall address-list entries, adding missing ones and removing stale entries:
import librouteros
DESIRED = { ('blocked', '198.51.100.0/24'), ('blocked', '203.0.113.7'), ('trusted', '10.0.0.0/8'),}
api = librouteros.connect('192.168.88.1', username='apiuser', password='StrongPassword')alist = api.path('/ip/firewall/address-list')
current = { (entry['list'], entry['address']): entry['.id'] for entry in alist}current_keys = set(current.keys())
to_add = DESIRED - current_keysto_remove = current_keys - DESIRED
for lst, addr in to_add: alist.add(list=lst, address=addr, comment='managed-by-api') print(f" Added: {lst} {addr}")
for key in to_remove: alist.remove(id=current[key]) print(f" Removed: {key[0]} {key[1]}")
print(f"Done. Added {len(to_add)}, removed {len(to_remove)}.")api.disconnect()Bulk configuration changes
Section titled “Bulk configuration changes”Apply a list of changes across multiple routers:
import librouteros
ROUTERS = [ {'host': '192.168.1.1', 'username': 'apiuser', 'password': 'pass1'}, {'host': '192.168.1.2', 'username': 'apiuser', 'password': 'pass2'},]
NTP_SERVERS = ['162.159.200.1', '162.159.200.123']
for router in ROUTERS: try: api = librouteros.connect(**router) ntp = api.path('/system/ntp/client') ntp.update(**{ 'enabled': True, 'servers': ','.join(NTP_SERVERS), }) identity = list(api.path('/system/identity'))[0]['name'] print(f" {router['host']} ({identity}): NTP updated") api.disconnect() except Exception as e: print(f" {router['host']}: FAILED — {e}")Error Handling
Section titled “Error Handling”Always handle !trap replies in production scripts. librouteros raises librouteros.exceptions.TrapError on API errors:
from librouteros.exceptions import TrapError
try: fw = api.path('/ip/firewall/filter') fw.remove(id='*999')except TrapError as e: print(f"API error: {e}")Common trap reasons:
| Reason | Meaning |
|---|---|
no such item | The .id does not exist |
not permitted | User lacks required policy |
invalid value | Parameter value is out of range |
already have such entry | Duplicate entry in list |
Troubleshooting
Section titled “Troubleshooting”Connection refused on port 8728/8729
Section titled “Connection refused on port 8728/8729”/ip service printVerify the api or api-ssl service shows disabled=no and the correct port. Check that the connecting host is within the allowed address range.
Authentication failures
Section titled “Authentication failures”- Confirm the username exists:
/user print - Confirm the user’s group includes the
apipolicy:/user group print - For SSL connections, verify the certificate is valid and assigned to
api-ssl
Enable API in firewall
Section titled “Enable API in firewall”If the router has a restrictive input firewall, allow API traffic:
/ip firewall filter add chain=input protocol=tcp dst-port=8729 \ src-address=192.168.88.0/24 action=accept comment="Allow API-SSL" \ place-before=0