Skip to content

API and automation

← Documentation index

The web UI is a client of the same REST API you can drive yourself. Anything the interface does is available to a script.

Scoped API tokens

Issue tokens under Settings → Automation.

  • Prefix mtm_…, with read or write scope and an optional expiry
  • Shown once on creation — only a SHA-256 hash is stored
  • Mapped onto the role model: no token can perform admin actions or manage other tokens, regardless of scope
curl -sk https://manager.example.com/api/devices \
  -H "Authorization: Bearer mtm_your_token_here"

A token with read scope is rejected on any mutating request, so a monitoring integration cannot change your network even if the token leaks. It is also refused on requests that make a device do something, such as a firmware check or a wireless scan, and it gets device secrets masked (see below).

What read-only access can see

Viewers and read tokens see configuration, but not the secrets inside it. Wi-Fi keys, WireGuard private keys, SNMP communities and hotspot user passwords come back as ••••••••, as do Slack and Discord webhook URLs for everyone. Operators and admins get the real values, because the edit forms need them.

Session authentication

The UI authenticates with a JWT obtained from /api/auth/login, optionally followed by /api/auth/totp/verify when two-factor is enabled. Tokens are bearer credentials and are sent in the same Authorization header. The short-lived token /login returns while waiting for the two-factor code only works for /api/auth/totp/verify; it isn't a session.

Setting up two-factor again (/api/auth/totp/setup) doesn't touch the authenticator in use: the new secret replaces it only when /api/auth/totp/confirm accepts a code from it. Before 0.24.50 starting setup and abandoning it locked the user out.

For scripting, prefer an API token — it is scoped, revocable, and unaffected by password or 2FA changes.

Endpoints worth knowing

Endpoint Purpose
GET /api/devices Fleet inventory with status, model, versions and serials
POST /api/devices/:id/sync Force an immediate full collection for one device
GET /api/devices/:id/management-path How the manager reaches a device, with the reason for each hop
POST /api/devices/:id/preflight Analyse a change without applying it
POST /api/devices/:id/change-guard/check Can this device arm auto-revert right now? (ready, reason)
POST /api/devices/:id/change-guard/probe Report which safety mechanisms the device supports
GET /api/devices/:id/config-health Latest standing-audit findings
POST /api/devices/:id/check-api For a device added over SSH only: does the API answer now? If so it becomes an ordinary device (api: true); otherwise reason says why
GET /api/devices/:id/l3hw L3 hardware offloading state, routed VLANs and chip counters (supported: false on devices without a capable chip)
PUT /api/devices/:id/l3hw Turn offloading on or off ({"enabled": true}) under Change Guard; returns 409 with needs_acknowledgement when forward or NAT rules would be bypassed, so resend with "acknowledge_firewall": true
GET /api/topology Graph of devices, links, external nodes, and distrusted identifiers
GET /api/operations/insights The dashboard's "things to handle" feed
GET /api/sites Sites with device counts (see Sites)
GET /api/metrics/resources/latest Newest CPU and memory point per device: {"devices": {"<deviceId>": {"cpu_load", "memory_used", "memory_total", "time"}}}. cpu_load is a percentage, memory is in bytes, time is ISO 8601. Devices with no point in the last 15 minutes are omitted. Returns 503 if the metrics database cannot be read. Read-only; honours site scoping
GET /api/metrics/interfaces/latest Newest byte counters per device and interface: {"devices": {"<deviceId>": {"<interface>": {"rx_bytes", "tx_bytes", "time"}}}}. See Interface counters. Returns 503 if the metrics database cannot be read. Read-only; honours site scoping

Interface counters

rx_bytes and tx_bytes from /api/metrics/interfaces/latest are the router's cumulative counters (rx-byte / tx-byte from /interface/print stats), not per-interval deltas. They only grow, and reset to zero when the device reboots or the interface is recreated. Each value is the newest sample in the last 15 minutes, and time is when that sample was collected.

To get a rate, poll the endpoint, keep the previous rx_bytes and time for each interface, and compute (rx_now - rx_prev) / seconds_between_times. Take the interval from the two time values, not from when you polled, and if the counter went down treat it as a reset and discard that interval. Multiply by 8 for bits per second.

Scoping a request to one site

Collection endpoints accept an X-Site-Id header (or ?site_id=) that limits the response to devices in that site:

curl -H "Authorization: Bearer $TOKEN" -H "X-Site-Id: 2" \
     https://mtm.example.com/api/devices

Omitting it means unscoped, returning the whole fleet exactly as before sites existed — existing scripts and tokens need no change. Per-device endpoints (/api/devices/:id/...) are already scoped by the device itself and ignore the header.

One thing to know if you create devices via the API: a device joins the site the request was scoped to, falling back to the default site. A device belonging to no site is invisible whenever a site is selected, so this is set rather than left null.

preflight is the useful one for automation: it returns the same verdict the UI shows, so a pipeline can refuse its own change before touching the device. See Change Guard.

Applying a guarded change from a script

Guarded endpoints return 409 with lockout: true and a verdict when a change is predicted to sever management. To proceed anyway, resend with confirm_lockout: true — the change then runs under Change Guard, so a mistake still recovers itself.

# Refused, with an explanation
curl -sk -X PUT https://manager.example.com/api/devices/8/ports/ether1/vlan \
  -H "Authorization: Bearer mtm_…" -H 'Content-Type: application/json' \
  -d '{"pvid":99,"tagged_vlans":[],"untagged_vlans":[99]}'

# Accepted, relying on auto-revert
curl -sk -X PUT https://manager.example.com/api/devices/8/ports/ether1/vlan \
  -H "Authorization: Bearer mtm_…" -H 'Content-Type: application/json' \
  -d '{"pvid":99,"tagged_vlans":[],"untagged_vlans":[99],"confirm_lockout":true}'

A confirmed override requires auto-revert. If the device can't arm it, the request fails with 422 and code: "guard_required", and nothing is applied. The same applies when the prediction flagged a warning or couldn't read the device.

For a DELETE, which has no body, send ?confirm_lockout=true instead.

Writes to one device run one at a time. While another change to the same device is being applied or verified, a write returns 409 with code: "device_busy"; retry after a few seconds. It is told apart from a lockout by code, and a lockout always carries lockout: true.

Firewall, NAT, address-list, routing and WireGuard writes return a short message and the guard block rather than the updated table; read the table again with the matching GET.

A successful guarded change returns a guard block describing what happened: confirmed, auto_reverting, an unprotected_reason when a routine change ran without the safety net, or revert_may_fire_at when the change was kept but its revert couldn't be confirmed removed. Check it rather than relying on the HTTP status alone.

Restores (POST /api/backups/:id/restore, and Config History rollback) return a status of applied, reverting, nothing_applied or partial, with a message. The last two come with HTTP 422, plus failedLine and error saying where RouterOS stopped. See Backups → Restoring.

Previewing a change

Send an edit request with the header X-Preview-Changes: 1 to see what it would do without applying it. This is what the Review changes button uses. The request is validated and run against the live device as usual, but no RouterOS write is sent and nothing is saved. The reply is:

{
  "preview": {
    "steps": [
      {
        "action": "set",
        "path": "/ip/dns",
        "host": "192.168.0.51",
        "params": { "servers": "1.1.1.1,9.9.9.9" },
        "target": { "servers": "172.24.1.15,172.24.1.16", "allow-remote-requests": "false" },
        "changes": [{ "field": "servers", "from": "172.24.1.15,172.24.1.16", "to": "1.1.1.1,9.9.9.9" }],
        "unchanged": [],
        "cli": "/ip dns set servers=1.1.1.1,9.9.9.9"
      }
    ],
    "verdict": null
  }
}

verdict carries Change Guard's lockout prediction for the changes it analyses, including a critical one, which a preview reports rather than refusing. Secrets come back as (hidden). An invalid request returns its usual error. Requests that can't be previewed, because they act rather than configure (reboot, flush, tools) or don't go through the RouterOS API, return 400 with code: "preview_unsupported".

curl -sk -X PUT 'https://manager.example.com/api/network-services/dns?deviceId=8' \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -H 'X-Preview-Changes: 1' \
  -d '{"servers":["1.1.1.1","9.9.9.9"]}'

Webhooks

For push rather than poll, see Alerting → outbound webhooks. Deliveries are HMAC-SHA256 signed and respect alert rules, cooldowns and maintenance windows.