Skip to content

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.

ServicePortEncryption
api8728None (plaintext)
api-ssl8729TLS

/ip/service

  • 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)

By default, the API service is enabled on port 8728. For production use, enable only the SSL variant.

/ip service print
/ip service set api-ssl disabled=no
/ip service set api disabled=yes

Assign a certificate for TLS:

/ip service set api-ssl certificate=your-server-cert
/ip service set api-ssl address=192.168.88.0/24
/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=StrongPassword

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

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.

First byte valueLength field sizeMax word length
0x00–0x7F1 byte127 bytes
0x80–0xBF2 bytes16 383 bytes
0xC0–0xDF3 bytes2 097 151 bytes
0xE0–0xEF4 bytes268 435 455 bytes

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 wordMeaning
!reOne result record
!doneCommand completed successfully
!trapRecoverable error
!fatalFatal error (connection closed)

Client sends (reading IP addresses):

/ip/address/print
← empty word ends sentence

Router replies:

!re
=.id=*1
=address=192.168.88.1/24
=network=192.168.88.0
=interface=bridge
!done

RouterOS API supports two login methods depending on the RouterOS version.

Send the username and password directly:

/login
=name=apiuser
=password=StrongPassword

Router responds with !done on success.

Challenge-response login (legacy, pre-v6.43)

Section titled “Challenge-response login (legacy, pre-v6.43)”
  1. Send /login with no credentials.
  2. Router returns !done with =ret=<hex-challenge>.
  3. Compute MD5 of \x00 + password + challenge bytes (as raw bytes).
  4. Send /login with =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.

librouteros is the officially recommended Python library for RouterOS API access.

Terminal window
pip install librouteros
import librouteros
api = librouteros.connect(
host='192.168.88.1',
username='apiuser',
password='StrongPassword',
port=8728,
)

For SSL (port 8729):

import ssl
import librouteros
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.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,
)

Use api.path() to navigate the RouterOS menu tree:

# List all IP addresses
addresses = 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)
fw = api.path('/ip/firewall/filter')
# List all rules
for rule in fw:
print(rule['.id'], rule['chain'], rule['action'])
# Add a rule
fw.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 ID
fw.remove(id='*5')
leases = api.path('/ip/dhcp-server/lease')
# Check if a static lease exists; create or update it
target_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',
})
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)

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.

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.

Start a traffic stream on ether1 (tag 1):

/interface/monitor-traffic
=interface=ether1
.tag=1

Router sends repeated replies:

!re
=name=ether1
=rx-bits-per-second=4096
=tx-bits-per-second=2048
.tag=1
!re
...

Cancel the stream:

/cancel
=tag=1

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)

Trigger a binary backup and a text export via API, then download the files:

import librouteros
import ftplib
import 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 backup
api.path('/system/backup').call('save', {'name': backup_name})
# Create text export
api.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=yes

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_keys
to_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()

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}")

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:

ReasonMeaning
no such itemThe .id does not exist
not permittedUser lacks required policy
invalid valueParameter value is out of range
already have such entryDuplicate entry in list
/ip service print

Verify the api or api-ssl service shows disabled=no and the correct port. Check that the connecting host is within the allowed address range.

  • Confirm the username exists: /user print
  • Confirm the user’s group includes the api policy: /user group print
  • For SSL connections, verify the certificate is valid and assigned to api-ssl

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