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.
AI Assistants
A ready-made skill teaches an AI coding agent to operate a NOTBOX through this API. Download notbox-agent-skill.zip, then unzip it where your agent looks for skills:
| Agent | Where the folder goes |
|---|---|
| Claude Code | .claude/skills/notbox-agent-skill/ in your project |
| OpenAI Codex | .agents/skills/notbox-agent-skill/ in your repo, or ~/.agents/skills/ for every repo |
| GitHub Copilot | .github/skills/notbox-agent-skill/ in your repo |
| Other agents | Attach SKILL.md and references/device-api.md as context |
Give the agent the management address. Provide the API token through the agent’s secret mechanism, never in the chat.
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 (public endpoint, no token needed) curl "http://192.168.1.100/banana/get_serial" # Add 100ms latency + 2% packet loss to downloads curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/set_impairment?interface=download&delay=100&jitter=20&loss=2" # Check current impairment status curl -H "Authorization: Bearer <token>" "http://192.168.1.100/banana/get_status" # Clear all impairments curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/clear_impairment?interface=both" # Start a 60-second packet capture curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/start_capture?duration=60" # Run a speed test curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/run_speedtest"
All responses are JSON. Every /banana/ endpoint requires your API token in the Authorization: Bearer header, except a small set of public endpoints (see the Authentication section). The token exists from first boot. Find it in the web UI under Settings > Security > API Access Token.
# Every non-public call carries the token (found in Settings > Security > API Access Token) curl -H "Authorization: Bearer <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
Read endpoints accept GET and tolerate POST. Endpoints that change something accept POST only; a GET to one returns 405. POST-only reads: check_update, network_tools, get_live_stats, and the action=status forms of bandwidth_monitor, port_mirror, and apply_update. Each endpoint below is tagged with its method.
Authentication
Every /banana/ endpoint requires authentication. Scripts authenticate with the device API token in the Authorization: Bearer <token> header. The token exists from first boot, so you never have to create one. Find it in the web UI under Settings > Security > API Access Token.
Two credential types are accepted:
- An API token in the
Authorization: Bearerheader, recommended for scripts - A session cookie set by
/banana/login, used by the browser UI
The only endpoints callable without a token are the public ones: check_auth, login, get_password_status, get_serial, get_control, and get_device_id. Every other endpoint returns 401 Unauthorized without a valid token.
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.
Tools like curl, Python scripts, and Postman aren't affected by the browser-origin restriction. A request with a valid customer API token also remains usable regardless of whether the local browser or HENcloud dashboard currently owns interactive control.
403 Forbidden can also appear when the local browser session calls a configuration endpoint while HENcloud owns control, or when HENcloud calls one while the local screen owns control. These restrictions do not apply to a valid API token.
Rate Limit
Up to 30 requests per second per IP address. Going over returns 429 Too Many Requests.
Request Size
Request bodies are capped at 64 KiB; a larger body fails with 400.
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; long-running things add state) { "status": "success", "state": "stopped" }
Errors look like:
{
"status": "error",
"message": "Invalid delay (must be 0-10000 ms)"
}
Check the status field first: it is success or error on every response and never carries anything else. Errors always use a real HTTP status code (table below) together with the JSON envelope; never plain text, never HTTP 200 with "status": "error". An unknown /banana/ path is a JSON 404.
Shared rules across every endpoint:
- Long-running things (captures, monitors, the bandwidth monitor, port mirror, the WiFi AP, mounted shares) report
state:runningorstopped - Calls that may or may not change something report
changed:trueorfalse - Numbers are JSON numbers, never strings, with one string exception:
get_wifi_configchannel(autoor a channel number). Sizes are bytes. Rates are bits per second (_bps) unless the key says_mbps. Durations are seconds (_s) or milliseconds (_ms). Percentages are plain numbers - Timestamps are Unix seconds in keys ending
_at(ortimestampinside data arrays) - Lists are JSON arrays. A value that does not exist (an unresolved host, no pending version, no address) is
null. Empty-string exception:get_network_configip_addr,netmask,gateway, anddnsare""when not configured - Direction words are
downloadandupload. Interface labels arewifi-2gandwifi-5g. WiFi band is2gor5g
HTTP Status Codes
| Code | Meaning | What to do |
|---|---|---|
200 | OK | The call succeeded; status is success |
400 | Bad Request | A parameter is missing or wrong; check the message |
401 | Unauthorized | Add your API token or log in |
403 | Forbidden | A browser called from a page not served by NOTBOX, or a screen called a configuration endpoint while the other screen owned control. API-token requests are never blocked by screen ownership. |
404 | Not Found | The endpoint, file, or resource doesn't exist |
405 | Method Not Allowed | Wrong HTTP method (GET to a POST endpoint) |
429 | Too Many Requests | Slow down. You're over the rate limit |
500 | Internal Server Error | Unusual. Try again; contact support if it persists |
502 | Bad Gateway | The NOTBOX update server could not be reached or gave an invalid reply (check_update) |
503 | Service Unavailable | The device has no internet connection (check_update) |
Endpoints
Impairment Control
Parameters
| Name | Required | Description |
|---|---|---|
| interface | required | upload, download, or both. A single WiFi band is also accepted: wifi-2g, wifi-5g |
| delay | optional | Latency in milliseconds, 0-10000 (default: 0) |
| jitter | optional | Jitter in milliseconds, 0-1000 (default: 0) |
| loss | optional | Packet loss percentage, 0-100 (default: 0) |
| bandwidth | optional | Bandwidth limit value, 0-10000 (default: 0 = unlimited) |
| bandwidth_unit | optional | mbit (default), kbit, or gbit |
Example
curl -H "Authorization: Bearer <token>" -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. A single WiFi band is also accepted: wifi-2g, wifi-5g |
Example
curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/clear_impairment?interface=both"
{
"status": "success",
"message": "Impairment cleared on both",
"changed": true
}
changed is false when no impairment was set on that interface.Parameters
None
Response
{
"status": "success",
"active": true,
"download": {
"delay": 100,
"jitter": 20,
"loss": 2,
"bandwidth": 10
},
"upload": {
"delay": 0,
"jitter": 0,
"loss": 0,
"bandwidth": 0
}
}
active is true if any interface has impairments. The download and upload objects carry delay and jitter in ms, loss in percent, bandwidth in Mbit. 0 means not 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: an address or CIDR (IPv4 or IPv6), or an IPv4 range of at most 256 addresses (e.g. dst:192.168.1.0/24, src:192.168.1.10-192.168.1.20) |
| 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, mqtts, coap, coaps, snmp. Other single-keyword protocols (icmp, arp, tcp, udp, ip6) are also accepted |
| use_network | optional | Set to true to save the capture on the mounted SMB or NFS share |
Example
# Capture HTTPS traffic for 120 seconds curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/start_capture?duration=120&proto_filter=either:https"
{
"status": "success",
"filename": "notbox_20260216_143022.pcap",
"duration_s": 120,
"storage": "local",
"filter": "( tcp port 443 )"
}
?ip_filter=src:192.168.1.5&ip_filter=dst:192.168.1.9). filter in the response is the compiled filter expression, null when capturing everything. storage is local, smb, or nfs.Parameters
None
Response
{
"status": "success",
"filename": "notbox_20260216_143022.pcap",
"size": 5242880,
"auto_stopped": false,
"stop_reason": null,
"started_at": 1715000000,
"ended_at": 1715000042,
"elapsed_s": 42,
"duration_s": 120,
"storage": "local"
}
stop_reason is null for a manual stop, otherwise time, size, write_error, cleanup_failed, process_exit, or daemon_restart. size is bytes; elapsed_s is how long the capture ran and duration_s the limit it was started with. A capture that did not finish cleanly adds "incomplete": true together with write_error and unwritten_bytes, or cleanup_failed. With no capture running, the call answers 200 with the last finished capture's details, or HTTP 400 No capture running when there is none.Parameters
None
Response
{
"status": "success",
"state": "running",
"filename": "notbox_20260216_143022.pcap",
"size": 1048576,
"elapsed_s": 15,
"duration_s": 60,
"storage": "local",
"filter": "( tcp port 443 )"
}
size is bytes. A capture started with filters also carries filters (the terms as an object) and filter (the compiled expression).{"status": "success", "state": "stopped", "auto_stopped": false}. While the last finished capture is still available it also carries its filename, size, duration_s, elapsed_s, started_at, ended_at, storage, and stop_reason (null after a manual stop), with auto_stopped true for a time or size stop. There is no endpoint that lists stored files: keep the name returned by start_capture.Parameters
| Name | Required | Description |
|---|---|---|
| filename | required | PCAP filename (e.g. notbox_20260216_143022.pcap) |
Example
# Download to local machine curl -H "Authorization: Bearer <token>" -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. A local capture is deleted five minutes after it is downloaded, so a later download is 404; files on a share are kept. Errors are JSON: HTTP 400 for an invalid filename, 404 when the file does not exist, 500 when it cannot be read.Parameters
| Name | Required | Description |
|---|---|---|
| filename | optional | Specific PCAP filename to delete. If omitted, deletes all local captures. |
Response
{
"status": "success",
"message": "Capture deleted",
"storage": "local",
"changed": true
}
filename) returns "Deleted N capture file(s)" with storage local, or changed false and no storage when nothing was on disk; it skips the running capture. A named file on a share returns "Capture deleted from network share" with storage smb or nfs. A named file that is not on disk returns changed false with no storage. Deleting the running capture is HTTP 400.Parameters
| Name | Required | Description |
|---|---|---|
| filename | required | PCAP filename returned from start_capture |
Example
curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/get_live_stats?filename=notbox_20260526_001518.pcap"
Response
{
"status": "success",
"summary_version": 2,
"cached": false,
"live": true,
"dns": [{ "name": "example.com", "count": 5, "ips": ["93.184.216.34"], "status": "NOERROR", "noise": false }],
"top_dest": [{ "ip": "93.184.216.34", "bytes": 2097152, "proto": "tcp", "app": "https", "port": 443, "name": "example.com", "name_source": "dns", "locals": ["192.168.1.10"], "noise": false }],
"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", "name_a": "", "name_b": "example.com", "name_a_source": "", "name_b_source": "dns", "proto": "tcp", "app": "https", "packets": 310, "bytes": 1048576, "bytes_a_to_b": 262144, "bytes_b_to_a": 786432, "noise": false }],
"protocols": [{ "name": "https", "packets": 420, "bytes": 4123456, "pct_bytes": 78.7, "pct_packets": 77.5, "noise": false }],
"tcp_health": { "out_of_order": 2, "dup_acks": 5, "resets": 1, "syn_packets": 12, "syn_ack_packets": 12, "syn_retries": 0, "failed_handshakes": 0 },
"failures": [{ "ip": "192.168.1.1", "count": 3, "type": "rst", "from": "local" }],
"sni": [{ "name": "example.com", "count": 8, "ip": "93.184.216.34", "source": "tls-parse" }],
"timeline": [{ "offset_sec": 0, "packets": 42, "bytes": 102400, "noise_packets": 3, "noise_bytes": 512 }],
"bucket_s": 5,
"total_bytes": 5242880,
"upload_bytes": 2097152,
"download_bytes": 3145728,
"packet_count": 542,
"span_s": 14.2,
"first_at": 1771252222.0,
"last_at": 1771252236.2
}
first_at and last_at are Unix seconds (fractional) of the first and last packet seen, null before the first packet; span_s is the seconds between them; bucket_s is the width of each timeline bucket. ips, locals, dns_names, and sni_names are arrays. top_dest[].port is a number, null for hosts without a port. Protocol and app names are icmp, dns, mdns, ssdp, dhcp, ntp, http, https, ssh, tcp, udp, other, arp, ipv6. Names are port-based, so encrypted traffic on a non-standard port reads as tcp. failures[] entries carry the remote ip, type (rst or icmp), from (local = sent by the tested device, remote = sent by the far end), and count; a remote can appear once per sender. top_conversations[] entries carry endpoint_a/endpoint_b (either side may be the client; the client's addresses are in top_dest[].locals), name_a/name_b with their _source, proto, app, packets, bytes, bytes_a_to_b, bytes_b_to_a, and noise. upload_bytes/download_bytes are from the tested client's perspective. The reply also carries summary_version and cached. The live field is true during capture and false once it stops. If the filename has no data (after a reboot or delete_capture), the response is HTTP 404 with {"status":"error","message":"No live capture state for that filename"}.Parameters
| Name | Required | Description |
|---|---|---|
| action | required | start, stop, or status (enable/disable are accepted aliases) |
Example
# Enable port mirror (LOCAL port becomes mirror-only) curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/port_mirror?action=start"
{
"status": "success",
"state": "running",
"changed": true,
"message": "Port mirror enabled - LOCAL port is now mirror-only"
}
action=status returns {"status": "success", "state": "running"} or {"status": "success", "state": "stopped"}. action=stop returns state stopped with changed false when no mirror was running. changed is false whenever the mirror was already in the requested state. An unknown action is HTTP 400 and a failure to switch is HTTP 500, both with the JSON error envelope.Monitoring
Parameters
| Name | Required | Description |
|---|---|---|
| target | optional | Host to ping (default: 8.8.8.8). Each distinct target runs its own monitor. |
| interval | optional | Ping interval in seconds, 5-300 (default: 10). Out-of-range values are clamped; non-numeric falls back to 10 |
Response
{
"status": "success",
"target": "8.8.8.8",
"interval": 10,
"resolved_ip": "8.8.8.8"
}
resolved_ip is null when the target could not be resolved. Starting a target that is already running updates its interval and answers with "message": "Updated interval". At most 8 targets run at once; the ninth is HTTP 400 Monitor limit reached (8).Parameters
| Name | Required | Description |
|---|---|---|
| target | required | Host being monitored (the same target you started). Returns Missing target if omitted; HTTP 400 Monitor not running for a target that is not running. |
Response
{ "status": "success", "target": "8.8.8.8" }
Parameters
| Name | Required | Description |
|---|---|---|
| target | required | Host being monitored. Returns Missing target if omitted. |
| 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": "success",
"state": "running",
"target": "8.8.8.8",
"data": [
{ "timestamp": 1705000007.5, "delay": 25.5, "loss": 0.0 },
{ "timestamp": 1705000022.5, "delay": 26.3, "loss": 0.0 }
],
"point_count": 2,
"bucket_s": 5,
"max_delay": 41.8,
"max_delay_at": 1705000019
}
timestamp is the bucket midpoint in Unix seconds. max_delay is the raw maximum sample (ms) in the window and max_delay_at is when it occurred. When the monitor is not running the response is {"status": "success", "state": "stopped", "data": [], "target": "8.8.8.8"}.bucket_s seconds (5s to 4h); each point is the bucket average. delay in ms, loss as a percentage. History is kept for 30 days. max_delay_at is null when the window has no samples.Parameters
None
Response
{
"status": "success",
"monitors": [
{ "target": "8.8.8.8", "interval": 10, "resolved_ip": "8.8.8.8", "data_points": 150 }
],
"count": 1
}
resolved_ip is null for a target that could not be resolved. count is the number of running monitors.Parameters
| Name | Required | Description |
|---|---|---|
| action | required | start, stop, or status |
| interface | optional | auto (default) |
Example
curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/bandwidth_monitor?action=start&interface=auto"
action=status returns {"status": "success", "state": "running", "interface": "auto"} or {"status": "success", "state": "stopped"}; action=stop returns {"status": "success", "message": "Stopped", "changed": true}, changed false when no monitor was running. Only one bandwidth monitor exists: starting replaces a running one, so check status first.{ "status": "success", "interface": "auto" }
download and upload are 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",
"state": "running",
"interface": "auto",
"data": [
{ "timestamp": 1705000005, "download_bps": 8192000, "upload_bps": 4096000, "wired_download_bps": 7200000, "wired_upload_bps": 3840000, "wifi_download_bps": 992000, "wifi_upload_bps": 256000 }
],
"point_count": 1,
"bucket_s": 5,
"totals": { "download_bytes": 10485760, "upload_bytes": 5242880, "running_s": 120 }
}
running_s is how long the monitor has been running. Data is aggregated into buckets of bucket_s seconds; each point's rates are the bucket average. Each point also carries wired_download_bps_min/_max, wired_upload_bps_min/_max, wifi_download_bps_min/_max and wifi_upload_bps_min/_max for the bucket. When no monitor is running the response is {"status": "success", "state": "stopped", "data": []}.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 (rdns and iplookup take an IPv4 address). Not needed for: arp, arpscan, publicip, update_oui. |
| count | optional | Ping count, 1-20 (default: 5). Values outside the range are clamped |
| server | optional | DNS server for dns lookups |
Examples
# Ping curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/network_tools?tool=ping&target=8.8.8.8&count=3" # DNS lookup curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/network_tools?tool=dns&target=google.com" # Traceroute curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/network_tools?tool=traceroute&target=cloudflare.com" # ARP table with vendor lookup curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/network_tools?tool=arp" # WHOIS lookup curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/network_tools?tool=iplookup&target=8.8.8.8" # Get public IP curl -H "Authorization: Bearer <token>" -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_ms": 21.5, "rtt_avg_ms": 24.2, "rtt_max_ms": 28.1
}
}
stats is absent when ping produced no summary. Other tools: dns returns resolver (the server that answered) and addresses[], plus cname when there is one; rdns returns ip and hostnames[]; arp returns entries[] (ip, mac, vendor) and arpscan adds scanned and subnet; update_oui returns count (vendors loaded); iplookup refuses private addresses with HTTP 400, always returns ip and hostnames[], plus a whois object carrying only the keys found (org, net_name, cidr, range, type, country, abuse_email); when the lookup fails, whois is replaced by whois_status unavailable and whois_error; traceroute returns output text (one line per hop); publicip returns ip.Parameters
None
Response
{
"status": "success",
"download_mbps": 146,
"upload_mbps": 89,
"rtt_ms": 12,
"jitter_ms": 3,
"server": "Chicago, USA (ORD)",
"sponsor": "",
"isp": "",
"provider": "cloudflare"
}
A speed test is already running. With no internet it is HTTP 400 Lost upstream connection. Measures the NOTBOX uplink, not the device under test. Throughput is rounded to whole Mbps. server is the nearest Cloudflare edge as City, Country (POP-code), or Cloudflare (POP-code) when the city is unlisted. sponsor and isp are always empty.Parameters
| Name | Required | Description |
|---|---|---|
| target | required | Hostname or IP |
| port | required | Port number (1-65535) |
Example
curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/test_port?target=google.com&port=443"
{
"status": "success",
"result": "open",
"rtt_ms": 45,
"target": "google.com",
"port": 443,
"ip": "142.251.33.14"
}
result is open, closed, or unresolved when the hostname cannot be resolved; ip is null in that case. rtt_ms is the connect time in milliseconds.WiFi Management
Parameters (POST body, form-encoded)
| Name | Required | Default | Description |
|---|---|---|---|
| ssid | optional | NetTest-Impaired | Network name (1-32 chars) |
| password | optional | WiFi password (8-63 chars, required unless encryption=none). Read only from the body; a query-string password is ignored | |
| 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 -H "Authorization: Bearer <token>" -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": "5g",
"interface": "wifi-5g"
}
Parameters
None
Response
{ "status": "success" }
Parameters
None
Response
{
"status": "success",
"state": "running",
"ssid": "Example-Test-AP",
"band": "5g",
"interface": "wifi-5g"
}
{"status": "success", "state": "stopped"} when WiFi is off. band is 2g or 5g, the same values configure_wifi accepts; interface is wifi-2g or wifi-5g.Parameters
None
Response
{
"status": "success",
"configured": true,
"ssid": "Example-Test-AP",
"band": "5g",
"channel": "auto",
"encryption": "psk2",
"password": "example-password"
}
password is the saved passphrase in clear text; any token holder can read it, so treat the response as a secret. Before an AP has ever been saved the response is {"status": "success", "configured": false}.Parameters
None
Response
{
"status": "success",
"count": 1,
"clients": [
{ "mac": "aa:bb:cc:dd:ee:ff", "signal_dbm": -42, "connected_s": 900 }
]
}
signal_dbm is the station's signal in dBm; connected_s is how long it has been associated, in seconds. count is 0 with an empty clients array when the AP is off.Network Storage (SMB / NFS)
Parameters (POST body, form-encoded)
| Name | Required | Description |
|---|---|---|
| host | required | SMB server hostname or IP |
| share | required | Share name |
| user | optional | Username for auth |
| password | optional | Password for auth. Read only from the body; a query-string password is ignored |
| domain | optional | Domain for auth |
| path | optional | Subfolder within share |
Response
{
"status": "success",
"message": "Connected and verified",
"changed": true,
"mount_point": "/mnt/capture_share",
"share": "//example-fileserver/captures",
"capture_dir": "/mnt/capture_share/notbox",
"available_bytes": 536870912000,
"storage": "smb"
}
available_bytes is free space in bytes. When the same share is already mounted the response is "message": "Already connected" with changed false and only mount_point, share, and storage. Mounting a different share while a capture is writing to the share is HTTP 400.Parameters
None
Response
{ "status": "success", "message": "Disconnected", "changed": true }
"message": "Not mounted" with changed false.Parameters (POST body or query)
| 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",
"changed": true,
"mount_point": "/mnt/capture_share",
"capture_dir": "/mnt/capture_share/site-a",
"share": "192.168.1.50:/srv/notbox-pcaps",
"available_bytes": 536870912000,
"storage": "nfs"
}
auto mode the NFS version is negotiated automatically. available_bytes is free space in bytes. When the same export is already mounted the response is "message": "Already connected" with changed false and only mount_point, share, and storage. Mounting a different export while a capture is writing to the share is HTTP 400.Parameters
None
Response
{ "status": "success", "message": "Disconnected", "changed": true }
"message": "Not mounted" with changed false.Parameters
None
Response
{
"status": "success",
"state": "running",
"share": "//example-fileserver/captures",
"capture_dir": "/mnt/capture_share/notbox",
"available_bytes": 536870912000,
"used_bytes": 268435456000,
"writable": true,
"capture_count": 5,
"storage": "smb"
}
{"status": "success", "state": "stopped"} when no share is mounted (running means mounted). Sizes are bytes; capture_count is the number of capture files on the share. If the share stops responding the response carries "stalled": true with writable false and zero size fields.Parameters
None
Response
{
"status": "success",
"state": "running",
"share": "192.168.1.50:/srv/notbox-pcaps",
"capture_dir": "/mnt/capture_share/site-a",
"available_bytes": 536870912000,
"used_bytes": 268435456000,
"writable": true,
"capture_count": 5,
"storage": "nfs"
}
{"status": "success", "state": "stopped"} when no NFS share is mounted (running means mounted). Sizes are bytes; capture_count is the number of capture files on the export. If the share stops responding the response carries "stalled": true with writable false and zero size fields.System & Configuration
Parameters
None
Response
{
"status": "success",
"cpu_pct": 42,
"ram_used_bytes": 72351744,
"ram_total_bytes": 245366784,
"disk_free_bytes": 86822912,
"tmp_used_bytes": 209715,
"tmp_total_bytes": 122683392,
"temp_c": 60,
"uptime_s": 445500,
"download_bps": 5200000,
"upload_bps": 1300000,
"time_at": 1771819200,
"timezone": "EST5EDT,M3.2.0,M11.1.0",
"zonename": "America/New_York"
}
cpu_pct is a percentage. Memory and storage are bytes. temp_c is Celsius, null when no sensor is available. uptime_s is seconds. download_bps and upload_bps are test-traffic rates in bits per second since the previous call; the first call reads 0. time_at is the device clock in Unix seconds.Parameters
None
Response
{
"status": "success",
"serial": "NB20260100042"
}
Parameters
None
Response
{
"status": "success",
"hostname": "NOTBOX",
"ip_mode": "dhcp",
"ip_addr": "",
"netmask": "",
"gateway": "",
"dns": "",
"mgmt_ip": "192.168.1.100",
"current_gateway": "192.168.1.1",
"current_dns": "192.168.1.1"
}
ip_addr, netmask, gateway, and dns are the saved static settings (empty strings under DHCP); mgmt_ip, current_gateway, and current_dns are what MGMT is using right now. mgmt_ip, current_gateway, and current_dns are null when MGMT has no address.Parameters
| Name | Required | Description |
|---|---|---|
| hostname | optional | Device hostname (1-63 chars) |
| ip_mode | required | 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",
"dhcp": false,
"needs_reload": true
}
ip_mode=dhcp, new_ip is null and dhcp is true: the new address is whatever the DHCP server hands out. Every static-mode call restarts networking, even with unchanged values; DHCP restarts only when switching from static. When nothing restarts (hostname only, or DHCP while already on DHCP) the reply is {"status": "success", "message": "Settings saved.", "needs_reload": false} with no new_ip or dhcp.Parameters
None
Response
{
"status": "success",
"mode": "full",
"internet": true,
"dns_ok": true,
"connection": "network",
"connection_desc": "Connected via network (MGMT)",
"mgmt_link_up": true,
"mgmt_ip": "192.168.1.100",
"local_link_up": true,
"local_ip": "192.168.100.1",
"disabled_features": []
}
mode is "full" with internet access or "limited" without. In limited mode disabled_features lists uplink_monitor, speedtest, smb_capture, and port_test_external; the web UI greys these out, but the API still accepts monitor, port test, and capture calls. Only run_speedtest refuses, with HTTP 400 Lost upstream connection. mgmt_ip is null when MGMT has no address; local_ip is never null.Parameters
None
Response
{
"status": "success",
"mgmt_exists": true,
"mgmt_ip": "192.168.1.100",
"mgmt_prefix_len": 24,
"ping_success": true,
"current_gateway": "192.168.1.1",
"all_interfaces": ["lo", "eth0"]
}
mgmt_ip is null when MGMT has no address (mgmt_prefix_len is then 0); current_gateway is null when there is no default route. ping_success is a ping to 8.8.8.8 from the MGMT port.Authentication
Parameters
None (reads notbox_session cookie automatically)
Response
{
"status": "success",
"auth_required": true,
"is_authorized": false
}
auth_required reflects whether a device password is set; with no password the response is just {"status": "success", "auth_required": false}. is_authorized reflects only the session cookie, so a token client always sees false.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. A wrong password is HTTP 400 Incorrect password; after 10 consecutive failures from one IP, login is refused with HTTP 400 for 60 seconds.Parameters
None
Response
{ "status": "success", "message": "Logged out" }
Parameters
| Name | Required | Description |
|---|---|---|
| action | required | Query param: enable, disable, or change |
| new_password | for enable/change | New password (min 8 characters, POST body) |
| current_password | for disable/change | Current password (POST body) |
Example
# Enable password protection curl -H "Authorization: Bearer <token>" -d "new_password=example-password" "http://192.168.1.100/banana/set_password?action=enable"
{ "status": "success", "message": "Password protection enabled" }
Parameters
None
Response
{ "status": "success", "password_set": false }
Parameters
None (requires password protection to be enabled, plus a session cookie or a Bearer token)
Example
curl -X POST -b cookies.txt "http://192.168.1.100/banana/generate_api_token"
{
"status": "success",
"token": "0123456789abcdef0123456789abcdef"
}
Parameters
None (requires a session cookie or a Bearer token)
Response
{
"status": "success",
"token_set": true,
"token": "0123456789abcdef0123456789abcdef"
}
{"status": "success", "token_set": false} with no token field.Parameters
None (requires a session cookie or a Bearer token)
Response
{ "status": "success", "message": "API token revoked" }
generate_api_token (needs a password) or the next reboot.Firmware Updates
Parameters
| Name | Required | Description |
|---|---|---|
| channel | optional | stable (default), beta, or pinned (specific and all are aliases of pinned). An unknown value falls back to stable; the call never fails on a bad channel or action by itself. |
| requested_version | pinned only | Exact version to check (for example 1.1.0) |
| action | optional | check (default) or list_versions |
Response
{
"status": "success",
"channel": "stable",
"device_id": "NB-0a1b2c3d4e5f",
"update_available": true,
"latest_version": "1.1.0",
"release_notes": "Bug fixes and improvements.",
"sha256": "<sha256-from-server>",
"signature": "<ed25519-signature-from-server>",
"size": 13875482,
"version": "1.0.4"
}
version is the running firmware; size is the image size in bytes. Every reply, errors included, carries version and device_id. release_notes, size, sha256, and signature are present only when update_available is true. Errors use the JSON error envelope: HTTP 503 when the device has no internet connection, HTTP 502 when the update server cannot be reached or gives an invalid reply ("message": "Update server unavailable"), HTTP 400 for a request the update service rejected.action=list_versions the response carries hardware_model and versions[] with version, filename, size (bytes), release_notes, published_at (Unix seconds), sha256, and signature.Parameters
| Name | Required | Description |
|---|---|---|
| action | required | download, apply, status, or cancel |
| version | required* | Version to download (e.g. 1.1.0). Required when action=download. |
| channel | for download | stable (default when omitted), beta, or pinned. Must match the channel used for check_update. |
| sha256 | for download | Expected SHA-256 checksum (64 lowercase hex chars). Required for download. |
| signature | for download | Ed25519 firmware signature, base64-encoded. Required for download. |
Example: Full Update Flow
# 1. Check for updates curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/check_update" # 2. Download firmware (sha256 + signature both required, from check_update) curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/apply_update?action=download&version=1.1.0&channel=stable&sha256=<sha256-from-check_update>&signature=<signature-from-check_update>" # 3. Apply update (device reboots) curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/apply_update?action=apply" # 4. Wait ~90 seconds, then check status curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/apply_update?action=status"
Response
# action=download { "status": "success", "message": "Firmware downloaded and verified", "target_version": "1.1.0", "size": 17008922 } # action=apply { "status": "success", "message": "Applying firmware update. Device will reboot.", "target_version": "1.1.0" } # action=status { "status": "success", "state": "downloaded", "version": "1.0.4", "target_version": "1.1.0", "firmware_ready": true }
state is idle, downloading, downloaded, applying, or failed. version is the running firmware and target_version the one being installed, null when nothing is pending. size is bytes. action=download blocks until the file is fetched (up to five minutes). A second download, apply, or cancel while one is in flight is HTTP 400 An update operation is already in progress.apply installs the firmware and reboots. The device is unavailable for 1-2 minutes. Do not power off during the update.HENcloud & Control
Optional. A NOTBOX works fully without HENcloud. These endpoints show and change how the device is linked to HENcloud and which screen, the local page or the HENcloud dashboard, currently owns configuration. Requests carrying the API token are never blocked by that ownership.
Parameters
None
Example
curl "http://192.168.1.100/banana/get_device_id"
Response
{
"status": "success",
"device_id": "NB-0a1b2c3d4e5f",
"version": "1.1.0",
"mac": "aa:bb:cc:dd:ee:ff",
"serial": "NB20260100042",
"model": "NOTBOXv1"
}
device_id is the identifier HENcloud shows.Parameters
None
Example
curl "http://192.168.1.100/banana/get_control"
Response
{
"status": "success",
"owner": "local",
"cloud_present": false,
"local_override": false,
"cloud_url": null
}
owner is local or cloud. cloud_present is true while HENcloud is connected. local_override is true after take_local_control. cloud_url is the device's link on the HENcloud dashboard, null until the device id exists.Parameters
None
Example
curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/take_local_control"
Response
{
"status": "success",
"message": "Local control. HENcloud is now view-only for this device.",
"changed": true,
"previous_owner": "cloud"
}
resume_cloud_control is called. The reply carries changed (false when local control was already taken) and previous_owner.Parameters
None
Example
curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/resume_cloud_control"
Response
{
"status": "success",
"message": "Cloud control resumed.",
"changed": true,
"previous_owner": "local"
}
previous_owner reports who owned control before the call (local or cloud); changed is false when HENcloud already owned it. When HENcloud itself resumes control on a password-protected device, local browser sessions are cleared and the local page asks for the password again.Parameters
None
Example
curl -H "Authorization: Bearer <token>" "http://192.168.1.100/banana/get_cloud_status"
Response
{
"status": "success",
"enrolled": true,
"claim_pending": false,
"state": "connected",
"device_id": "NB-0a1b2c3d4e5f"
}
state is off, pending (claim code saved, not yet redeemed), or connected (enrolled; live presence is get_control cloud_present). While not enrolled, an enroll_error string is added if HENcloud refused the last attempt.Parameters
| Name | Required | Description |
|---|---|---|
| code | required | Claim code from HENcloud (Add device). Form body or query. Case and separators are ignored. |
Example
curl -H "Authorization: Bearer <token>" -X POST -d "code=HEN-ABCDE-FGHJK" "http://192.168.1.100/banana/set_cloud_claim"
Response
{
"status": "success",
"message": "Claim code saved. Linking this device to your HENcloud account..."
}
get_cloud_status for state to become connected.Parameters
None
Example
curl -H "Authorization: Bearer <token>" -X POST "http://192.168.1.100/banana/disconnect_cloud"
Response
{
"status": "success",
"message": "Cloud management disconnected."
}
Scripting Examples
Bash: Latency Sweep Test
#!/bin/bash # Test app behavior across increasing latency values NOTBOX="http://192.168.1.100" TOKEN="example-token" # Required: from Settings > Security > API Access Token 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"} # Required: from Settings > Security > API Access Token # 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 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