API REFERENCE Beta
Every feature in the NOTBOX web UI is powered by a local HTTP API. Call these endpoints directly to automate testing, build scripts, or integrate with CI/CD pipelines.
Quick Start
All endpoints are available at http://<device-ip>/banana/<endpoint>
Replace <device-ip> with your NOTBOX management IP (shown in the web UI).
# Check device Serial curl "http://192.168.1.100/banana/get_serial" # Add 100ms latency + 2% packet loss to downloads curl -X POST "http://192.168.1.100/banana/set_impairment?interface=download&delay=100&jitter=20&loss=2" # Check current impairment status curl "http://192.168.1.100/banana/get_status" # Clear all impairments curl -X POST "http://192.168.1.100/banana/clear_impairment?interface=both" # Start a 60-second packet capture curl -X POST "http://192.168.1.100/banana/start_capture?duration=60" # Run a speed test curl -X POST "http://192.168.1.100/banana/run_speedtest"
All responses are JSON. No authentication is required by default. When password protection is enabled, pass an API token:
# With Bearer token (generate one in Settings > API Access Token) curl -H "Authorization: Bearer <example-token>" "http://192.168.1.100/banana/get_status"
API Conventions
Every endpoint shares the same rules for requests, responses, and errors. Read this once and you're set for the rest of the docs.
HTTP Methods
Endpoints that change something on the device accept POST only. Endpoints that just return data accept either GET or POST. Each endpoint below is tagged with its method.
Authentication
The API is open by default. No login or token needed. When you turn on password protection in Settings, authenticate with either:
- A session cookie set by
/banana/login, for browser-based use - An API token in the
Authorization: Bearerheader, recommended for scripts
See the Authentication section below for the full flow.
Browser Restriction
Your browser can only call the API when the web page is served from NOTBOX itself. If you build a custom dashboard and run it at http://localhost:3000, the browser will get 403 Forbidden when it tries to POST to NOTBOX. This protects your device from a sketchy website secretly firing commands at it.
Tools like curl, Python scripts, and Postman aren't affected. They don't have this restriction.
Rate Limit
Up to 30 requests per second per IP address. Going over returns 429 Too Many Requests. Normal use never hits this. It's there to catch runaway scripts.
Request Size
Requests larger than 64 KiB are rejected with 413 Payload Too Large. All normal API requests are far smaller.
Response Format
Endpoints return JSON. A successful call looks like one of these:
# Action endpoint { "status": "success", "message": "Impairment applied to download" } # Data endpoint (shape varies) { "serial": "NB20260412065" }
Errors look like:
{
"status": "error",
"message": "Invalid delay (must be 0-10000 ms)"
}
Check the status field first. A few low-level errors (rate limit, wrong method, etc.) come back as plain text instead of JSON. In those cases the HTTP status code tells you what happened.
HTTP Status Codes
| Code | Meaning | What to do |
|---|---|---|
200 | OK | Check the JSON status field for the actual outcome |
400 | Bad Request | A parameter is missing or wrong; check the message |
401 | Unauthorized | Add your API token or log in |
403 | Forbidden | Browser is calling from a page not served by NOTBOX |
404 | Not Found | The endpoint, file, or resource doesn't exist |
405 | Method Not Allowed | You used GET on a POST-only endpoint |
413 | Payload Too Large | Your request body is over 64 KiB |
429 | Too Many Requests | Slow down. You're over the rate limit |
500 | Internal Server Error | Unusual. Try again; contact support if it persists |
Endpoints
Impairment Control
Parameters
| Name | Required | Description |
|---|---|---|
| interface | required | upload, download, or both |
| delay | optional | Latency in milliseconds (default: 0) |
| jitter | optional | Jitter in milliseconds (default: 0) |
| loss | optional | Packet loss percentage (default: 0) |
| bandwidth | optional | Bandwidth limit value (default: 0 = unlimited) |
| bandwidth_unit | optional | mbit (default), kbit, or gbit |
Example
curl -X POST "http://192.168.1.100/banana/set_impairment?interface=download&delay=100&jitter=20&loss=2&bandwidth=10&bandwidth_unit=mbit"
{
"status": "success",
"message": "Impairment applied to download"
}
download applies to wired + both WiFi bands automatically. both applies to all directions in a single call.Parameters
| Name | Required | Description |
|---|---|---|
| interface | required | upload, download, or both |
Example
curl -X POST "http://192.168.1.100/banana/clear_impairment?interface=both"
{
"status": "success",
"message": "Impairment cleared on both"
}
Parameters
None
Response
{
"active": true,
"download": {
"latency": "100",
"jitter": "20",
"loss": "2",
"bandwidth": "10"
},
"upload": {
"latency": "",
"jitter": "",
"loss": "",
"bandwidth": ""
}
}
active is true if any interface has impairments. The download and upload objects contain the parsed values. Empty strings mean no impairment set.Packet Capture
Parameters
| Name | Required | Description |
|---|---|---|
| duration | optional | Capture duration in seconds (default: 60) |
| ip_filter | optional | IP filter as direction:value. Direction: src, dst, either. Value: single IP, CIDR, or range (e.g. dst:192.168.1.0/24) |
| port_filter | optional | Port filter as direction:port (e.g. either:443) |
| mac_filter | optional | MAC filter as direction:MAC (e.g. src:aa:bb:cc:dd:ee:ff) |
| proto_filter | optional | Protocol filter as direction:protocol. Built-in: dns, dhcp, http, https, ssh, ntp, mdns, ssdp, mqtt, snmp |
| use_network | optional | Set to true to save capture to mounted SMB share |
Example
# Capture HTTPS traffic for 120 seconds curl -X POST "http://192.168.1.100/banana/start_capture?duration=120&proto_filter=either:https"
{
"status": "success",
"filename": "notbox_20260216_143022.pcap",
"pid": 12345,
"duration": 120,
"network_storage": ""
}
Parameters
None
Response
{
"status": "success",
"filename": "notbox_20260216_143022.pcap",
"size": 5242880,
"auto_stopped": false,
"stop_reason": ""
}
Parameters
None
Response
{
"status": "capturing",
"size": 1048576,
"size_mb": 1,
"elapsed": 15,
"duration": 60
}
"status": "stopped" when no capture is running.Parameters
| Name | Required | Description |
|---|---|---|
| filename | required | PCAP filename (e.g. notbox_20260216_143022.pcap) |
Example
# Download to local machine curl -o capture.pcap "http://192.168.1.100/banana/download_capture?filename=notbox_20260216_143022.pcap"
Content-Type: application/vnd.tcpdump.pcap. Open with Wireshark.Parameters
| Name | Required | Description |
|---|---|---|
| filename | optional | Specific PCAP filename to delete. If omitted, deletes all local captures. |
Response
{
"status": "success",
"message": "Capture deleted",
"location": "local"
}
Parameters
| Name | Required | Description |
|---|---|---|
| filename | required | PCAP filename returned from start_capture |
Example
curl -X POST "http://192.168.1.100/banana/get_live_stats?filename=notbox_20260526_001518.pcap"
Response
{
"status": "success",
"summary_version": 2,
"live": true,
"dns": [{ "name": "example.com", "count": 5, "ips": "93.184.216.34", "status": "NOERROR" }],
"top_dest": [{ "ip": "93.184.216.34", "bytes": 2097152, "proto": "tcp", "port": "443", "name": "example.com" }],
"ip_correlations": [{ "ip": "93.184.216.34", "dns_names": "example.com", "sni_names": "example.com" }],
"top_conversations": [{ "endpoint_a": "192.168.1.10:55312", "endpoint_b": "93.184.216.34:443", "bytes": 1048576, "bytes_a_to_b": 524288, "bytes_b_to_a": 524288 }],
"protocols": [{ "name": "tls", "packets": 420, "bytes": 4123456, "pct_bytes": 78.7 }],
"tcp_health": { "out_of_order": 2, "dup_acks": 5, "resets": 1, "syn_retries": 0, "failed_handshakes": 1 },
"failures": [{ "ip": "192.168.1.1", "count": 3, "type": "rst" }],
"sni": [{ "name": "example.com", "count": 8, "ip": "93.184.216.34", "confidence": "high" }],
"timeline": [{ "ts": 1715000000, "bytes": 102400, "packets": 42 }],
"timeline_bucket_sec": 1,
"total_bytes": 5242880,
"upload_bytes": 2097152,
"download_bytes": 3145728,
"packet_count": 542,
"duration": 14,
"first_ts": 1715000000,
"last_ts": 1715000014
}
live field is true during capture and false once it stops; the sort order switches from newest-first (during) to highest-traffic-first (after). If the filename has no data (for example after a device reboot, or you've called delete_capture), the response is {"status":"not_found","message":"No live capture state for that filename"}.Parameters
| Name | Required | Description |
|---|---|---|
| action | required | start, stop, or status |
Example
# Enable port mirror (LOCAL port becomes mirror-only) curl -X POST "http://192.168.1.100/banana/port_mirror?action=start"
{
"success": true,
"status": "active",
"message": "Port mirror enabled - LOCAL port is now mirror-only"
}
Monitoring
Parameters
| Name | Required | Description |
|---|---|---|
| target | optional | Host to ping (default: 8.8.8.8) |
| interval | optional | Ping interval in seconds, 1-300 (default: 5) |
| tab_id | optional | Tab identifier for multiple concurrent monitors (default: default) |
Response
{
"status": "success",
"pid": 1234,
"target": "8.8.8.8",
"interval": 5,
"resolved_ip": "8.8.8.8",
"tab_id": "default"
}
Parameters
| Name | Required | Description |
|---|---|---|
| tab_id | optional | Tab identifier to stop (default: default) |
Response
{ "status": "success", "tab_id": "default" }
Parameters
| Name | Required | Description |
|---|---|---|
| tab_id | optional | Tab identifier (default: default) |
| timeframe | optional | Time window in seconds (default: 300). Ignored if start/end provided. |
| start | optional | Custom start time (Unix timestamp) |
| end | optional | Custom end time (Unix timestamp) |
Response
{
"status": "running",
"target": "8.8.8.8",
"tab_id": "default",
"data": [
{ "timestamp": 1705000005, "latency": 25.5, "loss": 0.0 },
{ "timestamp": 1705000020, "latency": 26.3, "loss": 0.0 }
],
"point_count": 2,
"bucket_size": 5
}
Parameters
None
Response
{
"status": "success",
"monitors": [
{ "tab_id": "default", "target": "8.8.8.8", "interval": 5, "pid": 1234, "data_points": 150 }
],
"count": 1
}
Parameters
| Name | Required | Description |
|---|---|---|
| action | required | start, stop, or status |
| interface | optional | auto (default), upload, download, wifi |
Example
curl -X POST "http://192.168.1.100/banana/bandwidth_monitor?action=start&interface=auto"
{ "status": "success", "pid": 2345, "interface": "auto" }
auto monitors the downlink + active WiFi together, with direction correction so RX = download and TX = upload from the test device's perspective.Parameters
| Name | Required | Description |
|---|---|---|
| timeframe | optional | Time window in seconds (default: 300) |
| start | optional | Custom start time (Unix timestamp) |
| end | optional | Custom end time (Unix timestamp) |
Response
{
"status": "success",
"interface": "auto",
"data": [
{ "timestamp": 1705000005, "rx_rate": 1024000, "tx_rate": 512000 }
],
"point_count": 1,
"bucket_size": 5,
"totals": { "rx": 10485760, "tx": 5242880, "duration": 120 }
}
Network Tools
Parameters
| Name | Required | Description |
|---|---|---|
| tool | required | Tool to run: ping, dns, rdns, traceroute, arp, arpscan, iplookup, publicip, update_oui |
| target | required* | Target host/IP. Required for: ping, dns, rdns, traceroute, iplookup. Not needed for: arp, arpscan, publicip, update_oui. |
| count | optional | Ping count, 1-20 (default: 5) |
| server | optional | DNS server for dns lookups |
Examples
# Ping curl -X POST "http://192.168.1.100/banana/network_tools?tool=ping&target=8.8.8.8&count=3" # DNS lookup curl -X POST "http://192.168.1.100/banana/network_tools?tool=dns&target=google.com" # Traceroute curl -X POST "http://192.168.1.100/banana/network_tools?tool=traceroute&target=cloudflare.com" # ARP table with vendor lookup curl -X POST "http://192.168.1.100/banana/network_tools?tool=arp" # WHOIS / IP geolocation curl -X POST "http://192.168.1.100/banana/network_tools?tool=iplookup&target=8.8.8.8" # Get public IP curl -X POST "http://192.168.1.100/banana/network_tools?tool=publicip"
Ping Response
{
"status": "success",
"output": "PING 8.8.8.8 ...",
"stats": {
"transmitted": 3, "received": 3, "loss": 0,
"rtt_min": "21.5", "rtt_avg": "24.2", "rtt_max": "28.1"
}
}
Parameters
None
Response
{
"status": "success",
"download_mbps": 146,
"upload_mbps": 89,
"ping_ms": 12,
"jitter_ms": 3,
"server": "Chicago (ORD)",
"sponsor": "",
"isp": "",
"provider": "cloudflare"
}
server is the nearest Cloudflare edge (City (POP-code)); sponsor and isp are not reported by Cloudflare and return empty.Parameters
| Name | Required | Description |
|---|---|---|
| target | required | Hostname or IP |
| port | required | Port number (1-65535) |
Example
curl -X POST "http://192.168.1.100/banana/test_port?target=google.com&port=443"
{
"status": "success",
"result": "open",
"response_time_ms": 45,
"target": "google.com",
"port": 443,
"ip": "142.251.33.14"
}
WiFi Management
Parameters
| Name | Required | Default | Description |
|---|---|---|---|
| ssid | optional | NetTest-Impaired | Network name (1-32 chars) |
| password | optional | WiFi password (8-63 chars, required unless encryption=none) | |
| band | optional | 2g | 2g or 5g |
| encryption | optional | psk2 | none, psk, psk2, psk-mixed, sae (WPA3), sae-mixed |
| channel | optional | auto | Channel number or auto |
Example
curl -X POST -d "ssid=Example-Test-AP&password=example-password&band=5g&encryption=sae" "http://192.168.1.100/banana/configure_wifi"
{
"status": "success",
"ssid": "Example-Test-AP",
"band": "5GHz",
"interface": "wifi-5g"
}
Parameters
None
Response
{ "status": "success" }
Parameters
None
Response
{
"status": "active",
"ssid": "Example-Test-AP",
"band": "5GHz",
"interface": "wifi-5g"
}
"status": "inactive" when WiFi is off.Parameters
None
Response
{
"clients": [
{ "mac": "aa:bb:cc:dd:ee:ff", "signal": "-42 dBm", "connected": "15m" }
]
}
Network Storage (SMB / NFS)
Parameters
| Name | Required | Description |
|---|---|---|
| host | required | SMB server hostname or IP |
| share | required | Share name |
| user | optional | Username for auth |
| password | optional | Password for auth |
| domain | optional | Domain for auth |
| path | optional | Subfolder within share |
Response
{
"status": "success",
"message": "Connected and verified",
"mount_point": "/mnt/capture_share",
"share": "//example-fileserver/captures",
"available_space": "500G"
}
Parameters
None
Response
{ "status": "success", "message": "Disconnected" }
Parameters
| Name | Required | Description |
|---|---|---|
| host | required | NFS server hostname or IP |
| export | required | NFS export path (for example /srv/notbox-pcaps) |
| path | optional | Subfolder within the mounted export |
| version | optional | auto (default), 4.2, 4.1, 4.0, or 3 |
| options | optional | Additional mount options (advanced) |
Response
{
"status": "success",
"message": "Connected and verified",
"mount_point": "/mnt/capture_share",
"capture_dir": "/mnt/capture_share/site-a",
"share": "192.168.1.50:/srv/notbox-pcaps",
"available_space": "500G",
"protocol": "nfs"
}
auto mode, NOTBOX tries NFS versions 4.2, 4.1, 4.0, then 3.Parameters
None
Response
{ "status": "success", "message": "Disconnected" }
Parameters
None
Response
{
"status": "connected",
"share": "//example-fileserver/captures",
"available_space": "500G",
"used_space": "250G",
"writable": true,
"capture_count": 5
}
"status": "disconnected" when no share is mounted.Parameters
None
Response
{
"status": "connected",
"share": "192.168.1.50:/srv/notbox-pcaps",
"capture_dir": "/mnt/capture_share/site-a",
"available_space": "500G",
"used_space": "250G",
"writable": true,
"capture_count": 5,
"protocol": "nfs"
}
{"status":"disconnected"} when no NFS share is mounted.System & Configuration
Parameters
None
Response
{
"cpu": "42",
"ram_used": "69",
"ram_total": "234",
"disk_free": "82.8",
"tmp_used": "0.2",
"tmp_total": "117.0",
"temp": "60",
"uptime": "5d 3h 45m",
"rx_rate": "5.2 Mbps",
"tx_rate": "1.3 Mbps",
"device_unix": 1771819200,
"device_time_utc": "2026-02-23T20:00:00Z",
"timezone": "EST5EDT,M3.2.0,M11.1.0",
"zonename": "America/New_York"
}
Parameters
None
Response
{
"serial": "NB20260412065"
}
{"serial": null}.Parameters
None
Response
{
"status": "success",
"hostname": "NOTBOX",
"ip_mode": "dhcp",
"ip_addr": "",
"netmask": "",
"gateway": "",
"dns": "",
"current_ip": "192.168.1.100",
"current_gateway": "192.168.1.1"
}
Parameters
| Name | Required | Description |
|---|---|---|
| hostname | optional | Device hostname (1-63 chars) |
| ip_mode | optional | dhcp or static |
| ip_addr | if static | Static IP address |
| netmask | optional | Subnet mask (default: 255.255.255.0) |
| gateway | optional | Gateway IP |
| dns | optional | DNS server IP |
Response
{
"status": "success",
"message": "Settings saved. Network will restart.",
"new_ip": "192.168.1.100",
"needs_reload": true
}
Parameters
None
Response
{
"status": "success",
"mode": "full",
"internet": true,
"dns_ok": true,
"connection": "network",
"connection_desc": "Connected via network (MGMT)",
"mgmt_link": 1,
"mgmt_ip": "192.168.1.100",
"local_link": 1,
"local_ip": "192.168.100.1",
"disabled_features": []
}
mode is "full" with internet access or "limited" without. Limited mode disables speedtest, uplink monitor, external port tests, and network captures.Parameters
None
Response
{
"mgmt_exists": true,
"mgmt_ip": "192.168.1.100/24",
"ping_success": true,
"default_gateway": "192.168.1.1"
}
Authentication
Parameters
None (reads notbox_session cookie automatically)
Response
{
"auth_required": true,
"is_authorized": false
}
auth_required is false, all endpoints are open. When true, authenticate with either a session cookie (from /banana/login) or a Bearer token (from /banana/generate_api_token). Bearer tokens are recommended for scripts: curl -H "Authorization: Bearer <example-token>"Parameters (POST body)
| Name | Required | Description |
|---|---|---|
| password | required | Device password |
Example
curl -c cookies.txt -d "password=example-password" "http://192.168.1.100/banana/login"
{
"status": "success",
"message": "Authenticated successfully"
}
-c cookies.txt to save the session cookie, then -b cookies.txt on subsequent requests.Parameters
None
Response
{ "status": "success", "message": "Logged out successfully" }
Parameters
| Name | Required | Description |
|---|---|---|
| action | required | Query param: enable, disable, or change |
| new_password | for enable/change | New password (min 4 chars, POST body) |
| current_password | for disable/change | Current password (POST body) |
Example
# Enable password protection curl -d "new_password=example-password" "http://192.168.1.100/banana/set_password?action=enable"
{ "status": "success", "message": "Password protection enabled" }
Parameters
None
Response
{ "enabled": false }
Parameters
None (requires password protection to be enabled and an active session)
Example
curl -X POST -b cookies.txt "http://192.168.1.100/banana/generate_api_token"
{
"status": "success",
"token": "0123456789abcdef0123456789abcdef"
}
curl -H "Authorization: Bearer <example-token>". Only one token exists at a time. Generating a new one replaces the previous. Token is revoked when password is changed or disabled.Parameters
None (requires active session)
Response
{
"exists": true,
"token": "0123456789abcdef0123456789abcdef"
}
Parameters
None (requires active session)
Response
{ "status": "success", "message": "API token revoked" }
Firmware Updates
Parameters
| Name | Required | Description |
|---|---|---|
| channel | optional | stable (default) or pinned/specific |
| requested_version | pinned only | Exact version to check (for example 1.0.2) |
| action | optional | check (default) or list_versions (returns versions for the UI "Specific Version" selector) |
Response
{
"status": "success",
"channel": "stable",
"update_available": true,
"latest_version": "1.0.2",
"release_notes": "Stable release. Live capture summary, ECharts visuals, hardened OTA signing...",
"sha256": "<sha256-from-server>",
"file_size": 13875482,
"current_version": "1.0.1"
}
action=list_versions, the response includes versions[] instead of the standard update-availability fields.Parameters
| Name | Required | Description |
|---|---|---|
| action | required | download, apply, status, or cancel |
| version | required* | Version to download (e.g. 1.0.2). Required when action=download. |
| channel | for download | stable or pinned. Used for server-side channel validation and file selection. |
| sha256 | for download | Expected checksum for verification |
Example: Full Update Flow
# 1. Check for updates curl -X POST "http://192.168.1.100/banana/check_update" # 2. Download firmware curl -X POST "http://192.168.1.100/banana/apply_update?action=download&version=1.0.2&channel=stable&sha256=<sha256-from-check_update>" # 3. Apply update (device reboots) curl -X POST "http://192.168.1.100/banana/apply_update?action=apply" # 4. Wait ~90 seconds, then check status curl -X POST "http://192.168.1.100/banana/apply_update?action=status"
apply action triggers a full firmware flash and reboot. The device will be unavailable for 1-2 minutes. Do not power off during the update.Scripting Examples
Bash: Latency Sweep Test
#!/bin/bash # Test app behavior across increasing latency values NOTBOX="http://192.168.1.100" TOKEN="example-token" # Optional: only needed if password is enabled AUTH="-H \"Authorization: Bearer ${TOKEN}\"" for latency in 0 25 50 100 200 500; do echo "Testing with ${latency}ms latency..." curl -s -X POST ${AUTH} "${NOTBOX}/banana/set_impairment?interface=download&delay=${latency}" > /dev/null sleep 2 # Run your test suite here # ./run-tests.sh echo " Latency test at ${latency}ms complete" done # Clean up curl -s -X POST ${AUTH} "${NOTBOX}/banana/clear_impairment?interface=download" > /dev/null echo "Done - impairments cleared"
Python: Automated Impairment + Capture
import requests, time, json NOTBOX = "http://192.168.1.100" HEADERS = {"Authorization": "Bearer example-token"} # Optional: only if password enabled # Set impairment requests.post(f"{NOTBOX}/banana/set_impairment", headers=HEADERS, params={ "interface": "download", "delay": "100", "loss": "5", "bandwidth": "10", "bandwidth_unit": "mbit" }) # Start capture r = requests.post(f"{NOTBOX}/banana/start_capture", headers=HEADERS, params={ "duration": "30", "proto_filter": "either:https" }) filename = r.json()["filename"] # Wait for capture to finish time.sleep(35) # Summarize the capture (sub-millisecond) summary = requests.post(f"{NOTBOX}/banana/get_live_stats", headers=HEADERS, params={"filename": filename}).json() print(f"Packets: {summary['packet_count']}") print(f"Download: {summary['download_bytes']} bytes") print(f"Top destinations:") for dest in summary["top_dest"][:5]: print(f" {dest['name'] or dest['ip']}: {dest['bytes']} bytes") # Clean up requests.post(f"{NOTBOX}/banana/clear_impairment", headers=HEADERS, params={"interface": "download"}) requests.post(f"{NOTBOX}/banana/delete_capture", headers=HEADERS, params={"filename": filename})
CI/CD: Pre-Deploy Network Check
#!/bin/bash # Verify app handles poor network conditions before deploy NOTBOX="http://192.168.1.100" AUTH="-H \"Authorization: Bearer ${NOTBOX_TOKEN}\"" # Set NOTBOX_TOKEN in CI secrets # Simulate a degraded network curl -s -X POST ${AUTH} "${NOTBOX}/banana/set_impairment?interface=both&delay=200&loss=3" # Run integration tests against device under test npm run test:integration TEST_EXIT=$? # Always clean up curl -s -X POST ${AUTH} "${NOTBOX}/banana/clear_impairment?interface=both" exit $TEST_EXIT