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.
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
| Method | Path | Description |
|---|---|---|
| GET | /api/devices | List devices with filtering and pagination |
| POST | /api/devices | Create a device (returns 201) |
| GET | /api/devices/:id | Get a single device by UUID |
| PUT | /api/devices/:id | Update a device (partial) |
| DELETE | /api/devices/:id | Delete a device |
| GET | /api/devices/with-backup-info | List devices including latest backup status |
Import, sync & credential testing
| Method | Path | Description |
|---|---|---|
| POST | /api/devices/import/csv | Bulk import from a CSV body |
| POST | /api/devices/import/json | Bulk import from a JSON array |
| POST | /api/devices/sync/netbox | Ad-hoc NetBox sync (URL + token in body) |
| POST | /api/devices/sync/netbox/test | Test a NetBox URL/token and count matches |
| GET | /api/devices/sync/history | NetBox sync history |
| POST | /api/devices/:device_id/test-credential | Test credentials for one device |
| POST | /api/devices/bulk/test-credentials | Test credentials for many devices |
| POST | /api/devices/:device_id/exec-command | Run a single command on a device |
Enrichment & polling
| Method | Path | Description |
|---|---|---|
| GET | /api/devices/:id/interfaces | Discovered interfaces |
| GET | /api/devices/:id/neighbors | LLDP/CDP neighbors |
| GET | /api/devices/:id/mac-table | MAC address table (limit, max 10000) |
| GET | /api/devices/:id/arp-table | ARP table (limit, max 10000) |
| GET | /api/devices/:id/vlans | VLAN list |
| GET | /api/devices/:id/status | Latest device status snapshot |
| GET | /api/devices/:id/poll-history | Poll history (limit, max 500) |
| POST | /api/devices/:id/poll | Trigger an out-of-cycle poll (returns 202) |
NetBox sources
| Method | Path | Description |
|---|---|---|
| GET / POST | /api/devices/sources/netbox | List / create a saved NetBox source |
| GET / PUT / DELETE | /api/devices/sources/netbox/:source_id | Read / update / delete a source |
| POST | /api/devices/sources/netbox/:source_id/sync | Sync from a saved source |
| POST | /api/devices/sources/netbox/:source_id/test | Test a saved source |
List query parameters
limit(int) — Items per page, default 50, capped at 100offset(int) — Pagination offset, default 0device_type(string) — Filter by device type (e.g.,cisco_ios)site(string) — Filter by site namesource(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.
| Field | Type | Notes |
|---|---|---|
| name | string | Required. Unique within the org. |
| host | string | Required. IP or hostname. |
| device_type | string | Required. e.g. cisco_ios. |
| port | int | Default 22. |
| protocol | string | Default "ssh". |
| source | string | Default "manual". |
| description | string | Optional free text. |
| manufacturer, model, platform | string | Optional inventory metadata. |
| site | string | Free-text site name (back-compat). |
| site_id | uuid | FK to a managed site. |
| serial_number, asset_tag | string | Optional asset fields. |
| tags | string[] | Stored as JSON. |
| metadata | object | Arbitrary JSON, defaults to {}. |
| default_credential_id | uuid | Primary credential. |
| snmp_credential_id | uuid | SNMP credential. |
| automation_transport | string | e.g. NETCONF / gNMI transport. |
| automation_port | int | Automation transport port. |
| automation_credential_id | uuid | Credential for the automation transport. |
| netbox_id | int | Source NetBox device ID, if synced. |
| poll_enabled | bool | Default false. Enrolls the device in the background poller. |
| poll_interval_minutes | int | Poll cadence when enabled. |
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
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"
# }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 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 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 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
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 DeviceDelete a Device
curl -X DELETE https://netstacks.example.net/api/devices/f47ac10b-58cc-4372-a567-0e02b2c3d479 \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Bulk Import (JSON)
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 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
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.
# 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).
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.
# 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.
# 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.accesspermission (theOperatorPermissionguard in source). The permission catalog also definesdevices.managefor create/edit/delete and NetBox sync; grant it to roles that should manage the inventory. There is no permission namedManageDevices. - How do I list all devices?
GET /api/devices. Results are paginated withlimit(default 50, max 100) andoffset. 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 aredevice_type,site, andsource. - What fields can I set when creating a device?
name,host, anddevice_typeare required.portdefaults to 22,protocolto"ssh",sourceto"manual". Optional fields includedescription,manufacturer,model,platform,site/site_id,serial_number,asset_tag,tags,metadata,default_credential_id,snmp_credential_id, theautomation_*fields,poll_enabled, andpoll_interval_minutes.- How do I bulk import devices?
POST /api/devices/import/jsonwith an array of device rows, orPOST /api/devices/import/csvwith a CSV body. Both return{"imported", "failed", "errors"}where each error hasrow,name, anderror.- 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-pullfor many devices orPOST /api/config/devices/:device_id/pullfor one. See the Config Backups section above. - How do I test device credentials?
POST /api/devices/bulk/test-credentialswith an optionaldevice_idsarray (omit it to test all devices) and an optionaltimeout_seconds. The response includes per-devicesuccess,message, andduration_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.
Related Features
- API Authentication — Obtain the Bearer tokens required for all device API calls.
- Templates API — Build configuration templates to render against devices.
- Stacks API — Deploy template stacks to multiple devices at once.
- Error Codes — Full reference for API error responses.
- Adding Devices — Add devices through the web UI.
- NetBox Integration — Sync inventory from NetBox.
- Config Snapshots — Capture and store device configs.
- Device Types — Supported platforms and what each type controls.