Cisco Catalyst SD-WAN Manager API

A working reference for every API area: what each endpoint family does, how the Manager serves the call, how to implement it, and where the data actually comes from.

Sources: DevNet API guide Release 26.1 (spec 26.1.0, 2026-01-06) and Release 20.18; Cisco Catalyst SD-WAN Getting Started Guide (updated July 2026); Cisco Control Components guide 26.x. Acronyms are spelled out on first use.

The Manager API is a REST (Representational State Transfer) interface for controlling, configuring, and monitoring devices in an overlay. Cisco frames four use cases: provisioning, network visibility, third-party tool integration, and Network as Code. Every path is prefixed with /dataservice, payloads are JSON except a few file uploads, and the calling role needs API access permission.

Where a call stops decides everything

The Manager sits between your tool and the routers and keeps three stores. A call either reads one of those stores or opens a live NETCONF (Network Configuration Protocol) session to the router. Cost, freshness, and pagination all follow from which one you hit.

Your client script · NMS · SDK SD-WAN Manager server-proxy · rate limits · RBAC Configuration DB templates · groups · users Device state cache BFD · OMP · control Statistics DB time series · approute · DPI Collection manager periodic sync from devices WAN edge router NETCONF over DTLS /device/<feature>?deviceId= — real-time, live pull, one device per call telemetry export offset/limit scrollId/count
Figure 1 — The three Manager-side stores and the one path that reaches the device. Blue paths are safe to poll; the amber path is for troubleshooting only.
Configuration DBTemplates, config groups, users, inventory. Paged with offset/limit.
Device state cacheCurrent state of every device, refreshed by the collection manager. Paged with count/startId.
Statistics DBHistorical counters exported by devices. Paged with scrollId/count.
Device (real-time)Live NETCONF query. Pagination decided by the device. CPU-intensive.
13OpenAPI categories in the 26.1 guide
~4,100operations across the full spec
3 + 1data stores plus the live device path

Authentication and sessions

Three methods are supported. API keys and JWT (JSON Web Token) arrived in 20.18; session cookies remain for backward compatibility. All three require the XSRF (cross-site request forgery) token on every non-GET request.

Which one to use. API key for long-lived service accounts (collectors, ITSM connectors) — no password in the pipeline, no refresh loop. JWT for short-lived scripts and CI/CD jobs where a bounded token life is a feature. Session cookies only for tooling you cannot change. DevNet's own lab standardises on the API key; verify the exact token-endpoint method (GET vs POST) on your Manager release, since DevNet's pages disagree with each other.

MethodLoginToken / cookieLifetimeLogout
JWTPOST /jwt/login — JSON body: username, password, optional duration (seconds)token claim → Authorization: Bearer; csrf claim → X-XSRF-TOKENDefault 1,800 s; range 1–604,800 s (7 days). Renew with POST /jwt/refreshNone — keep tokens short
API keyGenerate once in Manager: click your username → My profile → API token → Generate. No login call.Authorization: Bearer <apikey>; get XSRF with GET /dataservice/client/token (plain-string body) → X-XSRF-TOKENUntil revoked in the profile; rotate on a scheduleRevoke in profile
SessionPOST /j_security_check — form-urlencoded j_username, j_passwordJSESSIONID cookie; then GET /dataservice/client/token for XSRF24 h, 30-min idle; max 100 concurrent, least-recently-used evictedPOST /logout — always
Client Manager (server-proxy) Auth / RBAC store POST /jwt/login {username, password, duration} validate credentials · resolve usergroup features roles 200 {token, csrf, expiresIn} store both GET /dataservice/device (Bearer token) POST … Bearer + X-XSRF-TOKEN every non-GET needs XSRF POST /jwt/refresh before expiry
Figure 2 — JWT login. The csrf claim in the login response removes the extra call to /dataservice/client/token that the session method needs.
Service account Manager User profile store GET /dataservice/client/token Authorization: Bearer <apikey> validate key → resolve roles 200 "<xsrf-token>" (plain string, no JSON) POST … Bearer <apikey> + X-XSRF-TOKEN no login, no refresh, no logout
Figure 2b — API-key flow (20.18+). Two headers on every call; the only round trip is fetching the XSRF token.

Implementation

# Python — minimal JWT client with refresh and XSRF handling
import requests, time

class Manager:
    def __init__(self, host, user, pw, duration=1800, verify=True):
        self.base = f"https://{host}"; self.s = requests.Session(); self.s.verify = verify
        self.user, self.pw, self.duration = user, pw, duration
        self.login()

    def login(self):
        r = self.s.post(f"{self.base}/jwt/login",
                        json={"username": self.user, "password": self.pw, "duration": self.duration})
        r.raise_for_status(); body = r.json()
        self.token, self.csrf = body["token"], body.get("csrf")
        self.exp = time.time() + self.duration - 60
        self.s.headers.update({"Authorization": f"Bearer {self.token}",
                               "X-XSRF-TOKEN": self.csrf, "Content-Type": "application/json"})

    def refresh(self):
        r = self.s.post(f"{self.base}/jwt/refresh"); r.raise_for_status()
        self.token = r.json()["token"]; self.exp = time.time() + self.duration - 60
        self.s.headers["Authorization"] = f"Bearer {self.token}"

    def call(self, method, path, **kw):
        if time.time() > self.exp: self.refresh()
        r = self.s.request(method, f"{self.base}/dataservice{path}", **kw)
        if r.status_code == 401: self.login(); r = self.s.request(method, f"{self.base}/dataservice{path}", **kw)
        r.raise_for_status(); return r.json()

Session-method trap. A failed /j_security_check returns HTTP 200 with an HTML login page in the body. Check for an <html> tag before assuming success. Some administrative calls (login, logout) signal success only through the body.

Pagination, rate limits, and async tasks

Pagination follows the data source

StoreParametersExampleNotes
Configuration DBoffset, limitGET /template/feature?offset=1&limit=10Snapshot at call time; no consistency guarantee between calls
Statistics DBscrollId, countGET /statistics/approute/page?scrollId=…&count=10scrollId expires after 10 minutes; loop until hasMoreData is false
Device state cachecount, startIdGET /data/device/state/BFDSessions?count=1000pageInfo.moreEntries; pass returned endId as next startId
Device (real-time)—GET /device/bfd/sessions?deviceId=…Pagination decided by the device

Sorting and filtering are only available for device state and statistics APIs; the allowed field names for a table come from its /fields endpoint.

Rate limits

LimitValueWhere it bites
Bulk API (/data/device/statistics/)48 requests/minute per node (from 20.6); from 20.10.1 requests are distributed across a cluster, so effective limit = per-node × node countStatistics exports
Bulk statistics concurrency2 concurrent requestsParallel exporters
All other APIs100 requests/secondTight polling loops
Concurrent sessions250 on the ManagerMany scripts without logout
Cisco SD-WAN Cloud (SaaS fabric via API gateway, 20.15+)Real-time and bulk APIs are not available; other APIs limited to 10/s at the gateway and 5/s at the Manager; only certificate, configuration, inventory, monitoring and troubleshooting categories are exposedCisco-operated SaaS only — not Cisco-hosted control components

The per-node bulk limit is tunable with request nms server-proxy set ratelimit followed by request nms server-proxy restart on every node. Exhausting a limit returns 429 with no Retry-After header, so back off on your own schedule.

Every write is a task

Configuration pushes and device actions return a task ID, not a result. Poll GET /device/action/status/{taskId} and read the activity array until statusId settles.

Client Manager task engine Device(s) POST /v1/config-group/{id}/device/deploy 202 {parentTaskId} push config over control plane poll loop GET /device/action/status/{taskId} {summary.status, data[].statusId, activity[]} per-device result
Figure 3 — Asynchronous task pattern used by every configuration and action endpoint.
def wait_task(m, task_id, timeout=900):
    t0 = time.time()
    while time.time() - t0 < timeout:
        st = m.call("GET", f"/device/action/status/{task_id}")
        if st["summary"]["status"] == "done":
            return [(d["deviceIP"], d["statusId"]) for d in st["data"]]
        time.sleep(8)
    raise TimeoutError(task_id)

RBAC (role-based access control)

Each operation in the OpenAPI spec carries an x-roles-required value such as Template Deploy-write or Config Group > Device > Deploy-write. Build service accounts from those values: a read-only monitoring group for collectors, and a separate Settings-write account only for webhook rule management.

Administration and settings Configuration DB

Global Manager parameters, user and group management, tenants, software maintenance, and backup. Everything here reads and writes the configuration database; nothing touches a device except software install and reboot actions.

EndpointFunctionUse case
GET/POST /admin/userList and create usersService accounts for collectors and CI/CD (continuous integration / continuous delivery)
GET/POST /admin/usergroupGroups as lists of features with read/write flags (Alarms, Audit Log, Device Monitoring, Template Deploy…)Least-privilege roles
GET/POST /admin/resourcegroupScope users to sites or regionsRegional NOC access
GET/POST /settings/configuration/{type}Org name, Validator address, certificate authorization, statistics collection settingsDay-0 automation
POST /device/action/software, /install, /changepartitionImage upload, install, activate, set defaultUpgrade pipelines
GET/POST /tenant, /tenantbackupMultitenant operations (Provider view)MSP (managed service provider) onboarding
GET/PUT /statistics/settings/disable/devicelist/{indexName}Turn statistics collection on or off per index and device listReducing Manager load

How a user is created

  1. GET /admin/usergroup to confirm the group exists, or POST /admin/usergroup with the feature list.
  2. POST /admin/user with userName, password, group[], optional resGroupName.
  3. Verify with GET /admin/user; the password is never returned.
m.call("POST", "/admin/usergroup", json={
  "groupName": "monitoring-ro",
  "tasks": [{"feature": "Device Monitoring", "enabled": True, "read": True, "write": False},
            {"feature": "Alarms",            "enabled": True, "read": True, "write": False},
            {"feature": "Device Inventory",  "enabled": True, "read": True, "write": False}]})
m.call("POST", "/admin/user", json={"userName": "svc-collector", "password": "…", "group": ["monitoring-ro"]})

Device inventory Configuration DB

The foundation for everything else: which devices exist, whether they are reachable, and what state their certificates and control connections are in. All served from the Manager's own database.

EndpointReturnsUse case
GET /deviceAll connected devices: system-ip, host-name, reachability, site-id, version, BFD session counts, OMP peer counts, control connections, uptime, coordinates. 26.1 adds a site-id filter.CMDB (configuration management database) sync; health rollups
GET /system/device/controllersValidators, Controllers, Managers with certificate expiry, config sync stateCertificate watchdog
GET /system/device/vedgesAll WAN edges (vEdge and Catalyst IOS XE): cert state, configOperationMode (cli / vmanage), validityOnboarding audits
POST /system/device/fileuploadUpload the authorized serial fileZTP (zero-touch provisioning) pipelines
PUT /system/device/{uuid} / /decommission/{uuid}Set valid/invalid/staging, decommissionLifecycle
GET /system/device/bootstrap/device/{uuid}Generate bootstrap configurationDay-0 shipping
GET /device/modelsDevice models supported, interface naming per modelTemplate validation
GET /health/devices/overviewGood / fair / poor counts (new shape in 26.1)One-call fabric health
Client Manager Configuration DB GET /device?site-id=100 query inventory + last known reachability 200 {data:[{system-ip, reachability, …}]} no device is contacted
Figure 4 — Inventory reads never reach a device; reachability is the Manager's last known control-connection state.
devices = m.call("GET", "/device")["data"]
down = [d for d in devices if d.get("reachability") != "reachable"]
print(f"{len(devices)} devices, {len(down)} unreachable")

UX 1.0 — templates and centralized policy Writes config

UX (user experience) 1.0 is the template era: feature templates compose into device templates, which are attached to devices with per-device variables. Centralized policy is assembled and activated on the Controller. Still the majority of brownfield estates.

EndpointFunction
GET/POST /template/feature, /template/feature/object/{id}, /template/feature/typesFeature templates (system, VPN, interface, BFD…)
GET /template/device, POST /template/device/feature, POST /template/device/cli, GET /template/device/object/{id}Device templates; the object call returns full JSON (clone pattern)
POST /template/device/config/inputGenerate the variable sheet for templateId + deviceIds
POST /template/device/config/configPreview the rendered configuration
POST /template/device/config/attachfeature / /attachcliPush; returns a task ID
POST /template/config/device/mode/cliDetach (return to CLI mode)
/template/policy/list/*, /template/policy/definition/*, /template/policy/vsmart, /template/policy/vedge, /template/policy/securityLists (site, VPN, prefix, SLA, apps), definitions (control, data, app-route, cflowd, hub-and-spoke, mesh, zone-based firewall), centralized, localized, security policy
Client Manager Edge router 1 POST …/config/input {templateId, deviceIds} keys: //system/host-name, //system/system-ip, //system/site-id 2 POST …/config/config {device:[values]} rendered configuration for review / diff 3 POST …/config/attachfeature {id: taskId} NETCONF push · commit · verify success / rollback 4 GET /device/action/status/{taskId} (loop) statusId: success
Figure 5 — The four-step template attach. Steps 1–2 are safe dry runs; step 3 is the only write.
tpl = "a1b2…"; dev = "C8K-…uuid"
sheet = m.call("POST", "/template/device/config/input",
               json={"templateId": tpl, "deviceIds": [dev], "isEdited": False, "isMasterEdited": False})
row = sheet["data"][0]
row.update({"//system/host-name": "BR-100-R1", "//system/system-ip": "10.255.1.1", "//system/site-id": "100"})
task = m.call("POST", "/template/device/config/attachfeature",
              json={"deviceTemplateList": [{"templateId": tpl, "device": [row], "isEdited": False, "isMasterEdited": False}]})
print(wait_task(m, task["id"]))

UX 2.0 — configuration groups and feature profiles Writes config

The strategic direction for new deployments. A configuration group is a bundle of feature profiles (system, transport, service, policy-object, CLI, other); devices are associated to the group, receive per-device variables, and are deployed in one task. Policy groups and topology groups follow the same CRUD (create, read, update, delete) + associate + deploy pattern.

EndpointFunction
POST/GET /v1/config-group, PUT/DELETE /v1/config-group/{id}Group lifecycle
PUT /v1/config-group/{id}/device/associateBind devices
PUT /v1/config-group/{id}/device/variablesPer-device variable values
POST /v1/config-group/{id}/device/deployDeploy; returns task ID; role Config Group > Device > Deploy-write
/v1/policy-group, /v1/topology-groupSame pattern for policy and topology
/v1/network-hierarchyRegions and sites that groups reference
/v1/feature-profile/sdwan/{system|transport|service|policy-object|cli|other}/{profileId}/{feature}Parcel-level features, e.g. …/system/{id}/aaa, …/system/{id}/omp, …/transport/{id}/wan/vpn/{vpnId}/interface/ethernet, …/service/{id}/lan/vpn
/v1/feature-profile/sd-routing/…Same model for autonomous (non-SD-WAN) routers, e.g. …/cli/{cliId}/full-config/{fullConfigId}
1 Create profilessystem · transport · service 2 Create groupPOST /v1/config-group 3 Associate devicesPUT …/device/associate 4 Set variablesPUT …/device/variables 5 DeployPOST …/device/deploy task ID → GET /device/action/status/{id} → per-device statusId Steps 1–4 write only to the configuration DB. Step 5 is the only device contact.
Figure 6 — Configuration group lifecycle. Variables are typically injected from IPAM (IP address management) or an ITSM (IT service management) record.
gid = m.call("POST", "/v1/config-group", json={"name": "branch-std", "solution": "sdwan", "description": "Standard branch",
              "profiles": [{"id": SYSTEM_PROFILE}, {"id": TRANSPORT_PROFILE}, {"id": SERVICE_PROFILE}]})["id"]
m.call("PUT", f"/v1/config-group/{gid}/device/associate", json={"devices": [{"id": dev}]})
m.call("PUT", f"/v1/config-group/{gid}/device/variables",
       json={"solution": "sdwan", "devices": [{"device-id": dev,
             "variables": [{"name": "host_name", "value": "BR-100-R1"}, {"name": "system_ip", "value": "10.255.1.1"}, {"name": "site_id", "value": 100}]}]})
task = m.call("POST", f"/v1/config-group/{gid}/device/deploy", json={"devices": [{"id": dev}]})
wait_task(m, task["parentTaskId"])

SD-WAN services and partner integrations Writes config

SD-WAN services

  • /multicloud/* — cloud account onboarding (AWS, Azure, GCP), cloud gateway creation, VPC/VNet discovery and intent mapping.
  • /template/cloudx/* — Cloud OnRamp for SaaS application enablement and DIA (direct Internet access) probes.
  • /cloudservices/* — cloud service tokens and Microsoft 365 preferred-path settings (26.1 changed the M365 request schema).
  • 26.1 deprecates the /dca/* data-collection endpoints.

Partner integrations

  • Webex and Cisco Secure Access / SIG (secure Internet gateway) credentials and tunnel objects.
  • ThousandEyes Enterprise Agent enablement — a Docker container on IOS XE edges from 17.6.1, configured as a feature-profile parcel (…/other/{id}/thousandeyes).
  • SSE (security service edge) tunnel automation.
ClientPOST /multicloud/accounts Managerstores account, discovers VPCs Cloud provider APIcreates gateway VMs / transit Cloud gateway edgesjoin overlay via Validator Every step is a task; poll /device/action/status/{id}. The Manager, not your client, talks to the cloud provider.
Figure 7 — Multicloud gateway creation. Your client never holds cloud credentials; they are stored on the Manager.

Customer questions → endpoints Statistics DB

NOCs (network operations centres) don't ask for endpoint families; they ask "is site 90 healthy?" This table maps the common questions to the scalable endpoint and to the troubleshooting-only equivalent. Every scalable row is served from the Manager; none touches a device.

QuestionScalable endpoint (poll this)Troubleshooting-only equivalent
Which devices are at a site, and are they reachable?GET /device?site-id={id}—
Is each WAN edge healthy (good / fair / poor)?GET /statistics/devicehealth/overview/cpu?last_n_hours=1&limit=100 (optionally &site=). Manager-calculated score from CPU, memory, QoE, reachabilityGET /device/system/status?deviceId=
What did CPU, memory and disk look like over time?POST /statistics/system with entry_time and vdevice_name rulessame
How good is each tunnel path (latency / jitter / loss)?POST /statistics/approute/aggregation grouped by local_system_ip, local_color, remote_system_ip, remote_colorGET /device/app-route/statistics?deviceId=
Is a circuit (transport) up right now?GET /data/device/state/BFDSessions?count= grouped client-side by device + local-colorGET /device/bfd/sessions?deviceId=
How good was each circuit historically?POST /statistics/approute/aggregation grouped by vdevice_name, local_color—
How available was each site (downtime)?POST /statistics/nwa/details with type=site, sum down_time by site_id (NWA = network availability)—
Composite site health, ready-made?GET /statistics/sitehealth/common?last_n_hours=24&includeDetails=true—
Circuit availability across the fabric?POST /statistics/nwa/aggregation with type=link, grouped by system_ip, color—
Which applications are seen at a site?GET /statistics/dpi/applications?query={url-encoded JSON} with every site edge in one vdevice_name ruleGET /device/dpi/applications?deviceId=
Which app families use the most bandwidth?POST /statistics/dpi/aggregation grouped by family, sum octets—
Application health across sites?GET /statistics/perfmon/applications/sites/health?last_n_hours=24&includeUsage=true—

Tunnel is not circuit

Customers use the words interchangeably; the API doesn't. A tunnel (path) is one local device + colour to one remote device + colour. A circuit (transport) is one local device + colour, carrying many tunnels. Group your aggregation accordingly, and never group by local_color alone across devices — it merges every "mpls" in the fabric into one row.

BR-90 edgesystem-ip 10.0.90.1 circuit: biz-internet DC1-A10.2.1.210 · mpls DC1-B10.2.1.211 · biz-internet Hub-210.3.0.1 · public-internet tunnel 1 · BFD session tunnel 2 · BFD session tunnel 3 · BFD session 1 circuit = vdevice_name + local_color 1 tunnel = local ip + local color + remote ip + remote color
Figure 8a — One circuit carries many tunnels. Circuit availability = BFD sessions grouped by local colour; tunnel quality = app-route aggregation by the full four-field tuple.

The same device has four names

Inventory uses hyphenated fields, statistics use underscores, and real-time calls use deviceId. Mixing them up is the number one cause of "the API returns nothing" tickets.

Inventory field (GET /device)Used asWhere
site-idsite-id query parameter · site_id response field · site parameter on devicehealthInventory filter, statistics responses, health overview
system-ipdeviceIdEvery real-time call: /device/…?deviceId=
system-ipvdevice_nameEvery statistics query rule and response
system-ipsystem_ip / local_system_ipHealth overview, NWA, app-route aggregation
reachabilitySelection conditionSkip real-time calls to unreachable devices
personalityvedge vs controllersExclude controllers from edge-only queries

Never use a hostname, management IP, or chassis UUID where an operation asks for the system IP. And never build on /analytics/api/v4/dataservice/aggregate/* — those paths are used by the Manager UI but are not in the OpenAPI spec and can change without notice; every result they give has a documented /statistics equivalent above.

Device state — bulk State cache

The "what is up or down right now" view. The Manager's collection manager polls devices on its own schedule and stores current state; you read the cache, and the router is never touched. One request returns the whole fabric in batches.

EndpointDetail
GET /data/device/state/{DataType}?count=Ncount mandatory (1–10,000); data types are case-sensitive
Data typesBFDSessions · BGPNeighbor · Bridge · ControlConnection · ControlLocalProperty · ControlWanInterface · HardwareAlarms · HardwareEnvironment · HardwareInventory · Interface · OMPPeer · SystemStatus · System
PagingResponse pageInfo carries moreEntries and endId; call again with startId=endId
Per-device cached viewGET /device/{feature}/synced?deviceId= — same shape as real-time, served from the NMS (network management system) cache
Sync controlPOST /device/blockSync?blockSync=true|false stops or resumes the collection manager — if state looks stale, check this first
Client State cache Collection manager All devices background, independent of your calls periodic sync write state rows GET /data/device/state/BFDSessions?count=1000 {data[1000], pageInfo:{moreEntries:true, endId}} …?count=1000&startId=<endId> {data[…], pageInfo:{moreEntries:false}} no device is contacted
Figure 8 — Bulk state read. Your calls decouple from the device sync, which is why this path scales to thousands of routers.
def state(m, table, count=1000):
    rows, start = [], None
    while True:
        q = f"count={count}" + (f"&startId={start}" if start else "")
        r = m.call("GET", f"/data/device/state/{table}?{q}")
        rows += r["data"]; pi = r.get("pageInfo", {})
        if not pi.get("moreEntries"): return rows
        start = pi["endId"]

bfd = state(m, "BFDSessions")
down = [s for s in bfd if s.get("state") != "up"]

Statistics — export and query Statistics DB

Devices export counters to the Manager, which stores them in a time-series database. Two flavours share the same store: a bulk export for moving raw rows out, and a query/aggregation API that does the maths server-side and powers the Manager GUI charts.

Bulk export

ItemDetail
EndpointGET /data/device/statistics/{datatype}?startDate=&endDate=&count=&timeZone= — start and end mandatory, yyyy-MM-ddThh:mm:ss
Data typesalarm · approutestatsstatistics (tunnel loss/latency/jitter) · auditlog · cflowdstatistics · cloudxstatistics · deviceconfiguration · deviceevent · devicesystemstatusstatistics (CPU/memory) · dpistatistics · flowlogstatistics · interfacestatistics · wlanclientinfostatistics
Helpers…/{datatype}/doccount, …/{datatype}/fields; v2 interface stats at GET /v2/data/device/statistics/interfacestatistics
PagingscrollId in the response; pass it back until hasMoreData is false; expires after 10 minutes
Limits2 concurrent, 48/min per node

Discover fields before you filter

Every statistics table exposes GET /statistics/{table}/fields (fields you can return) and GET /statistics/{table}/query/fields (fields you can filter on). Field names differ between releases; hard-coding them without checking is how integrations break silently on upgrade.

fields = {f["property"] for f in m.call("GET", "/statistics/approute/fields")}
assert {"latency","jitter","loss_percentage","local_color"} <= fields

Query and aggregation

ItemDetail
Endpoint familyGET|POST /statistics/{table} plus /aggregation, /csv, /doccount, /fields, /page; 26.1 adds page, pageSize, sortBy parameters
Tablesapproute · interface · dpi · qos · flowlog · system (/cpu, /memory) · sitehealth · tunnelhealth · devicehealth · perfmon · fwall · urlf · ipsalert · umbrella · speedtest · qfp · qfpdrop · temperature · powerconsumption · endpointTracker · eiolte · wlanclientinfo · art · apphosting · sul · ppl/ctg and ppl/reordering (new in 26.1)
Query bodyquery (condition AND/OR + rules: field, type, value[], operator) · sort (field/order pairs) · fields · aggregation (histogram buckets + metrics)
Deprecated in 26.1/statistics/bfd /statistics/cflowd /statistics/device /statistics/system/stats
Client server-proxy Statistics DB Devices continuous telemetry export GET /data/device/statistics/approutestatsstatistics?…&count=10000 rate-limit check time-range scan, open scroll {data[10000], scrollId, hasMoreData:true} …?scrollId=<id>&count=10000 (≤10 min) {data[…], hasMoreData:false} no device is contacted
Figure 9 — Bulk statistics export with scrollId paging. The proxy enforces 2 concurrent / 48 per minute before the query runs.
# Bulk export of tunnel SLA rows for the last hour
from datetime import datetime, timedelta
end = datetime.utcnow(); start = end - timedelta(hours=1)
fmt = lambda t: t.strftime("%Y-%m-%dT%H:%M:%S")
path = f"/data/device/statistics/approutestatsstatistics?startDate={fmt(start)}&endDate={fmt(end)}&count=10000&timeZone=UTC"
rows, r = [], m.call("GET", path)
while True:
    rows += r["data"]
    if not r.get("hasMoreData"): break
    r = m.call("GET", f"/data/device/statistics/approutestatsstatistics?scrollId={r['scrollId']}&count=10000")

# Server-side aggregation: hourly mean latency per tunnel, last 24 h
q = {"query": {"condition": "AND", "rules": [
        {"field": "entry_time", "type": "date", "value": ["24"], "operator": "last_n_hours"},
        {"field": "vdevice_name", "type": "string", "value": ["10.255.1.1"], "operator": "in"}]},
     "aggregation": {"field": [{"property": "name", "sequence": 1}],
                     "histogram": {"property": "entry_time", "type": "hour", "interval": 1, "order": "asc"},
                     "metrics": [{"property": "latency", "type": "avg"}, {"property": "loss_percentage", "type": "avg"}]}}
agg = m.call("POST", "/statistics/approute/aggregation", json=q)["data"]

Alarms, events, and webhooks Alarm store

Alarms are correlated conditions with severity; events are the raw notifications behind them. Retrieving alarms over REST means frequent polling, so the Manager can push them instead: a notification rule with a webhook URL delivers an HTTP POST the moment a matching alarm is raised.

EndpointFunction
GET|POST /alarmsActive alarms; POST body filters by severity, time, site; 26.1 adds site-id, page, pageSize, sortBy
/alarms/aggregation, /alarms/severity/summary, /alarms/count, /alarms/topnCounts by severity (MINOR, MAJOR, MEDIUM, CRITICAL) over time buckets
GET /alarms/uuid/{alarm_uuid}, /alarms/notviewed, POST /alarms/markallasviewed?type=active|clearedSingle alarm and viewed state
POST /alarms/disabled?eventName=&time=Suppress an alarm type for 0–72 h (maintenance windows)
GET|POST /event, /event/aggregation, /event/severityRaw events with the same filters
GET /auditlogWho changed what, when
POST /notifications/rule, GET /notifications/rules, PUT /notifications/rule?ruleId=Webhook / e-mail rules; role Settings-write
GET /data/device/statistics/alarm/active?startDate&endDateBulk export of active alarms for a window
Admin client Alarm engine Device Your webhook POST /notifications/rule {severity[], webhookUrl} rule stored (one-time setup) BFD down event via control plane correlate → alarm POST https://your-host/hook {severity, message, devices[]} 200 OK (fast; queue the work) POST /alarms (backfill / reconcile hourly)
Figure 10 — Push instead of poll. The webhook is the primary path; /alarms queries reconcile anything missed.
m.call("POST", "/notifications/rule", json={
  "notificationRuleName": "noc-critical-major",
  "severity": ["Critical", "Major"],
  "alarmName": [],                      # empty = all alarm types
  "devicesAttached": [],                # empty = all devices; or [{"system-ip": "…"}]
  "webHookEnabled": True, "webhookUrl": "https://hooks.example.com/sdwan",
  "webhookUsername": "svc", "webhookPassword": "…",
  "emailEnabled": False})

# Backfill: critical alarms in the last 24 h for one site
q = {"query": {"condition": "AND", "rules": [
      {"field": "entry_time", "type": "date", "value": ["24"], "operator": "last_n_hours"},
      {"field": "severity", "type": "string", "value": ["Critical"], "operator": "in"}]}, "size": 1000}
alarms = m.call("POST", "/alarms?site-id=100", json=q)["data"]

Real-time monitoring Reaches the device

Real-time monitoring APIs query device state and information in real time. The Manager opens a NETCONF session to the router over its DTLS (Datagram Transport Layer Security) control connection, runs the query, and returns the result — one device per call. This is the only monitoring path that consumes CPU on the router, and it shares the control channel with configuration pushes and OMP (Overlay Management Protocol) updates.

The scale math: 10 endpoints × 500 routers × once a minute = 5,000 NETCONF sessions per minute — a self-inflicted outage on the Manager.

Deployment model matters. On Cisco SD-WAN Cloud (the Cisco-operated SaaS fabric accessed through an API gateway with an API key, Release 20.15+), real-time and bulk APIs are not exposed. On on-prem or Cisco-hosted control components, where you call your own Manager directly, they are available with the standard limits.

Engineer Manager DTLS tunnel Edge router GET /device/app-route/statistics?deviceId=… RBAC + no cache NETCONF <get> over DTLS router CPU spendscycles on the query XML reply, parsed to JSON 200 {data:[per-tunnel loss/latency/jitter]} shares the channel with config pushes and OMP
Figure 11 — A real-time call is a live round trip to the router. Contrast with figures 8 and 9, where no device is contacted.

The synced twins

Many real-time endpoints have a synced variant that returns the same shape from the Manager NMS cache: /device/system/status vs /device/system/synced/status, /device/bfd/sessions vs /device/bfd/synced/sessions, /device/control/connections vs /device/control/synced/connections, /hardware/alarms vs /hardware/synced/alarms. If you need one device's view without touching it, use the synced twin.

Endpoint catalog (26.1)

All are GET /dataservice/…?deviceId=<system-ip>. Cisco annotates most as "on vEdge routers only"; the same paths generally work on Catalyst IOS XE edges, but test each one — bridge, PPP, and dot1x are Viptela-OS specific.

Application-aware routing, app logs, ARP
  • device/app-route/sla-class — SLA classes operating on the router
  • device/app-route/statistics — traffic characteristics per operational data-plane tunnel
  • device/app/log/flow-count, device/app/log/flows — logged packet flows
  • device/arp — IPv4 ARP table; device/ndv6 — IPv6 neighbors
BFD, BGP, OSPF, IP forwarding
  • device/bfd/history, device/bfd/sessions, device/bfd/synced/sessions, device/bfd/summary, device/bfd/tloc
  • device/bgp/neighbors, device/bgp/routes, device/bgp/summary
  • device/ospf/database, /databasesummary, /databaseexternal, /interface, /neighbor, /process, /routes
  • device/ip/fib (26.1 also v4fib, v6fib), device/ip/routetable, device/ip/mfiboil, /mfibstats, /mfibsummary
  • device/ip/nat/filter, /nat/interface, /nat/interfacestatistics
Control plane, OMP, orchestrator (Validator)
  • device/control/connections, /synced/connections, /synced/connectionshistory, /localproperties, /synced/localproperties, /statistics, /summary, /waninterface, /synced/waninterface, /affinity/config, /affinity/status, /validdevices, /validvsmarts
  • device/omp/peers, /synced/peers, /routes/advertised, /routes/received, /tlocs/advertised, /tlocs/received, /services, /summary, /mcastautodiscoveradvt, /mcastautodiscoverrecv, /mcastroutesadvt, /mcastroutesrecv
  • device/orchestrator/connections, /connectionshistory, /localproperties, /summary, /validvedges, /validsmarts
  • tunnel/transport/connection — DTLS connection status to the Validator
Interfaces, tunnels, IPsec, QoS and policy
  • device/interface, device/interface/synced, /arp_stats, /error_stats, /pkt_size, /port_stats, /queue_stats, /stats
  • device/tunnel/statistics, device/tunnel/gre-keepalives
  • device/ipsec/inbound, /localsa, /outbound; device/security/information
  • device/policer; device/policy/accesslistassociations, /accesslistcounters, /accesslistnames, /accesslistpolicers, /approutepolicyfilter, /datapolicyfilter, /qosmapinfo, /qosschedulerinfo, /rewriteassociations
  • device/qfp/cpustat, device/qfp/memstat — QFP (quantum flow processor) data-plane load on IOS XE
Flow visibility: DPI, cflowd, CloudExpress
  • device/dpi/applications, /flows, /summary, /supported-applications (DPI = deep packet inspection)
  • device/cflowd/collector, /flows, /flows-count, /statistics, /template; IOS XE: device/cedgecflowd/app-fwd-cflowd-flows, /app-fwd-cflowd-v6-flows
  • device/cloudx/applications — best interface per CloudExpress application
System, hardware, software, users
  • device/system/status, device/system/synced/status; 26.1: device/system/info, device/featuresupport
  • hardware/alarms, hardware/synced/alarms, hardware/environment, hardware/synced/environment, hardware/synced/inventory, hardware/threshold
  • device/software, device/software/synced; device/reboothistory, device/reboothistory/synced; device/crashlog, device/crashlog/synced
  • device/users, device/vpn, device/vrrp, device/ntp/associations, device/ntp/peer
Access: cellular, WLAN, DHCP, dot1x, PPP, bridge, multicast
  • device/cellular/modem, /network, /profiles, /radio, /sessions, /status, /connection; 26.1: /ursp/routes, /ursp/rules
  • device/wlan/clients, /interfaces, /radios; 26.1: device/wireless/status
  • device/dhcp/interface, device/dhcpv6/interface, device/dhcp/server
  • device/dot1x/clients, device/dot1x/interfaces; device/ppp/interface; device/pppoe/session, /statistics
  • device/bridge/interface, /mac, /table
  • device/igmp/groups, /interface, /statistics, /summary; device/pim/interface, pim/neighbor, device/pim/rp-mapping, pim/statistics; device/multicast/replicator, /rpf, /topology, /tunnel
  • 26.1: device/dns/defense/info, device/dns/defense/device-registration; endpoint tracker family under the Real-Time Monitoring - Endpoint Tracker Service tag

Sanctioned implementation: an on-demand runbook

# Triggered by an engineer for ONE device. Never scheduled.
def site_snapshot(m, system_ip):
    q = f"?deviceId={system_ip}"
    return {
      "control": m.call("GET", "/device/control/connections" + q)["data"],
      "bfd":     m.call("GET", "/device/bfd/sessions" + q)["data"],
      "sla":     m.call("GET", "/device/app-route/statistics" + q)["data"],
      "ifaces":  m.call("GET", "/device/interface/error_stats" + q)["data"],
      "cpu":     m.call("GET", "/device/system/status" + q)["data"],
    }

Troubleshooting tools Reaches the device

Diagnostic operations that go beyond a single query: log bundles, path traces, utilities. Tagged in the spec as Troubleshooting Tools - Device Connectivity and Troubleshooting Tools - Network Wide Path Insight.

EndpointFunction
POST /device/tools/admintechGenerate an admin-tech bundle; 26.1 body accepts deviceIP, device-type, exclude-cores, exclude-tech, exclude-logs and a custom-commands array such as show version, show platform
GET /device/tools/admintechs, /device/tools/admintech/download/{filename}List and download bundles
GET /troubleshooting/control/{uuid}Troubleshoot control connections
GET /troubleshooting/devicebringup?uuid=Onboarding diagnostics
POST /device/tools/reset/interface/{deviceIP}Reset an interface
POST /device/tools/ping/{deviceIP}, /traceroute/{deviceIP}, /nslookup/{deviceIP}Connectivity utilities executed on the device
/stream/device/nwpi/*Network-Wide Path Insight: trace/start, trace/stop/{traceId}, traceHistory, exportTrace, importTrace, tasks/*
/stream/device/umts/*Underlay Measurement and Tracing Service sessions
/stream/device/log/*Live log streaming sessions (create, search, renew, download)
/stream/device/speedSpeed test session; returns sessionId, startTime, renewalTime
Engineer Manager Device POST /device/tools/admintech {deviceIP, commands[]} {fileName, taskId} request admin-tech (minutes of device CPU) tar.gz uploaded to Manager GET /device/tools/admintechs (poll) GET /device/tools/admintech/download/{fileName}
Figure 12 — Admin-tech generation is the heaviest device-side operation in the API; run it one device at a time.

Errors and resilience

HTTP status codes

CodeMessageMeaningClient action
200OKSuccess—
201CreatedNew resource created—
400Bad requestRequest was invalidFix the body; do not retry as-is
401UnauthorizedAuthentication missing or incorrectRe-authenticate, retry once
403ForbiddenUnderstood but not allowedCheck XSRF header and x-roles-required
404Not foundResource not foundCheck path / release (deprecated in 26.1?)
429Too many requestsRate limit exceededExponential backoff; no Retry-After is sent
500Internal server errorProblem with the serverRetry with backoff; capture body for TAC
503Service unavailableServer unable to complete requestBack off; check Manager cluster health

Two things the spec does not give you. Login and logout signal success or failure in the response body, not the status code — a failed session login is a 200 with an HTML page. And the OpenAPI document declares 400, 403 and 500 on nearly every operation with no body schema, while 401, 429 and 503 are documented only in prose and carry no rate-limit headers. Your client needs its own backoff and its own error parsing.

When the API returns 200 and nothing

An empty data array with HTTP 200 is not an error to the Manager. Work it top-down:

200, data: []statistics query 1 Widen window168 h, no device rule Data appearsdevice value mismatch Copy exact vdevice_namefrom a raw row 2 Inspect raw rowsPOST /statistics/{table} Still empty → not a query bugcollection off · no traffic · retention 403 instead?needs Monitoring-read roles Compare with Manager UIUI empty too → data issue If the UI has data and the API does not: capture release, device model, HTTP status, body and the exact URL sent — that is a TAC case.
Figure 14 — Empty-response decision tree. Separate a value mismatch from a genuine absence of data before touching the query logic.

Failure patterns and fixes

SymptomLikely causeFix
200 with HTML body on loginBad credentials or locked accountCheck for <html>; never parse as JSON
403 on POST that works as GETMissing X-XSRF-TOKENSend it on every non-GET
403 with valid tokenRole lacks the feature (e.g. Settings-write for webhook rules)Read the operation's x-roles-required; adjust the usergroup
429Exceeded 48/min bulk, 2 concurrent bulk, or 100/s generalSerialize bulk calls; backoff 2 s → 60 s
Empty data from state tablesSync blocked or statistics collection disabledCheck /device/blockSync and statistics settings
scrollId returns nothingExpired after 10 minutesRestart the time window
Task never completesDevice unreachable or variable errorRead activity[] in the task status
Real-time call times outDevice busy or control connection flappingUse the synced twin; check /device/control/synced/connections
Send request status? 2xx → parse bodycheck for <html> on login 401 → re-loginretry once, then fail 429 / 503 → wait2 s · 4 s · 8 s … 60 s, max 6 tries 400 / 403 / 404do not retry; log + alert
Figure 13 — Client retry policy. Only 401, 429, 500 and 503 are retried; 4xx client errors are surfaced immediately.

What changed from 20.18 to 26.1

The Monitoring and Troubleshooting changelog ends with an explicit statement that result-API changes broke backward compatibility. Integrations built against 20.x should be reviewed before an upgrade.

Deprecated

  • /statistics/bfd and all sub-paths
  • /statistics/cflowd and all sub-paths
  • /statistics/device and all sub-paths
  • /statistics/system/stats and all sub-paths
  • /accesstoken/…, /refreshtoken/…, /token/…, /device_authorization/… cloud token helpers
  • /dca/* data-collection endpoints (SD-WAN Services)

Deleted

  • /statistics/cflowd/applications, …/applications/summary, …/device/applications
  • POST /device/tier/{tierName} → replaced by POST /device/tier

New

  • /advisories/insecure-config/summary, /devices, /devices/{deviceIP}; /device/insecure-config
  • /statistics/ppl/ctg, /statistics/ppl/reordering (full query family)
  • /device/featuresupport, /device/wireless/status, /device/cellular/ursp/routes|rules, /device/dns/defense/*, /device/system/info, /device/ip/v4fib|v6fib
  • /statistics/download/{processType}/file/{token}/{fileName}

Changed

  • site-id filter across /alarms*, /event*, /notifications/rules, /device
  • page, pageSize, sortBy across most /statistics/{table}
  • /health/devices and /health/devices/overview response reshaped
  • GET /alarms/master operation renamed getMasterManagerState → getLeaderManagerState
  • POST /device/tools/admintech gained custom-commands
  • JWT authentication introduced in 20.18.1; session auth kept
NeedUse in 26.1Replace
Tunnel SLA / app-route trendingPOST /statistics/approute; bulk approutestatsstatistics/statistics/bfd
Flow / application visibility/statistics/dpi, /statistics/flowlog/statistics/cflowd/*
Device CPU / memory/statistics/system (/cpu, /memory); bulk devicesystemstatusstatistics/statistics/system/stats, /statistics/device

Guidance checklist

  1. Never schedule real-time calls. Per-device NETCONF pulls over the control plane belong in an on-demand runbook, scoped to one device or site.
  2. Poll bulk state for "now." One call per table returns the whole fabric. Five-minute cadence covers NOC dashboards; use /health/devices/overview for the headline.
  3. Export statistics in windows. Bulk /data/device/statistics/{table} with scrollId paging, two concurrent, 48/min per node; use /statistics/{table}/aggregation when the Manager can do the maths.
  4. Push alarms, don't pull them. Register /notifications/rule webhooks; use /alarms only for backfill.
  5. Build a resilient client. Short-lived JWT with refresh; XSRF on every non-GET; backoff on 429 (no Retry-After); re-auth once on 401; inspect the login body.
  6. Ramp load gradually and watch the Manager. Start with a low request count and interval, increase one dimension at a time, and watch every Manager node for CPU and memory, API latency and timeouts, 429 and 5xx rates, statistics-query and bulk-export duration, and device reachability. Stop when any of them moves. The rate limit is a ceiling, not a target.
  7. Least privilege. A read-only monitoring usergroup for collectors; Settings-write only for the account that manages webhook rules.
  8. Target 26.1 endpoint names now. Anything calling /statistics/bfd, /statistics/cflowd, /statistics/device or /statistics/system/stats will break on upgrade.
  9. Know which deployment model you have. On-prem and Cisco-hosted control components expose the full API against your own Manager. Cisco SD-WAN Cloud (the SaaS fabric reached through an API gateway) does not expose real-time or bulk APIs and limits the rest to 5–10 requests/second.
  10. Know when REST is the wrong tool. Sub-minute telemetry at scale is a job for SD-WAN telemetry data collection or the ThousandEyes Enterprise Agent, not a polling loop.
  11. Start from the companion kit. A Postman/Bruno collection (40 requests across auth, inventory, bulk state, statistics, alarms, real-time, tasks) and a Python manager_client.py with all three auth modes, retry policy and paging helpers ship alongside this guide as sdwan-api-field-guide-companion.zip.
  12. Don't reinvent the client. The catalystwan Python SDK, terraform-provider-sdwan, the Pulumi provider, the DevNet SD-WAN Reporting Tool and the two DevNet MCP (Model Context Protocol) servers already wrap these endpoints; Sastre handles configuration backup and restore.

Built from the Cisco DevNet Catalyst SD-WAN Manager API guides (Releases 20.18 and 26.1), the Cisco Catalyst SD-WAN Getting Started Guide, and the Cisco Control Components and Device Management Guide 26.x. Endpoint names reflect Release 26.1; verify against your Manager's /apidocs before production use.