NetStacksNetStacks

Devices API

REST CRUD for managed network devices: list, create, update, delete, bulk import, NetBox sources, enrichment reads, and credential testing.

Overview

The Devices API manages the network device inventory in the NetStacks Controller. Use it to list, create, update, and delete devices, import devices in bulk from CSV or JSON, sync from NetBox, read enrichment data (interfaces, neighbors, ARP/MAC tables), and test device credentials.

All device endpoints are mounted under /api/devices and require a Bearer token. Both read and write endpoints are guarded by the devices.access permission (in the source this is the OperatorPermission guard, which resolves to devices.access). The permission catalog also defines a finer-grained devices.manage key for create / edit / delete and NetBox sync; assign it to roles that should manage the inventory. See API Authentication to obtain a token.

Controller feature

The Devices API is part of the NetStacks Controller. Every request is scoped to the caller's organization, so you only ever see and modify devices in your own org.

Endpoints

All endpoints accept and return JSON. Device IDs are UUIDs assigned by the server on creation. Errors are returned as a flat {"error": "message"} body with the HTTP status code carrying the meaning (400, 401, 403, 404, 409, 500).

Core CRUD

MethodPathDescription
GET/api/devicesList devices with filtering and pagination
POST/api/devicesCreate a device (returns 201)
GET/api/devices/:idGet a single device by UUID
PUT/api/devices/:idUpdate a device (partial)
DELETE/api/devices/:idDelete a device
GET/api/devices/with-backup-infoList devices including latest backup status

Import, sync & credential testing

MethodPathDescription
POST/api/devices/import/csvBulk import from a CSV body
POST/api/devices/import/jsonBulk import from a JSON array
POST/api/devices/sync/netboxAd-hoc NetBox sync (URL + token in body)
POST/api/devices/sync/netbox/testTest a NetBox URL/token and count matches
GET/api/devices/sync/historyNetBox sync history
POST/api/devices/:device_id/test-credentialTest credentials for one device
POST/api/devices/bulk/test-credentialsTest credentials for many devices
POST/api/devices/:device_id/exec-commandRun a single command on a device

Enrichment & polling

MethodPathDescription
GET/api/devices/:id/interfacesDiscovered interfaces
GET/api/devices/:id/neighborsLLDP/CDP neighbors
GET/api/devices/:id/mac-tableMAC address table (limit, max 10000)
GET/api/devices/:id/arp-tableARP table (limit, max 10000)
GET/api/devices/:id/vlansVLAN list
GET/api/devices/:id/statusLatest device status snapshot
GET/api/devices/:id/poll-historyPoll history (limit, max 500)
POST/api/devices/:id/pollTrigger an out-of-cycle poll (returns 202)

NetBox sources

MethodPathDescription
GET / POST/api/devices/sources/netboxList / create a saved NetBox source
GET / PUT / DELETE/api/devices/sources/netbox/:source_idRead / update / delete a source
POST/api/devices/sources/netbox/:source_id/syncSync from a saved source
POST/api/devices/sources/netbox/:source_id/testTest a saved source

List query parameters

  • limit (int) — Items per page, default 50, capped at 100
  • offset (int) — Pagination offset, default 0
  • device_type (string) — Filter by device type (e.g., cisco_ios)
  • site (string) — Filter by site name
  • source (string) — Filter by source (manual, netbox, csv)

Device Model

POST /api/devices accepts the fields below. Only name, host, and device_type are required; everything else is optional with the defaults noted.

FieldTypeNotes
namestringRequired. Unique within the org.
hoststringRequired. IP or hostname.
device_typestringRequired. e.g. cisco_ios.
portintDefault 22.
protocolstringDefault "ssh".
sourcestringDefault "manual".
descriptionstringOptional free text.
manufacturer, model, platformstringOptional inventory metadata.
sitestringFree-text site name (back-compat).
site_iduuidFK to a managed site.
serial_number, asset_tagstringOptional asset fields.
tagsstring[]Stored as JSON.
metadataobjectArbitrary JSON, defaults to {}.
default_credential_iduuidPrimary credential.
snmp_credential_iduuidSNMP credential.
automation_transportstringe.g. NETCONF / gNMI transport.
automation_portintAutomation transport port.
automation_credential_iduuidCredential for the automation transport.
netbox_idintSource NetBox device ID, if synced.
poll_enabledboolDefault false. Enrolls the device in the background poller.
poll_interval_minutesintPoll cadence when enabled.
Automation transport vs SSH

port/protocol/default_credential_id drive the interactive SSH path. The automation_* fields let model-driven platforms (NETCONF, gNMI, REST) use a separate transport, port, and credential for structured config operations.

Step-by-Step Guide

1. Authenticate

Obtain a JWT from the Authentication API and send it on every request as Authorization: Bearer <token>.

2. List devices with filtering

Call GET /api/devices with optional device_type, site, and source filters. Results are paginated and the response carries a total count.

3. Get a single device

Call GET /api/devices/:id with the device UUID.

4. Create a device

POST /api/devices with at least name, host, and device_type. The server fillsport=22, protocol="ssh", and source="manual" if omitted, and returns 201 Created with the full device.

5. Update a device

PUT /api/devices/:id with only the fields you want to change.

6. Delete a device

DELETE /api/devices/:id.

7. Bulk import

POST /api/devices/import/json with an array, or /import/csv with a CSV body. Both return an import summary with per-row errors.

Code Examples

Create a Device

create-device.shbash
curl -X POST https://netstacks.example.net/api/devices \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "core-rtr-01.dc1.example.net",
    "host": "10.0.1.1",
    "port": 22,
    "device_type": "cisco_ios",
    "manufacturer": "Cisco",
    "model": "ISR 4451-X",
    "platform": "ios-xe",
    "site": "dc1-east",
    "tags": ["core", "wan"],
    "default_credential_id": "c8a7b6d5-e4f3-2a1b-9c8d-7e6f5a4b3c2d",
    "poll_enabled": true,
    "poll_interval_minutes": 60
  }'

# Response (201 Created) — full Device object:
# {
#   "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
#   "org_id": "a1b2c3d4-0000-0000-0000-000000000000",
#   "name": "core-rtr-01.dc1.example.net",
#   "host": "10.0.1.1",
#   "port": 22,
#   "protocol": "ssh",
#   "device_type": "cisco_ios",
#   "description": null,
#   "manufacturer": "Cisco",
#   "model": "ISR 4451-X",
#   "platform": "ios-xe",
#   "site": "dc1-east",
#   "serial_number": null,
#   "asset_tag": null,
#   "source": "manual",
#   "netbox_id": null,
#   "tags": ["core", "wan"],
#   "metadata": {},
#   "default_credential_id": "c8a7b6d5-e4f3-2a1b-9c8d-7e6f5a4b3c2d",
#   "snmp_credential_id": null,
#   "automation_transport": null,
#   "automation_port": null,
#   "automation_credential_id": null,
#   "site_id": null,
#   "poll_enabled": true,
#   "poll_interval_minutes": 60,
#   "created_at": "2026-03-10T14:30:00Z",
#   "updated_at": "2026-03-10T14:30:00Z"
# }
create-device.pypython
import requests

base_url = "https://netstacks.example.net/api"
headers = {"Authorization": "Bearer eyJhbGciOiJIUzI1NiIs..."}

# Create a device — name, host, device_type are required;
# port (22) and protocol ("ssh") default if omitted.
device = requests.post(f"{base_url}/devices", headers=headers, json={
    "name": "core-rtr-01.dc1.example.net",
    "host": "10.0.1.1",
    "device_type": "cisco_ios",
    "site": "dc1-east",
    "default_credential_id": "c8a7b6d5-e4f3-2a1b-9c8d-7e6f5a4b3c2d",
    "poll_enabled": True,
})
device.raise_for_status()
print(f"Created device: {device.json()['id']}")

List Devices with Filters

list-devices.shbash
# List Cisco IOS devices in dc1-east, first 25 results
curl "https://netstacks.example.net/api/devices?device_type=cisco_ios&site=dc1-east&limit=25" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

# Response:
# {
#   "devices": [
#     {
#       "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
#       "org_id": "a1b2c3d4-0000-0000-0000-000000000000",
#       "name": "core-rtr-01.dc1.example.net",
#       "host": "10.0.1.1",
#       "port": 22,
#       "device_type": "cisco_ios",
#       "manufacturer": "Cisco",
#       "model": "ISR 4451-X",
#       "site": "dc1-east",
#       "source": "manual",
#       "default_credential_id": "c8a7b6d5-e4f3-2a1b-9c8d-7e6f5a4b3c2d",
#       "connect_commands": [],
#       "created_at": "2026-03-10T14:30:00Z",
#       "updated_at": "2026-03-10T14:30:00Z"
#     }
#   ],
#   "total": 42,
#   "limit": 25,
#   "offset": 0
# }
list-devices.pypython
# List devices filtered by type and site
devices = requests.get(
    f"{base_url}/devices",
    headers=headers,
    params={"device_type": "cisco_ios", "site": "dc1-east", "limit": 25},
)
data = devices.json()
print(f"Found {data['total']} Cisco IOS devices in dc1-east")
for d in data["devices"]:
    print(f"  {d['name']} ({d['host']})")
list-devices.jsjson
// List devices filtered by type and site (capped at 100 per page)
const params = new URLSearchParams({
  device_type: "cisco_ios",
  site: "dc1-east",
  limit: "25",
});
const resp = await fetch(`https://netstacks.example.net/api/devices?${params}`, {
  headers: { Authorization: "Bearer eyJhbGciOiJIUzI1NiIs..." },
});
const list = await resp.json();
console.log(`Found ${list.total} Cisco IOS devices in dc1-east`);
list.devices.forEach((d) => console.log(`  ${d.name} (${d.host})`));

Update a Device

update-device.shbash
curl -X PUT https://netstacks.example.net/api/devices/f47ac10b-58cc-4372-a567-0e02b2c3d479 \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{"site": "dc2-west", "port": 2222, "poll_enabled": false}'
# Response: 200 OK with the full updated Device

Delete a Device

delete-device.shbash
curl -X DELETE https://netstacks.example.net/api/devices/f47ac10b-58cc-4372-a567-0e02b2c3d479 \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Bulk Import (JSON)

bulk-import.shbash
curl -X POST https://netstacks.example.net/api/devices/import/json \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '[
    {"name": "dist-sw-01.dc1", "host": "10.0.10.1", "device_type": "cisco_ios", "site": "dc1-east"},
    {"name": "dist-sw-02.dc1", "host": "10.0.10.2", "device_type": "cisco_ios", "site": "dc1-east"},
    {"name": "fw-01.dc1", "host": "10.0.20.1", "device_type": "paloalto_panos", "site": "dc1-east"}
  ]'

# Response:
# {
#   "imported": 3,
#   "failed": 0,
#   "errors": []
# }
#
# On partial failure each error has row, name, and a message, e.g.:
# { "imported": 2, "failed": 1,
#   "errors": [ { "row": 1, "name": "dist-sw-02.dc1",
#                 "error": "Device with this name already exists" } ] }
bulk-import.pypython
# Bulk import from a list. Each row accepts name, host, port (default 22),
# device_type, and optional manufacturer/model/platform/site/description.
devices_to_import = [
    {"name": "dist-sw-01.dc1", "host": "10.0.10.1", "device_type": "cisco_ios", "site": "dc1-east"},
    {"name": "fw-01.dc1", "host": "10.0.20.1", "device_type": "paloalto_panos", "site": "dc1-east"},
]
resp = requests.post(f"{base_url}/devices/import/json", headers=headers, json=devices_to_import)
result = resp.json()
print(f"Imported {result['imported']} devices, {result['failed']} failed")
for err in result["errors"]:
    print(f"  row {err['row']} ({err.get('name')}): {err['error']}")

Test Credentials in Bulk

test-credentials.shbash
curl -X POST https://netstacks.example.net/api/devices/bulk/test-credentials \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{"device_ids": ["f47ac10b-58cc-4372-a567-0e02b2c3d479"], "timeout_seconds": 10}'

# Omit device_ids to test every device in the org.
# Response:
# {
#   "total": 1,
#   "succeeded": 1,
#   "failed": 0,
#   "duration_ms": 842,
#   "results": [
#     {
#       "device_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
#       "device_name": "core-rtr-01.dc1.example.net",
#       "host": "10.0.1.1",
#       "port": 22,
#       "success": true,
#       "message": "Authentication succeeded",
#       "duration_ms": 842
#     }
#   ]
# }

Enrichment & Polling

When polling is enabled (poll_enabled: true), the Controller's background poller collects operational state and exposes it through read-only enrichment endpoints. You can also trigger an out-of-cycle poll on demand.

enrichment.shbash
# Read discovered interfaces, neighbors, and tables
curl https://netstacks.example.net/api/devices/$ID/interfaces -H "$AUTH"
curl https://netstacks.example.net/api/devices/$ID/neighbors  -H "$AUTH"
curl "https://netstacks.example.net/api/devices/$ID/mac-table?limit=500" -H "$AUTH"
curl "https://netstacks.example.net/api/devices/$ID/arp-table?limit=500" -H "$AUTH"
curl https://netstacks.example.net/api/devices/$ID/vlans        -H "$AUTH"

# Latest status snapshot + poll history
curl https://netstacks.example.net/api/devices/$ID/status       -H "$AUTH"
curl "https://netstacks.example.net/api/devices/$ID/poll-history?limit=50" -H "$AUTH"

# Trigger an out-of-cycle poll (returns 202 Accepted; runs async)
curl -X POST https://netstacks.example.net/api/devices/$ID/poll -H "$AUTH"

mac-table and arp-table accept a limit query parameter (default 1000, max 10000); poll-history defaults to 50 and is capped at 500. The poll trigger only enqueues a request and returns 202; results land in the enrichment endpoints once the poll completes. If the poll service is not running or its queue is full, the trigger returns 503.

Run a Single Command

POST /api/devices/:device_id/exec-command runs one command over the device's configured transport and returns the output. timeout_secs defaults to 30 (clamped 1–300).

exec-command.shbash
curl -X POST https://netstacks.example.net/api/devices/$ID/exec-command \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{"command": "show version", "timeout_secs": 30}'

# Response:
# {
#   "success": true,
#   "output": "Cisco IOS XE Software, Version 17.09.04a ...",
#   "error": null,
#   "execution_time_ms": 1184
# }

NetBox Sources

Save reusable NetBox sources so you can sync inventory on a schedule without resending the URL and token each time. A source stores the URL, an (encrypted, never returned) token, device filters, verify_ssl, and optional auto-sync settings.

netbox-sources.shbash
# Create a saved NetBox source
curl -X POST https://netstacks.example.net/api/devices/sources/netbox \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Primary NetBox",
    "url": "https://netbox.example.net",
    "token": "0123456789abcdef0123456789abcdef01234567",
    "verify_ssl": true,
    "device_filters": {"status": "active", "has_primary_ip": true},
    "auto_sync_enabled": true,
    "sync_interval_hours": 6
  }'

# Test the saved source (counts matching devices, never imports)
curl -X POST https://netstacks.example.net/api/devices/sources/netbox/$SOURCE_ID/test -H "$AUTH"

# Run a sync from the saved source
curl -X POST https://netstacks.example.net/api/devices/sources/netbox/$SOURCE_ID/sync -H "$AUTH"
# Sync response: { "added": 12, "updated": 3, "unchanged": 40,
#                  "failed": 0, "removed": 1, "errors": [], "duration_ms": 5210 }

For an ad-hoc, one-off sync without saving a source, use POST /api/devices/sync/netbox with {"netbox_url", "token", "filters"} in the body. See NetBox Integration for filter details.

Config Backups

Config backups are not part of the Devices API. They live under the config router. Trigger a backup for many devices at once with POST /api/config/devices/bulk-pull, or pull a single device with POST /api/config/devices/:device_id/pull.

config-backups.shbash
# Bulk pull configs for a set of devices
curl -X POST https://netstacks.example.net/api/config/devices/bulk-pull \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{"device_ids": ["f47ac10b-58cc-4372-a567-0e02b2c3d479"]}'

# Response:
# {
#   "succeeded": 1,
#   "failed": 0,
#   "errors": []
# }

# Pull a single device's config
curl -X POST "https://netstacks.example.net/api/config/devices/$ID/pull" -H "$AUTH"

Stored configs are then available via /api/config/devices/:device_id/configs (and /configs/latest, /configs/:version, /diff). See Config Snapshots and Config Diffs for the UI workflow.

Questions & Answers

What permission do device endpoints require?
All device read and write endpoints are guarded by the devices.access permission (the OperatorPermission guard in source). The permission catalog also defines devices.manage for create/edit/delete and NetBox sync; grant it to roles that should manage the inventory. There is no permission named ManageDevices.
How do I list all devices?
GET /api/devices. Results are paginated with limit (default 50, max 100) and offset. The response is {"devices", "total", "limit", "offset"}.
How do I filter devices by type or site?
Add query parameters: /api/devices?device_type=cisco_ios&site=dc1-east. Supported filters are device_type, site, and source.
What fields can I set when creating a device?
name, host, and device_type are required. port defaults to 22, protocol to "ssh", source to "manual". Optional fields include description, manufacturer, model, platform, site/site_id, serial_number, asset_tag, tags, metadata, default_credential_id, snmp_credential_id, the automation_* fields, poll_enabled, and poll_interval_minutes.
How do I bulk import devices?
POST /api/devices/import/json with an array of device rows, or POST /api/devices/import/csv with a CSV body. Both return {"imported", "failed", "errors"} where each error has row, name, and error.
What device types are supported?
NetStacks supports many device types including cisco_ios, cisco_nxos, cisco_asa, juniper_junos, arista_eos, paloalto_panos, fortinet_fortios, linux, and more. The device type determines how NetStacks connects and parses output. See Device Types.
How do I back up device configs via the API?
Use the config router, not the Devices API: POST /api/config/devices/bulk-pull for many devices or POST /api/config/devices/:device_id/pull for one. See the Config Backups section above.
How do I test device credentials?
POST /api/devices/bulk/test-credentials with an optional device_ids array (omit it to test all devices) and an optional timeout_seconds. The response includes per-device success, message, and duration_ms.

Troubleshooting

400 Bad Request

The request body is missing a required field (name, host, or device_type) or contains an invalid value. The Devices API returns a flat {"error": "message"} body — there is no 422 response and no field-level details object. See Error Codes.

401 / 403

401 means the Bearer token is missing or invalid. 403 means the token lacks the required device permission. Reauthenticate via the Authentication API and confirm the role grants devices.access (and devices.manage for writes).

404 Device Not Found

The UUID does not exist or belongs to a different organization. Requests are scoped to the caller's org, so a device from another org appears as not found.

409 Conflict (Duplicate Name)

A device with the same name already exists in your organization. Device names must be unique. Choose a different name or update the existing device.

Credential Test Failing After Creation

If a device was created but credential tests fail, verify the host and port are correct, the device is reachable from the Controller, and the assigned credential has the correct username/password or SSH key. Use POST /api/devices/:device_id/test-credential to isolate the issue.

503 on Poll Trigger

POST /api/devices/:id/poll returns 503 when the background poll service is not running or its queue is full. Try again shortly, or confirm the poller is enabled on the Controller.