Heller Enterprise Networks
Setup Guide API Docs BETA Contact About HENcloud

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.

Beta API: This API is functional but still under active development. Endpoints, parameters, and response formats may change between firmware versions. Please report any issues to [email protected].

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:

AgentWhere 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 agentsAttach 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:

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:

HTTP Status Codes

CodeMeaningWhat to do
200OKThe call succeeded; status is success
400Bad RequestA parameter is missing or wrong; check the message
401UnauthorizedAdd your API token or log in
403ForbiddenA 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.
404Not FoundThe endpoint, file, or resource doesn't exist
405Method Not AllowedWrong HTTP method (GET to a POST endpoint)
429Too Many RequestsSlow down. You're over the rate limit
500Internal Server ErrorUnusual. Try again; contact support if it persists
502Bad GatewayThe NOTBOX update server could not be reached or gave an invalid reply (check_update)
503Service UnavailableThe device has no internet connection (check_update)

Endpoints

Impairment Control

POST /banana/set_impairment Apply network impairments to an interface ▶

Parameters

NameRequiredDescription
interfacerequiredupload, download, or both. A single WiFi band is also accepted: wifi-2g, wifi-5g
delayoptionalLatency in milliseconds, 0-10000 (default: 0)
jitteroptionalJitter in milliseconds, 0-1000 (default: 0)
lossoptionalPacket loss percentage, 0-100 (default: 0)
bandwidthoptionalBandwidth limit value, 0-10000 (default: 0 = unlimited)
bandwidth_unitoptionalmbit (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.
POST /banana/clear_impairment Remove all impairments from an interface ▶

Parameters

NameRequiredDescription
interfacerequiredupload, 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.
GET /banana/get_status Get current impairment status on all interfaces ▶

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

POST /banana/start_capture Start a packet capture with optional filters ▶

Parameters

NameRequiredDescription
durationoptionalCapture duration in seconds (default: 60)
ip_filteroptionalIP 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_filteroptionalPort filter as direction:port (e.g. either:443)
mac_filteroptionalMAC filter as direction:MAC (e.g. src:aa:bb:cc:dd:ee:ff)
proto_filteroptionalProtocol 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_networkoptionalSet 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 )"
}
Local captures are capped at 50 MB. Network storage captures have no size limit. Only one capture can run at a time. Starting a capture deletes the previous local capture file (share captures are kept), so download it first. Filters are query parameters and may be repeated (?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.
POST /banana/stop_capture Stop a running capture ▶

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.
GET /banana/get_capture_status Check capture progress ▶

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).
When nothing is running the response is {"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.
GET /banana/download_capture Download a PCAP file ▶

Parameters

NameRequiredDescription
filenamerequiredPCAP 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"
Returns the PCAP file as binary data with 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.
POST /banana/delete_capture Delete capture files ▶

Parameters

NameRequiredDescription
filenameoptionalSpecific PCAP filename to delete. If omitted, deletes all local captures.

Response

{
  "status": "success",
  "message": "Capture deleted",
  "storage": "local",
  "changed": true
}
Bulk deletion (no 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.
POST /banana/get_live_stats Live and post-capture summary: hosts, conversations, DNS/SNI, protocols, TCP health ▶

Parameters

NameRequiredDescription
filenamerequiredPCAP 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
}
Safe to poll once per second while a capture is running. 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"}.
POST /banana/port_mirror Mirror all traffic to the LOCAL port for external capture ▶

Parameters

NameRequiredDescription
actionrequiredstart, 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"
}
When running, the LOCAL port loses its IP and becomes a mirror-only tap. Blue LED indicates mirror mode. Connect a device running Wireshark to capture all test traffic in real time.
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

POST /banana/start_uplink_monitor Start continuous latency/loss monitoring ▶

Parameters

NameRequiredDescription
targetoptionalHost to ping (default: 8.8.8.8). Each distinct target runs its own monitor.
intervaloptionalPing 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).
POST /banana/stop_uplink_monitor Stop a latency monitor ▶

Parameters

NameRequiredDescription
targetrequiredHost 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" }
GET /banana/get_uplink_data Retrieve latency/loss time-series data ▶

Parameters

NameRequiredDescription
targetrequiredHost being monitored. Returns Missing target if omitted.
timeframeoptionalTime window in seconds (default: 300). Ignored if start/end provided.
startoptionalCustom start time (Unix timestamp)
endoptionalCustom 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"}.
Data is aggregated into buckets of 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.
GET /banana/list_monitors List all active uplink monitors ▶

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.
POST /banana/bandwidth_monitor Start/stop/check bandwidth throughput monitor ▶

Parameters

NameRequiredDescription
actionrequiredstart, stop, or status
interfaceoptionalauto (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.
GET /banana/get_bandwidth_data Retrieve bandwidth time-series data ▶

Parameters

NameRequiredDescription
timeframeoptionalTime window in seconds (default: 300)
startoptionalCustom start time (Unix timestamp)
endoptionalCustom 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 }
}
Rates in bits per second. Totals in bytes; 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

POST /banana/network_tools Ping, DNS, traceroute, ARP scan, IP lookup, public IP ▶

Parameters

NameRequiredDescription
toolrequiredTool to run: ping, dns, rdns, traceroute, arp, arpscan, iplookup, publicip, update_oui
targetrequired*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.
countoptionalPing count, 1-20 (default: 5). Values outside the range are clamped
serveroptionalDNS 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.
POST /banana/run_speedtest Run a full speed test (about 15-20 seconds) ▶

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"
}
Blocking call: about 15-20 seconds, up to 90 seconds. A concurrent call is HTTP 400 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.
POST /banana/test_port Test TCP port connectivity ▶

Parameters

NameRequiredDescription
targetrequiredHostname or IP
portrequiredPort 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

POST /banana/configure_wifi Enable and configure the WiFi test AP ▶

Parameters (POST body, form-encoded)

NameRequiredDefaultDescription
ssidoptionalNetTest-ImpairedNetwork name (1-32 chars)
passwordoptionalWiFi password (8-63 chars, required unless encryption=none). Read only from the body; a query-string password is ignored
bandoptional2g2g or 5g
encryptionoptionalpsk2none, psk, psk2, psk-mixed, sae (WPA3), sae-mixed
channeloptionalautoChannel 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"
}
Devices connected via WiFi experience the same download impairments as wired devices.
POST /banana/stop_wifi Disable the WiFi test AP ▶

Parameters

None

Response

{ "status": "success" }
GET /banana/get_wifi_status Check WiFi AP status ▶

Parameters

None

Response

{
  "status": "success",
  "state": "running",
  "ssid": "Example-Test-AP",
  "band": "5g",
  "interface": "wifi-5g"
}
Returns {"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.
GET /banana/get_wifi_config Read the saved WiFi AP settings ▶

Parameters

None

Response

{
  "status": "success",
  "configured": true,
  "ssid": "Example-Test-AP",
  "band": "5g",
  "channel": "auto",
  "encryption": "psk2",
  "password": "example-password"
}
Returns the saved settings whether or not the AP is running. 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}.
GET /banana/get_wifi_clients List connected WiFi clients ▶

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)

POST /banana/mount_smb Mount a network share for capture storage ▶

Parameters (POST body, form-encoded)

NameRequiredDescription
hostrequiredSMB server hostname or IP
sharerequiredShare name
useroptionalUsername for auth
passwordoptionalPassword for auth. Read only from the body; a query-string password is ignored
domainoptionalDomain for auth
pathoptionalSubfolder 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"
}
The SMB version is negotiated automatically. The share is reached over the MGMT port. Guest access supported. 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.
POST /banana/unmount_smb Disconnect network share ▶

Parameters

None

Response

{ "status": "success", "message": "Disconnected", "changed": true }
When nothing is mounted the response is "message": "Not mounted" with changed false.
Will refuse to unmount if a capture is currently running.
POST /banana/mount_nfs Mount an NFS export for capture storage ▶

Parameters (POST body or query)

NameRequiredDescription
hostrequiredNFS server hostname or IP
exportrequiredNFS export path (for example /srv/notbox-pcaps)
pathoptionalSubfolder within the mounted export
versionoptionalauto (default), 4.2, 4.1, 4.0, or 3
optionsoptionalAdditional 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"
}
The export is reached over the MGMT port. In 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.
POST /banana/unmount_nfs Disconnect NFS network share ▶

Parameters

None

Response

{ "status": "success", "message": "Disconnected", "changed": true }
When nothing is mounted the response is "message": "Not mounted" with changed false.
Will refuse to unmount if a capture is currently running.
GET /banana/check_smb_mount Check network share status ▶

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"
}
Returns {"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.
GET /banana/check_nfs_mount Check NFS network share status ▶

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"
}
Returns {"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

GET /banana/get_system_info CPU, memory, disk, temperature, uptime, throughput ▶

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.
GET /banana/get_serial Get the Serial that matches the sticker on your device ▶

Parameters

None

Response

{
  "status": "success",
  "serial": "NB20260100042"
}
This is the value to use when contacting support or recording inventory.
GET /banana/get_network_config Get management network configuration ▶

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.
POST /banana/set_network_config Change management IP, hostname, DNS ▶

Parameters

NameRequiredDescription
hostnameoptionalDevice hostname (1-63 chars)
ip_moderequireddhcp or static
ip_addrif staticStatic IP address
netmaskoptionalSubnet mask (default: 255.255.255.0)
gatewayoptionalGateway IP
dnsoptionalDNS server IP

Response

{
  "status": "success",
  "message": "Settings saved. Network will restart.",
  "new_ip": "192.168.1.100",
  "dhcp": false,
  "needs_reload": true
}
For 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.
The network restarts. If you change the IP, reconnect to the new address.
GET /banana/get_mgmt_mode Check connectivity mode and available features ▶

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.
GET /banana/check_network_interfaces List all network interfaces and connectivity ▶

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

GET /banana/check_auth Check if authentication is required/active ▶

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.
POST /banana/login Authenticate and get a session cookie ▶

Parameters (POST body)

NameRequiredDescription
passwordrequiredDevice password

Example

curl -c cookies.txt -d "password=example-password" "http://192.168.1.100/banana/login"
{
  "status": "success",
  "message": "Authenticated successfully"
}
Use -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.
POST /banana/logout End the current session ▶

Parameters

None

Response

{ "status": "success", "message": "Logged out" }
POST /banana/set_password Enable, disable, or change device password ▶

Parameters

NameRequiredDescription
actionrequiredQuery param: enable, disable, or change
new_passwordfor enable/changeNew password (min 8 characters, POST body)
current_passwordfor disable/changeCurrent 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" }
Enable, change, and disable all invalidate existing browser sessions; enable and change set a fresh session cookie on the calling browser. The API token is not affected.
GET /banana/get_password_status Check if password protection is enabled ▶

Parameters

None

Response

{ "status": "success", "password_set": false }
POST /banana/generate_api_token Generate an API access token for scripts ▶

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"
}
Only one token exists at a time; generating a new one replaces it. The token survives password changes and disabling.
GET /banana/get_api_token Retrieve the current API token ▶

Parameters

None (requires a session cookie or a Bearer token)

Response

{
  "status": "success",
  "token_set": true,
  "token": "0123456789abcdef0123456789abcdef"
}
When no token exists the response is {"status": "success", "token_set": false} with no token field.
POST /banana/revoke_api_token Revoke the current API token ▶

Parameters

None (requires a session cookie or a Bearer token)

Response

{ "status": "success", "message": "API token revoked" }
Any scripts using the revoked token will immediately lose access. After a revoke there is no token until generate_api_token (needs a password) or the next reboot.

Firmware Updates

POST /banana/check_update Check if a firmware update is available ▶

Parameters

NameRequiredDescription
channeloptionalstable (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_versionpinned onlyExact version to check (for example 1.1.0)
actionoptionalcheck (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.
For action=list_versions the response carries hardware_model and versions[] with version, filename, size (bytes), release_notes, published_at (Unix seconds), sha256, and signature.
POST /banana/apply_update Download, apply, check status, or cancel firmware updates ▶

Parameters

NameRequiredDescription
actionrequireddownload, apply, status, or cancel
versionrequired*Version to download (e.g. 1.1.0). Required when action=download.
channelfor downloadstable (default when omitted), beta, or pinned. Must match the channel used for check_update.
sha256for downloadExpected SHA-256 checksum (64 lowercase hex chars). Required for download.
signaturefor downloadEd25519 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.

GET /banana/get_device_id Device identity (public, no token needed) ▶

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.
GET /banana/get_control Which screen owns configuration (public, no token needed) ▶

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.
POST /banana/take_local_control Make the local page the owner of configuration ▶

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"
}
Persists across reboots until resume_cloud_control is called. The reply carries changed (false when local control was already taken) and previous_owner.
POST /banana/resume_cloud_control Hand configuration back to HENcloud ▶

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.
GET /banana/get_cloud_status HENcloud link state ▶

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.
POST /banana/set_cloud_claim Link this device to a HENcloud organization with a claim code ▶

Parameters

NameRequiredDescription
coderequiredClaim 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..."
}
Watch get_cloud_status for state to become connected.
POST /banana/disconnect_cloud Remove the HENcloud link and return to local-only ▶

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."
}
Fully reversible: claim the device again with a new code.

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