Scripting the Aruba Instant On API
If you run Aruba (now HPE Networking) Instant On gear at home, you've probably assigned friendly names to your clients in the dashboard. Those names live in Instant On's cloud, not on your LAN — no local API, no SNMP, no config export. But the web portal is backed by a REST API that the community reverse-engineered years ago, and it still works in 2026 with a couple of adjustments. Here's the current state of it, plus a small Python script that dumps a MAC → name table.
The obligatory caveat: HPE's official position is that this API doesn't exist. It's undocumented and unsupported; it can change or disappear at any time. Treat everything below as a snapshot from July 2026.
What's changed since the original writeups
The reverse-engineering groundwork was done around 2022 by Luke Whitelock (PowerShell, in the HuduAutomation repo) with a Python token-flow gist by crockk and a Postman collection floating around. Following those docs verbatim no longer works. What I found:
- HPE rebrand moved the portal.
portal.arubainstanton.comnow answers with a308 Permanent Redirecttoportal.instant-on.hpe.com. The SSO host (sso.arubainstanton.com) is unchanged — for now. Rather than hardcoding hosts, bootstrap fromhttps://portal.instant-on.hpe.com/settings.json, which self-describes everything you need:ssoFqdn(SSO host),ssoClientIdAuthZ(OAuth client id),ssoRedirectUrl(redirect URI), andrestApiUrl(API base). wiredClientSummaryis gone. The old docs list separateclientSummary(wireless) andwiredClientSummary(wired) endpoints. The wired one now returns 404. Instead,clientSummaryreturns all clients, wired and wireless; each record carries bothwired*andwireless*fields, and you can classify a client by whetherwirelessNetworkIdis set.x-ion-api-version: 7still works. The API version header from the 2022 writeups is still accepted.
Authentication flow
It's OAuth2 authorization-code with PKCE, with one quirk: instead of a browser login, you POST credentials to an MFA-validation endpoint and pass the resulting session token into the authorization request.
GET https://portal.instant-on.hpe.com/settings.json→ grabssoFqdn,ssoClientIdAuthZ,ssoRedirectUrl,restApiUrl.- Generate a PKCE pair: random
code_verifier, andcode_challenge = base64url(sha256(code_verifier))without padding. POST {ssoFqdn}/aio/api/v1/mfa/validate/fullwith form fieldsusernameandpassword→ JSON response contains anaccess_token(a session token, not the bearer token yet).GET {ssoFqdn}/as/authorization.oauth2with query paramsclient_id,redirect_uri,response_type=code,scope=profile openid, a randomstate,code_challenge_method=S256,code_challenge, andsessionToken=<token from step 3>. Don't follow the redirect — the authorizationcodeis in theLocationheader's query string.POST {ssoFqdn}/as/token.oauth2with form fieldsclient_id,redirect_uri,code,code_verifier,grant_type=authorization_code→ JSON response contains the real beareraccess_token.
Because step 3 is a straight username/password POST, the account can't have MFA enabled. Don't strip MFA from your main account — create a dedicated API account (any email alias works), leave MFA off, and invite it to your site from your primary account. A view-only role is enough for all the read endpoints below (verified), and limits the damage if the no-MFA account is ever compromised.
The API
All requests: GET {restApiUrl}/api/... with headers Authorization: Bearer <token> and x-ion-api-version: 7. Collection responses wrap their list in an elements key.
| Endpoint | Returns |
|---|---|
/api/sites/ |
Your sites and their ids |
/api/sites/{site_id}/clientSummary |
All clients, wired + wireless |
/api/sites/{site_id}/inventory |
Your Instant On devices (APs, switches) |
/api/sites/{site_id}/alerts |
Active and resolved alerts |
/api/sites/{site_id}/networksSummary |
WiFi network configuration |
The clientSummary records are rich. Fields of note: name (the friendly name you assigned in the dashboard — this is the prize), macAddress, ipAddress, hostName, detectedOs, signalInDbm, wirelessNetworkName, vlanId, traffic counters, and health metrics. One gotcha: each record has a kind field, but it's the resource type ("clientSummary"), not wired/wireless — use the presence of wirelessNetworkId for that. In my testing, wired clients came back with an empty ipAddress.
The script
Python, single dependency (requests) — the PKCE bits are all stdlib. It reads INSTANT_ON_USER and INSTANT_ON_PASSWORD from the environment (or a .env file in the working directory) and prints an aligned table, or JSON with --json.
#!/usr/bin/env python3
# Dump MAC -> friendly-name pairings from the unofficial Aruba Instant On cloud API.
# The API is undocumented and unsupported by HPE; endpoints reverse-engineered by the
# community (https://mspp.io/documenting-aruba-instant-on-sites-aruba-instant-on-api/).
# Requires a portal account WITHOUT MFA (the scripted login can't answer a challenge).
import argparse
import base64
import hashlib
import json
import os
import secrets
import sys
from urllib.parse import parse_qs, urlparse
import requests
# portal.arubainstanton.com now 308-redirects here (HPE rebrand); settings.json
# self-describes the SSO host, OAuth client id, redirect URI, and API base
PORTAL_BASE = 'https://portal.instant-on.hpe.com'
ION_API_VERSION = '7'
# Load .env file
try:
with open('.env', 'r') as f:
for line in f:
if line.strip() and not line.startswith('#') and '=' in line:
key, value = line.strip().split('=', 1)
os.environ[key] = value
except FileNotFoundError:
pass
def get_bearer_token(settings, username, password):
sso_base = settings['ssoFqdn']
redirect_uri = settings['ssoRedirectUrl']
# PKCE: challenge is base64url(sha256(verifier)) without padding
code_verifier = secrets.token_urlsafe(48)
code_challenge = base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest()).rstrip(b'=').decode()
r = requests.post(f'{sso_base}/aio/api/v1/mfa/validate/full', data={'username': username, 'password': password})
if r.status_code != 200:
sys.exit(f'login failed ({r.status_code}): check credentials, and that the account has MFA disabled')
session_token = r.json()['access_token']
# the authorization code comes back in the redirect Location, so don't follow it
r = requests.get(f'{sso_base}/as/authorization.oauth2',
params={
'client_id': settings['ssoClientIdAuthZ'],
'redirect_uri': redirect_uri,
'response_type': 'code',
'scope': 'profile openid',
'state': secrets.token_urlsafe(32),
'code_challenge_method': 'S256',
'code_challenge': code_challenge,
'sessionToken': session_token,
},
allow_redirects=False)
location = r.headers.get('Location', '')
query = parse_qs(urlparse(location).query)
if 'code' not in query:
sys.exit(f'authorization failed: no code in redirect ({location or r.status_code})')
r = requests.post(f'{sso_base}/as/token.oauth2',
data={
'client_id': settings['ssoClientIdAuthZ'],
'redirect_uri': redirect_uri,
'code': query['code'][0],
'code_verifier': code_verifier,
'grant_type': 'authorization_code',
})
r.raise_for_status()
return r.json()['access_token']
def api_get(settings, token, path):
r = requests.get(f'{settings["restApiUrl"]}/api/{path}',
headers={
'Authorization': f'Bearer {token}',
'x-ion-api-version': ION_API_VERSION
})
r.raise_for_status()
data = r.json()
# collection responses wrap the list in "elements"
return data.get('elements', data) if isinstance(data, dict) else data
def first_key(record, *candidates):
# field names vary slightly across API versions, so probe a few
for key in candidates:
if record.get(key):
return record[key]
return ''
def collect_clients(settings, token, site_id, site_name):
# clientSummary covers wired and wireless alike (the old wiredClientSummary endpoint is gone)
rows = []
for client in api_get(settings, token, f'sites/{site_id}/clientSummary'):
rows.append({
'site': site_name,
'name': first_key(client, 'name', 'hostName'),
'mac': first_key(client, 'macAddress', 'mac').lower(),
'ip': first_key(client, 'ipAddress', 'ip'),
'kind': 'wireless' if client.get('wirelessNetworkId') else 'wired',
})
return rows
def main():
parser = argparse.ArgumentParser(description='List MAC -> friendly-name pairings from Aruba Instant On')
parser.add_argument('--json', action='store_true', help='output JSON instead of a table')
args = parser.parse_args()
try:
username = os.environ['INSTANT_ON_USER']
password = os.environ['INSTANT_ON_PASSWORD']
except KeyError as e:
sys.exit(f'missing {e.args[0]} — add it to .env or the environment')
settings = requests.get(f'{PORTAL_BASE}/settings.json').json()
token = get_bearer_token(settings, username, password)
rows = []
for site in api_get(settings, token, 'sites/'):
rows.extend(collect_clients(settings, token, site['id'], first_key(site, 'name', 'siteName')))
rows.sort(key=lambda r: (r['site'], r['name'].lower()))
if args.json:
print(json.dumps(rows, indent=2))
return
widths = {col: max([len(col)] + [len(r[col]) for r in rows]) for col in ('mac', 'name', 'ip', 'kind', 'site')}
for r in [{col: col.upper() for col in widths}] + rows:
print(' '.join(r[col].ljust(widths[col]) for col in ('mac', 'name', 'ip', 'kind', 'site')))
if __name__ == '__main__':
main()
Output looks like:
MAC NAME IP KIND SITE
aa:bb:cc:dd:ee:01 apple-tv 192.168.1.20 wireless Home
aa:bb:cc:dd:ee:02 thermostat 192.168.1.31 wireless Home
aa:bb:cc:dd:ee:03 nas 192.168.1.10 wired Home
Setup recap
- Create a fresh Instant On account at portal.instant-on.hpe.com with an email alias, verify it, and leave MFA off.
- From your main account, invite it to your site with a view-only role; accept the invite.
- Export
INSTANT_ON_USERandINSTANT_ON_PASSWORD(or drop them in a.envnext to the script) and run it.
The credentials are still a real portal login for your network, so treat them like any other secret: keep them out of your repo history and public paste destinations.
Closing thoughts
This is exactly the kind of data that's useful to join against DHCP leases, DNS logs, or an nmap sweep — the dashboard names are the only place the "human" identity of a device lives, and now they're one JSON call away. Credit to Luke Whitelock and the others who did the original reverse-engineering; hopefully this post saves someone the hour of discovering what the rebrand broke.
Disclaimer: Claude Fable authored this post after testing the functionality against my Instant On account.