Summary

Application Programmable Interface (API) allows users to create custom software solutions to communicate with RouterOS to gather information, adjust the configuration, and manage the router. API closely follows syntax from the command-line interface (CLI). It can be used to create translated or custom configuration tools to aid ease of use running and managing routers with RouterOS.

API service must be enabled before trying to establish the API connection. By default, API uses TCP:8728 and TCP:8729 (secure). 

Protocol

Communication with the router is done by sending sentences and receiving one or more sentences in return. A sentence is a sequence of words terminated by zero-length words. Word is part of a sentence encoded in a certain way - encoded length and data. Communication happens by sending sentences to the router and receiving replies to sent sentences. Each sentence sent to the router using API should contain a command as a first word followed by words in no particular order, the end of the sentence is marked by a zero-length word. When the router receives a full sentence (command word, no or more attribute words, and zero-length word) it is evaluated and executed, then a reply is formed and returned.

API words

Words are part of a sentence. Each word has to be encoded in a certain way - length of the word followed by word content. The length of the word should be given as a count of bytes that are going to be sent.

Length of the word is encoded as follows:

Value of length# of bytesEncoding
0 <= len <= 0x7F1len, lowest byte
0x80 <= len <= 0x3FFF2len | 0x8000, two lower bytes
0x4000 <= len <= 0x1FFFFF3len | 0xC00000, three lower bytes
0x200000 <= len <= 0xFFFFFFF4len | 0xE0000000
len >= 0x1000000050xF0 and len as four bytes


In general, words can be described like this <<encoded word length><word content>>. Word content can be separated into 5 parts: command word, attribute word, API attribute word. query word, and reply word

Command word

First word in sentence has to be command followed by attribute words and zero length word or terminating word. Name of command word should begin with '/'. Names of commands closely follow CLI, with spaces replaced with '/'. There are commands that are specific to API;

Command word structure in strict order:


API specific commands:

login
cancel

Command word content examples:

/login
/user/active/listen
/interface/vlan/remove
/system/reboot

Attribute word

Each command word has its own list of attribute words depending on content.

Atribute word structure consists of 5 parts in this order:

Note: Value can hold multiple equal signs in the value of attribute word since the way word is encoded


Note: Value can be empty



Examples without encoded length prefix:

=address=10.0.0.1
=name=iu=c3Eeg
=disable-running-check=yes


Order of attribute words and API parameters is not important and should not be relied on


API attribute word

API attribute word structure is in strict order:

Currently the only such API attribute is tag.

If sentence contain API attribute word tag then each returned sentence in reply from router to that tagged sentence will be tagged with same tag.

Query word

Senteces can have additional query paramteres that restrict their scope. They are explained in detail in separate section.

Example of sentence using query word attributes:

/interface/print
?type=ether
?type=vlan
?#|!


Order of query words is significant


Reply word

It is sent only by the router. It is only sent in response to full sentence send by the client.

API sentences

API sentence is main object of communication using API.

Zero length word terminates the sentence. If it is not provided router will not start to evaluate sent words and will consider all the input as part of the same sentence.

Initial login

Note: that each command and response ends with an empty word.

Login method post-v6.43:

/login
=name=admin
=password=
 !done


Tags

Command description

Queries

print command accepts query words that limit set of returned sentences. This feature is available since RouterOS 3.21.

QueryDesciption
?namepushes 'true' if item has value of property name, 'false' if it does not.
?-namepushes 'true' if item does not have value of property name, 'false' otherwise.
?name=x
?=name=x
pushes 'true' if property name has value equal to x, 'false' otherwise.
?<name=xpushes 'true' if property name has value less than x, 'false' otherwise.
?>name=xpushes 'true' if property name has value greater than x, 'false' otherwise.
?#operationsapplies operations to the values in the stack.
  • operation string is evaluated left to right.
  • sequence of decimal digits followed by any other character or end of word is interpreted as a stack index. top value has index 0.
  • index that is followed by a character pushes copy of value at that index.
  • index that is followed by the end of word replaces all values with the value at that index.
  • ! character replaces top value with the opposite.
  • & pops two values and pushes result of logical 'and' operation.
  • | pops two values and pushes result of logical 'or' operation.
  • . after an index does nothing.
  • . after another character pushes copy of top value.


Warning: Regular expressions are not supported in API, so do not try to send query with ~ symbol


Examples:

/interface/print
?type=ether
?type=vlan
?#|
/ip/route/print
?>comment=

OID

print command can return OID values for properties that are available in SNMP. This feature appeared in 3.23 version.

In console, OID values can be seen by running 'print oid' command. In API, these properties have name that ends with ".oid", and can be retrieved by adding their name to the value of '.proplist'. An example:


/system/resource/print
=.proplist=uptime,cpu-load,uptime.oid,cpu-load.oid
 !re
=uptime=01:22:53
=cpu-load=0
=uptime.oid=.1.3.6.1.2.1.1.3.0
=cpu-load.oid=.1.3.6.1.2.1.25.3.3.1.2.1

 !done


!trap

When for some reason API sentence fails trap is sent in return accompanied with message attribute and on some occasions category argument.

message

When API sentence fails some generic message or message from used internal process is return to give more details about failure

<<< /ip/address/add
<<< =address=192.168.88.1
<<< =interface=asdf
<<< 
>>> !trap
>>> =category=1
>>> =message=input does not match any value of interface

category

if it is a general error, it is categorized and error category is returned. possible values for this attribute are

Command examples

/system/package/getall

/system/package/getall

 !re
=.id=*5802
=disabled=no
=name=routeros-x86
=version=3.0beta2
=build-time=oct/18/2006 16:24:41
=scheduled=

 !re
=.id=*5805
=disabled=no
=name=system
=version=3.0beta2
=build-time=oct/18/2006 17:20:46
=scheduled=

... more !re sentences ...
 !re
=.id=*5902
=disabled=no
=name=advanced-tools
=version=3.0beta2
=build-time=oct/18/2006 17:20:49
=scheduled=

 !done

/user/active/listen

/user/active/listen

 !re
=.id=*68
=radius=no
=when=oct/24/2006 08:40:42
=name=admin
=address=0.0.0.0
=via=console

 !re
=.id=*68
=.dead=yes

... more !re sentences ...

/cancel, simultaneous commands

/login

 !done
=ret=856780b7411eefd3abadee2058c149a3

/login
=name=admin
=response=005062f7a5ef124d34675bf3e81f56c556

 !done
-- first start listening for interface changes (tag is 2)
/interface/listen
.tag=2
-- disable interface (tag is 3)
/interface/set
=disabled=yes
=.id=ether1
.tag=3
-- this is done for disable command (tag 3)
 !done
.tag=3

-- enable interface (tag is 4)
/interface/set
=disabled=no
=.id=ether1
.tag=4
-- this update is generated by change made by first set command (tag 3)
 !re
=.id=*1
=disabled=yes
=dynamic=no
=running=no
=name=ether1
=mtu=1500
=type=ether
.tag=2

-- this is done for enable command (tag 4)
 !done
.tag=4

-- get interface list (tag is 5)
/interface/getall
.tag=5

-- this update is generated by change made by second set command (tag 4)
 !re
=.id=*1
=disabled=no
=dynamic=no
=running=yes
=name=ether1
=mtu=1500
=type=ether
.tag=2

-- these are replies to getall command (tag 5)
 !re
=.id=*1
=disabled=no
=dynamic=no
=running=yes
=name=ether1
=mtu=1500
=type=ether
.tag=5

 !re
=.id=*2
=disabled=no
=dynamic=no
=running=yes
=name=ether2
=mtu=1500
=type=ether
.tag=5

-- here interface getall ends (tag 5)
 !done
.tag=5

-- stop listening - request to cancel command with tag 2, cancel itself uses tag 7
/cancel
=tag=2
.tag=7

-- listen command is interrupted (tag 2)
 !trap
=category=2
=message=interrupted
.tag=2

-- cancel command is finished (tag 7)
 !done
.tag=7

-- listen command is finished (tag 2)
 !done
.tag=2

Example client

#!/usr/bin/python

import sys, posix, time, md5, binascii, socket, select, ssl

class ApiRos:
"Routeros api"
def __init__(self, sk):
self.sk = sk
self.currenttag = 0

def login(self, username, pwd):

for repl, attrs in self.talk(["/login", "=name=" + username,
"=password=" + pwd]):
if repl == '!trap':
return False
elif '=ret' in attrs.keys():
#for repl, attrs in self.talk(["/login"]):
chal = binascii.unhexlify(attrs['=ret'])
md = md5.new()
md.update('\x00')
md.update(pwd)
md.update(chal)
for repl2, attrs2 in self.talk(["/login", "=name=" + username,
"=response=00" + binascii.hexlify(md.digest())]):
if repl2 == '!trap':
return False
return True

def talk(self, words):
if self.writeSentence(words) == 0: return
r = []
while 1:
i = self.readSentence();
if len(i) == 0: continue
reply = i[0]
attrs = {}
for w in i[1:]:
j = w.find('=', 1)
if (j == -1):
attrs[w] = ''
else:
attrs[w[:j]] = w[j+1:]
r.append((reply, attrs))
if reply == '!done': return r

def writeSentence(self, words):
ret = 0
for w in words:
self.writeWord(w)
ret += 1
self.writeWord('')
return ret

def readSentence(self):
r = []
while 1:
w = self.readWord()
if w == '': return r
r.append(w)

def writeWord(self, w):
print "<<< " + w
self.writeLen(len(w))
self.writeStr(w)

def readWord(self):
ret = self.readStr(self.readLen())
print ">>> " + ret
return ret

def writeLen(self, l):
if l < 0x80:
self.writeStr(chr(l))
elif l < 0x4000:
l |= 0x8000
self.writeStr(chr((l >> 8) & 0xFF))
self.writeStr(chr(l & 0xFF))
elif l < 0x200000:
l |= 0xC00000
self.writeStr(chr((l >> 16) & 0xFF))
self.writeStr(chr((l >> 8) & 0xFF))
self.writeStr(chr(l & 0xFF))
elif l < 0x10000000:
l |= 0xE0000000
self.writeStr(chr((l >> 24) & 0xFF))
self.writeStr(chr((l >> 16) & 0xFF))
self.writeStr(chr((l >> 8) & 0xFF))
self.writeStr(chr(l & 0xFF))
else:
self.writeStr(chr(0xF0))
self.writeStr(chr((l >> 24) & 0xFF))
self.writeStr(chr((l >> 16) & 0xFF))
self.writeStr(chr((l >> 8) & 0xFF))
self.writeStr(chr(l & 0xFF))

def readLen(self):
c = ord(self.readStr(1))
if (c & 0x80) == 0x00:
pass
elif (c & 0xC0) == 0x80:
c &= ~0xC0
c <<= 8
c += ord(self.readStr(1))
elif (c & 0xE0) == 0xC0:
c &= ~0xE0
c <<= 8
c += ord(self.readStr(1))
c <<= 8
c += ord(self.readStr(1))
elif (c & 0xF0) == 0xE0:
c &= ~0xF0
c <<= 8
c += ord(self.readStr(1))
c <<= 8
c += ord(self.readStr(1))
c <<= 8
c += ord(self.readStr(1))
elif (c & 0xF8) == 0xF0:
c = ord(self.readStr(1))
c <<= 8
c += ord(self.readStr(1))
c <<= 8
c += ord(self.readStr(1))
c <<= 8
c += ord(self.readStr(1))
return c

def writeStr(self, str):
n = 0;
while n < len(str):
r = self.sk.send(str[n:])
if r == 0: raise RuntimeError, "connection closed by remote end"
n += r

def readStr(self, length):
ret = ''
while len(ret) < length:
s = self.sk.recv(length - len(ret))
if s == '': raise RuntimeError, "connection closed by remote end"
ret += s
return ret


def open_socket(dst, port, secure=False):
s = None
res = socket.getaddrinfo(dst, port, socket.AF_UNSPEC, socket.SOCK_STREAM)
af, socktype, proto, canonname, sockaddr = res[0]
skt = socket.socket(af, socktype, proto)
if secure:
s = ssl.wrap_socket(skt, ssl_version=ssl.PROTOCOL_TLSv1_2, ciphers="ADH-AES128-SHA256") #ADH-AES128-SHA256
else:
s = skt
s.connect(sockaddr)
return s

def main():
s = None
dst = sys.argv[1]
user = "admin"
passw = ""
secure = False
port = 0

# use default username and pasword if not specified
if len(sys.argv) == 4:
user = sys.argv[2]
passw = sys.argv[3]
elif len(sys.argv) == 3:
user = sys.argv[2]

if (port==0):
port = 8729 if secure else 8728

s = open_socket(dst, port, secure)
if s is None:
print ('could not open socket')
sys.exit(1)

apiros = ApiRos(s);
if not apiros.login(user, passw):
return

inputsentence = []

while 1:
r = select.select([s, sys.stdin], [], [], None)
if s in r[0]:
# something to read in socket, read sentence
x = apiros.readSentence()

if sys.stdin in r[0]:
# read line from input and strip off newline
l = sys.stdin.readline()
l = l[:-1]

# if empty line, send sentence and start with new
# otherwise append to input sentence
if l == '':
apiros.writeSentence(inputsentence)
inputsentence = []
else:
inputsentence.append(l)

if __name__ == '__main__':
if len(sys.argv) == 1:
print "Usage: %s IP [user] [pass] [--secure]" % str(sys.argv[0])
else:
main()


debian@localhost:~/api-test$ ./api.py 10.0.0.1 admin ''
<<< /login
<<< 
>>> !done
>>> =ret=93b438ec9b80057c06dd9fe67d56aa9a
>>> 
<<< /login
<<< =name=admin
<<< =response=00e134102a9d330dd7b1849fedfea3cb57
<<< 
>>> !done
>>> 
/user/getall

<<< /user/getall
<<< 
>>> !re
>>> =.id=*1
>>> =disabled=no
>>> =name=admin
>>> =group=full
>>> =address=0.0.0.0/0
>>> =netmask=0.0.0.0
>>> 
>>> !done
>>> 

See also

API examples

API implementations in different languages, provided by different sources. They are not ordered in any particular order.

in the Wiki

on the MikroTik Forum

External sources