22 KiB
NetBox REST API Guide
NetBox Version: 4.3.0 API Documentation: https://netboxlabs.com/docs/netbox/en/stable/
Complete reference for working with the NetBox REST API, including authentication, common operations, filtering, pagination, and error handling patterns for the Virgo-Core infrastructure.
Table of Contents
- Quick Start
- Authentication
- API Endpoints Structure
- Common Operations
- Filtering and Search
- Pagination
- Bulk Operations
- Error Handling
- Rate Limiting
- Python Client (pynetbox)
- Best Practices
- Security
Quick Start
Using curl
# Get all sites
curl -H "Authorization: Token YOUR_API_TOKEN" \
https://netbox.spaceships.work/api/dcim/sites/
# Create a new IP address
curl -X POST \
-H "Authorization: Token YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"address": "192.168.3.10/24",
"dns_name": "docker-01-nexus.spaceships.work",
"status": "active",
"tags": ["production-dns", "terraform"]
}' \
https://netbox.spaceships.work/api/ipam/ip-addresses/
Using pynetbox (Python)
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = ["pynetbox>=7.0.0", "infisical-python>=2.3.3"]
# ///
import pynetbox
from infisical import InfisicalClient
# Get token from Infisical (following Virgo-Core security pattern)
client = InfisicalClient()
token = client.get_secret(
secret_name="NETBOX_API_TOKEN",
project_id="7b832220-24c0-45bc-a5f1-ce9794a31259",
environment="prod",
path="/matrix"
).secret_value
# Connect to NetBox
nb = pynetbox.api('https://netbox.spaceships.work', token=token)
# Query sites
sites = nb.dcim.sites.all()
for site in sites:
print(f"{site.name}: {site.description}")
See ../tools/netbox_api_client.py for complete working example.
Authentication
Token Authentication
NetBox uses token-based authentication for API access.
Creating a Token
- Log into NetBox web UI
- Navigate to User → API Tokens
- Click Add Token
- Configure permissions (read, write)
- Copy token (only shown once)
Storing Tokens Securely
❌ NEVER hardcode tokens:
# DON'T DO THIS
token = "a1b2c3d4e5f6..." # NEVER hardcode!
✅ Use Infisical (Virgo-Core standard):
from infisical import InfisicalClient
def get_netbox_token() -> str:
"""Get NetBox API token from Infisical."""
client = InfisicalClient()
secret = client.get_secret(
secret_name="NETBOX_API_TOKEN",
project_id="7b832220-24c0-45bc-a5f1-ce9794a31259",
environment="prod",
path="/matrix"
)
return secret.secret_value
See netbox-best-practices.md for complete security patterns.
Using Tokens
With curl:
curl -H "Authorization: Token YOUR_TOKEN" \
https://netbox.spaceships.work/api/dcim/sites/
With pynetbox:
import pynetbox
nb = pynetbox.api('https://netbox.spaceships.work', token='YOUR_TOKEN')
Session Authentication
Session authentication is available when using the NetBox web interface (browser-based). Not recommended for API automation.
API Endpoints Structure
All API endpoints are prefixed with /api/ followed by the app name:
| Prefix | Purpose | Example Endpoints |
|---|---|---|
/api/dcim/ |
Data Center Infrastructure Management | sites, racks, devices, cables |
/api/ipam/ |
IP Address Management | ip-addresses, prefixes, vlans, vrfs |
/api/virtualization/ |
Virtual Machines | virtual-machines, clusters |
/api/circuits/ |
Circuit Management | circuits, providers |
/api/tenancy/ |
Multi-tenancy | tenants, tenant-groups |
/api/extras/ |
Additional Features | tags, custom-fields, webhooks |
Common Endpoint Patterns
All endpoints follow REST conventions:
GET /api/{app}/{model}/ # List all objects
POST /api/{app}/{model}/ # Create new object(s)
GET /api/{app}/{model}/{id}/ # Get specific object
PUT /api/{app}/{model}/{id}/ # Full update
PATCH /api/{app}/{model}/{id}/ # Partial update
DELETE /api/{app}/{model}/{id}/ # Delete object
Common Operations
Sites
List all sites:
GET /api/dcim/sites/
sites = nb.dcim.sites.all()
Create a site:
POST /api/dcim/sites/
{
"name": "Matrix Cluster",
"slug": "matrix",
"status": "active",
"description": "3-node Proxmox cluster",
"region": null,
"tags": ["proxmox", "production"]
}
site = nb.dcim.sites.create(
name="Matrix Cluster",
slug="matrix",
status="active",
description="3-node Proxmox cluster",
tags=[{"name": "proxmox"}, {"name": "production"}]
)
Get specific site:
GET /api/dcim/sites/{id}/
GET /api/dcim/sites/?slug=matrix
site = nb.dcim.sites.get(slug='matrix')
Update a site:
PATCH /api/dcim/sites/{id}/
{
"description": "Updated description"
}
site.description = "Updated description"
site.save()
Delete a site:
DELETE /api/dcim/sites/{id}/
site.delete()
Devices
List devices:
GET /api/dcim/devices/
GET /api/dcim/devices/?site=matrix
devices = nb.dcim.devices.filter(site='matrix')
Create a device:
POST /api/dcim/devices/
{
"name": "foxtrot",
"device_type": 1,
"site": 1,
"device_role": 3,
"status": "active",
"tags": ["proxmox-node"]
}
device = nb.dcim.devices.create(
name="foxtrot",
device_type=1,
site=nb.dcim.sites.get(slug='matrix').id,
device_role=3,
status="active",
tags=[{"name": "proxmox-node"}]
)
Get device with related objects:
GET /api/dcim/devices/{id}/?include=interfaces,config_context
device = nb.dcim.devices.get(name='foxtrot')
interfaces = device.interfaces # Related interfaces
IP Addresses
List IP addresses:
GET /api/ipam/ip-addresses/
GET /api/ipam/ip-addresses/?vrf=management
ips = nb.ipam.ip_addresses.all()
ips_mgmt = nb.ipam.ip_addresses.filter(vrf='management')
Create IP address with DNS:
POST /api/ipam/ip-addresses/
{
"address": "192.168.3.10/24",
"dns_name": "docker-01-nexus.spaceships.work",
"status": "active",
"description": "Docker host for Nexus registry",
"tags": ["production-dns", "terraform"]
}
ip = nb.ipam.ip_addresses.create(
address="192.168.3.10/24",
dns_name="docker-01-nexus.spaceships.work",
status="active",
description="Docker host for Nexus registry",
tags=[{"name": "production-dns"}, {"name": "terraform"}]
)
Assign IP to interface:
ip = nb.ipam.ip_addresses.get(address='192.168.3.10/24')
interface = nb.dcim.interfaces.get(device='foxtrot', name='eth0')
ip.assigned_object_type = 'dcim.interface'
ip.assigned_object_id = interface.id
ip.save()
Get IP with assigned device:
GET /api/ipam/ip-addresses/{id}/
ip = nb.ipam.ip_addresses.get(address='192.168.3.10/24')
if ip.assigned_object:
print(f"Assigned to: {ip.assigned_object.device.name}")
Prefixes
List prefixes:
GET /api/ipam/prefixes/
GET /api/ipam/prefixes/?site=matrix
prefixes = nb.ipam.prefixes.filter(site='matrix')
Get available IPs in prefix:
GET /api/ipam/prefixes/{id}/available-ips/
prefix = nb.ipam.prefixes.get(prefix='192.168.3.0/24')
available = prefix.available_ips.list()
print(f"Available IPs: {len(available)}")
Create IP from available pool:
POST /api/ipam/prefixes/{id}/available-ips/
{
"dns_name": "k8s-01-worker.spaceships.work",
"tags": ["production-dns"]
}
prefix = nb.ipam.prefixes.get(prefix='192.168.3.0/24')
ip = prefix.available_ips.create(
dns_name="k8s-01-worker.spaceships.work",
tags=[{"name": "production-dns"}]
)
Virtual Machines
List VMs:
GET /api/virtualization/virtual-machines/
GET /api/virtualization/virtual-machines/?cluster=matrix
vms = nb.virtualization.virtual_machines.filter(cluster='matrix')
Create VM:
POST /api/virtualization/virtual-machines/
{
"name": "docker-01",
"cluster": 1,
"role": 2,
"vcpus": 4,
"memory": 8192,
"disk": 100,
"status": "active",
"tags": ["docker", "production"]
}
vm = nb.virtualization.virtual_machines.create(
name="docker-01",
cluster=nb.virtualization.clusters.get(name='matrix').id,
vcpus=4,
memory=8192,
disk=100,
status="active",
tags=[{"name": "docker"}, {"name": "production"}]
)
Create VM interface:
vm_interface = nb.virtualization.interfaces.create(
virtual_machine=vm.id,
name='eth0',
type='virtual',
enabled=True
)
Assign IP to VM interface:
ip = nb.ipam.ip_addresses.create(
address='192.168.3.10/24',
dns_name='docker-01-nexus.spaceships.work',
assigned_object_type='virtualization.vminterface',
assigned_object_id=vm_interface.id,
tags=[{"name": "production-dns"}]
)
# Set as primary IP
vm.primary_ip4 = ip.id
vm.save()
Filtering and Search
Query Parameters
Most endpoints support filtering via query parameters:
# Filter by single value
GET /api/dcim/devices/?site=matrix
# Filter by multiple values (OR logic)
GET /api/dcim/devices/?site=matrix&site=backup
# Exclude values
GET /api/dcim/devices/?site__n=decommissioned
# Partial matching (case-insensitive)
GET /api/dcim/devices/?name__ic=docker
# Starts with
GET /api/dcim/devices/?name__isw=k8s
# Ends with
GET /api/dcim/devices/?name__iew=worker
# Greater than / less than
GET /api/ipam/prefixes/?prefix_length__gte=24
# Empty / not empty
GET /api/dcim/devices/?tenant__isnull=true
Python Filtering
# Single filter
devices = nb.dcim.devices.filter(site='matrix')
# Multiple filters (AND logic)
devices = nb.dcim.devices.filter(site='matrix', status='active')
# Partial matching
devices = nb.dcim.devices.filter(name__ic='docker')
# Greater than
prefixes = nb.ipam.prefixes.filter(prefix_length__gte=24)
# Get single object
device = nb.dcim.devices.get(name='foxtrot')
Full-Text Search
# Search across all fields
GET /api/dcim/devices/?q=foxtrot
results = nb.dcim.devices.filter(q='foxtrot')
Tag Filtering
# Devices with specific tag
GET /api/dcim/devices/?tag=proxmox-node
# Multiple tags (OR logic)
GET /api/dcim/devices/?tag=proxmox-node&tag=ceph-node
devices = nb.dcim.devices.filter(tag='proxmox-node')
Pagination
API responses are paginated by default (50 results per page).
Response Format
{
"count": 1000,
"next": "https://netbox.spaceships.work/api/dcim/devices/?limit=50&offset=50",
"previous": null,
"results": [...]
}
Controlling Pagination
# Custom page size (max 1000)
GET /api/dcim/devices/?limit=100
# Skip to offset
GET /api/dcim/devices/?limit=50&offset=100
Python Pagination
# Get all (handles pagination automatically)
all_devices = nb.dcim.devices.all()
# Custom page size
devices = nb.dcim.devices.filter(limit=100)
# Manual pagination
page1 = nb.dcim.devices.filter(limit=50, offset=0)
page2 = nb.dcim.devices.filter(limit=50, offset=50)
⚠️ Warning: Using all() on large datasets can be slow. Use filtering to reduce result set.
Bulk Operations
NetBox supports bulk creation, update, and deletion.
Bulk Create
curl:
POST /api/dcim/devices/
[
{"name": "device1", "device_type": 1, "site": 1},
{"name": "device2", "device_type": 1, "site": 1},
{"name": "device3", "device_type": 1, "site": 1}
]
pynetbox:
devices_data = [
{"name": "device1", "device_type": 1, "site": 1},
{"name": "device2", "device_type": 1, "site": 1},
{"name": "device3", "device_type": 1, "site": 1}
]
# Bulk create (more efficient than loop)
devices = nb.dcim.devices.create(devices_data)
Bulk Update
curl:
PUT /api/dcim/devices/
[
{"id": 1, "status": "active"},
{"id": 2, "status": "active"},
{"id": 3, "status": "offline"}
]
pynetbox:
# Update multiple objects
for device in nb.dcim.devices.filter(site='matrix'):
device.status = 'active'
device.save()
# Or bulk update with PUT
updates = [
{"id": 1, "status": "active"},
{"id": 2, "status": "active"}
]
nb.dcim.devices.update(updates)
Bulk Delete
curl:
DELETE /api/dcim/devices/
[
{"id": 1},
{"id": 2},
{"id": 3}
]
pynetbox:
# Delete multiple objects
devices_to_delete = nb.dcim.devices.filter(status='decommissioned')
for device in devices_to_delete:
device.delete()
Error Handling
HTTP Status Codes
| Code | Meaning | Description |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Invalid data or malformed request |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Insufficient permissions |
| 404 | Not Found | Resource not found |
| 500 | Internal Server Error | Server error |
Error Response Format
{
"detail": "Not found.",
"error": "Object does not exist"
}
Python Error Handling
import pynetbox
from requests.exceptions import HTTPError
try:
device = nb.dcim.devices.get(name='nonexistent')
if not device:
print("Device not found")
except HTTPError as e:
if e.response.status_code == 404:
print("Resource not found")
elif e.response.status_code == 403:
print("Permission denied")
else:
print(f"HTTP Error: {e}")
except Exception as e:
print(f"Unexpected error: {e}")
Validation Errors
try:
site = nb.dcim.sites.create(
name="Invalid Site",
slug="invalid slug" # Spaces not allowed
)
except pynetbox.RequestError as e:
print(f"Validation error: {e.error}")
Best Practices
-
Always check for None:
device = nb.dcim.devices.get(name='foo') if device: print(device.name) else: print("Device not found") -
Use try/except for create/update:
try: ip = nb.ipam.ip_addresses.create(address='192.168.1.1/24') except pynetbox.RequestError as e: print(f"Failed to create IP: {e.error}") -
Validate data before API calls:
import ipaddress def validate_ip(ip_str: str) -> bool: try: ipaddress.ip_interface(ip_str) return True except ValueError: return False
See ../tools/netbox_api_client.py for complete error handling examples.
Rate Limiting
NetBox may enforce rate limits on API requests.
Response Headers
X-RateLimit-Limit: 1000 # Total requests allowed
X-RateLimit-Remaining: 950 # Remaining requests
X-RateLimit-Reset: 1640000000 # Reset time (Unix timestamp)
Handling Rate Limits
import time
from requests.exceptions import HTTPError
def api_call_with_retry(func, max_retries=3):
"""Retry API call if rate limited."""
for attempt in range(max_retries):
try:
return func()
except HTTPError as e:
if e.response.status_code == 429: # Rate limited
retry_after = int(e.response.headers.get('Retry-After', 60))
print(f"Rate limited. Retrying in {retry_after}s...")
time.sleep(retry_after)
else:
raise
raise Exception("Max retries exceeded")
# Usage
result = api_call_with_retry(lambda: nb.dcim.devices.all())
Best Practices
- Use pagination to reduce request count
- Cache responses when data doesn't change frequently
- Batch operations using bulk endpoints
- Implement exponential backoff for retries
- Monitor rate limit headers in production
Python Client (pynetbox)
Installation
pip install pynetbox
Or with uv (Virgo-Core standard):
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = ["pynetbox>=7.0.0"]
# ///
Basic Usage
import pynetbox
# Connect
nb = pynetbox.api(
'https://netbox.spaceships.work',
token='your-token'
)
# Query
sites = nb.dcim.sites.all()
device = nb.dcim.devices.get(name='foxtrot')
# Create
site = nb.dcim.sites.create(
name='Matrix',
slug='matrix'
)
# Update
device.status = 'active'
device.save()
# Delete
device.delete()
Advanced Patterns
Lazy loading:
# Only fetches when accessed
device = nb.dcim.devices.get(name='foxtrot')
interfaces = device.interfaces # API call happens here
Custom fields:
device.custom_fields['serial_number'] = 'ABC123'
device.save()
Relationships:
# Get device's primary IP
device = nb.dcim.devices.get(name='foxtrot')
if device.primary_ip4:
print(device.primary_ip4.address)
# Get IP's assigned device
ip = nb.ipam.ip_addresses.get(address='192.168.3.5/24')
if ip.assigned_object:
print(ip.assigned_object.device.name)
Threading (for bulk operations):
from concurrent.futures import ThreadPoolExecutor
def get_device_info(device_name):
return nb.dcim.devices.get(name=device_name)
device_names = ['foxtrot', 'golf', 'hotel']
with ThreadPoolExecutor(max_workers=5) as executor:
devices = list(executor.map(get_device_info, device_names))
Best Practices
1. Use Filtering to Reduce Data Transfer
# ❌ Inefficient: Get all devices then filter
all_devices = nb.dcim.devices.all()
matrix_devices = [d for d in all_devices if d.site.slug == 'matrix']
# ✅ Efficient: Filter on server
matrix_devices = nb.dcim.devices.filter(site='matrix')
2. Use Specific Fields
# Only get specific fields
GET /api/dcim/devices/?fields=name,status,primary_ip4
3. Cache Responses
from functools import lru_cache
@lru_cache(maxsize=128)
def get_site(site_slug: str):
"""Cache site lookups."""
return nb.dcim.sites.get(slug=site_slug)
4. Validate Before API Calls
import ipaddress
import re
def validate_dns_name(name: str) -> bool:
"""Validate DNS naming convention."""
pattern = r'^[a-z0-9-]+-\d{2}-[a-z0-9-]+\.[a-z0-9.-]+$'
return bool(re.match(pattern, name))
def validate_ip(ip_str: str) -> bool:
"""Validate IP address format."""
try:
ipaddress.ip_interface(ip_str)
return True
except ValueError:
return False
# Use before API calls
if validate_dns_name(dns_name) and validate_ip(ip_address):
ip = nb.ipam.ip_addresses.create(
address=ip_address,
dns_name=dns_name
)
5. Use Bulk Operations
# ❌ Slow: Create in loop
for ip in ip_list:
nb.ipam.ip_addresses.create(address=ip)
# ✅ Fast: Bulk create
nb.ipam.ip_addresses.create([
{"address": ip} for ip in ip_list
])
6. Implement Proper Error Handling
See Error Handling section above.
7. Use HTTPS in Production
# ✅ Always use HTTPS
nb = pynetbox.api('https://netbox.spaceships.work', token=token)
# ❌ Never use HTTP in production
nb = pynetbox.api('http://netbox.spaceships.work', token=token)
8. Rotate Tokens Regularly
Store tokens in Infisical and rotate every 90 days. See Security section.
Security
API Token Security
-
Store in Infisical (never hardcode):
from infisical import InfisicalClient client = InfisicalClient() token = client.get_secret( secret_name="NETBOX_API_TOKEN", project_id="7b832220-24c0-45bc-a5f1-ce9794a31259", environment="prod", path="/matrix" ).secret_value -
Use environment variables (alternative):
import os token = os.getenv('NETBOX_API_TOKEN') if not token: raise ValueError("NETBOX_API_TOKEN not set") -
Rotate tokens regularly (every 90 days)
-
Use minimal permissions (read-only for queries, write for automation)
HTTPS Only
# ✅ Verify SSL certificates
nb = pynetbox.api(
'https://netbox.spaceships.work',
token=token,
ssl_verify=True # Default, but explicit is better
)
# ⚠️ Only disable for dev/testing
nb = pynetbox.api(
'https://netbox.local',
token=token,
ssl_verify=False # Self-signed cert
)
Audit API Usage
Monitor API calls in production:
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def audit_api_call(action: str, resource: str, details: dict):
"""Log API calls for audit."""
logger.info(f"API Call: {action} {resource} - {details}")
# Example usage
ip = nb.ipam.ip_addresses.create(address='192.168.1.1/24')
audit_api_call('CREATE', 'ip-address', {'address': '192.168.1.1/24'})
Network Security
- Use VPN for remote access to NetBox
- Restrict NetBox API access by IP (firewall rules)
- Use Proxmox VLANs to isolate management traffic
Additional Resources
- Official API Docs: https://netboxlabs.com/docs/netbox/en/stable/
- pynetbox Docs: https://pynetbox.readthedocs.io/
- OpenAPI Schema:
GET https://netbox.spaceships.work/api/schema/ - GraphQL API:
https://netbox.spaceships.work/graphql/
Related Documentation
- NetBox Data Models - Data model relationships
- NetBox Best Practices - Infrastructure patterns
- Tools: netbox_api_client.py - Complete working example
- DNS Naming Conventions - DNS naming rules
Next: NetBox Data Models Guide