Unofficial Node.js client and documentation for the TP-Link Omada Controller Web API v2.
The Omada Controller has a powerful internal API that drives its web UI — but TP-Link doesn't document it. This toolkit provides a zero-dependency Node.js client and comprehensive endpoint documentation, all reverse-engineered from browser DevTools and real-world usage.
Tested on OC220 hardware controller (firmware 5.x / 6.1.x). Software controller and other hardware versions not yet verified — PRs welcome!
If you've tried to automate your Omada setup, you've probably hit these walls:
- The official OpenAPI v1 doesn't cover VLANs, SSIDs, firewall rules, mDNS, or port profiles
- The internal Web API v2 is completely undocumented
- The auth flow uses a quirky triple-auth mechanism (Controller ID + CSRF token + session cookie)
- Payload structures are not guessable — one wrong field and you get cryptic errors
This toolkit gives you everything you need to automate your Omada Controller programmatically.
git clone https://github.com/spectator81-png/omada-api-toolkit.git
cd omada-api-toolkit
# Set your controller credentials
export OMADA_URL="https://192.168.x.x" # Your controller IP
export OMADA_USER="admin"
export OMADA_PASS="your-password"
# Disable SSL verification (self-signed cert)
export NODE_TLS_REJECT_UNAUTHORIZED=0
# Run the demo (lists devices, networks, ACLs)
node omada-api-helper.jsNo npm install needed — zero external dependencies.
The Omada Web API v2 uses a 3-step authentication:
Step 1: GET /api/info
→ Returns controllerId (omadacId)
Step 2: POST /{controllerId}/api/v2/login
Body: { "username": "admin", "password": "..." }
→ Returns CSRF token
→ Sets TPOMADA_SESSIONID cookie
Step 3: GET /{controllerId}/api/v2/sites?token={token}
→ Returns siteId
All subsequent requests require:
- Header: Csrf-Token: {token}
- Cookie: TPOMADA_SESSIONID={value}
- URL param: ?token={token}
The helper handles all of this automatically:
const omada = require('./omada-api-helper.js');
await omada.connect();
// Now make any API call
const networks = await omada.apiCall('GET', '/setting/lan/networks?currentPage=1¤tPageSize=100');
console.log(networks.result.data);| File | Description |
|---|---|
omada-api-helper.js |
Zero-dependency Node.js API client with auth flow, cookie jar, and helper methods |
API-REFERENCE.md |
Complete endpoint documentation with exact payloads for ACLs, VLANs, SSIDs, mDNS, switch ports, port profiles, AP radio/channel config |
PITFALLS.md |
20 common mistakes and undocumented behavior that will save you hours |
examples/ |
Ready-to-use scripts for common tasks |
const omada = require('./omada-api-helper.js');
await omada.connect();
const networks = await omada.getNetworks();
networks.result.data.forEach(n => {
console.log(`${n.name} — VLAN ${n.vlanId}, ${n.subnet}/${n.cidr}`);
});await omada.apiCall('POST', '/setting/firewall/acls', {
name: 'Allow-HTTP-Trusted-to-IoT',
status: true,
policy: 1, // 1 = Permit, 0 = Deny
protocols: [6], // 6 = TCP
sourceType: 0, // 0 = Network
sourceIds: ['YOUR_TRUSTED_NETWORK_ID'],
destinationType: 0,
destinationIds: ['YOUR_IOT_NETWORK_ID'],
direction: {
lanToWan: false,
lanToLan: true, // LAN-to-LAN rule
wanInIds: [],
vpnInIds: [],
},
type: 0, // 0 = Gateway ACL
biDirectional: false,
stateMode: 0, // 0 = Auto (stateful)
ipSec: 0,
syslog: false,
customAclDevices: [],
customAclOsws: [],
customAclStacks: [],
});// First, get the WLAN group ID
const wlanGroups = await omada.getWlanGroups();
const wlanGroupId = wlanGroups.result.data[0].id;
// Get rate limit ID from existing config (needed for creation)
const existingSsids = await omada.getSsids(wlanGroupId);
const rateLimitId = existingSsids.result.data[0]?.rateLimit?.rateLimitId;
// Create SSID
await omada.createSsid(wlanGroupId, {
name: 'MyNetwork',
band: 3, // 2.4 + 5 GHz
type: 0,
guestNetEnable: false,
security: 3, // WPA2/WPA3 (do NOT use 2!)
broadcast: true,
vlanSetting: { mode: 1, customConfig: { vlanId: 10 } },
pskSetting: {
securityKey: 'your-wifi-password',
encryptionPsk: 3, versionPsk: 2, gikRekeyPskEnable: false
},
rateLimit: { rateLimitId },
ssidRateLimit: { rateLimitId },
wlanScheduleEnable: false,
macFilterEnable: false,
rateAndBeaconCtrl: { rate2gCtrlEnable: false, rate5gCtrlEnable: false, rate6gCtrlEnable: false },
wlanId: '', enable11r: false, pmfMode: 3,
multiCastSetting: { multiCastEnable: true, arpCastEnable: true, filterEnable: false, ipv6CastEnable: true, channelUtil: 100 },
wpaPsk: [2, 3], deviceType: 1,
dhcpOption82: { dhcpEnable: false },
greEnable: false, prohibitWifiShare: false, mloEnable: false
});// IMPORTANT: PATCH /eaps/{mac} silently ignores ssidOverrides!
// Must use PUT /eaps/{mac}/config/wlans instead.
// Get AP config and WLAN group ID
const ap = await omada.getEap('AA-BB-CC-DD-EE-FF');
const wlanGroups = await omada.getWlanGroups();
const wlanGroupId = wlanGroups.result.data[0].id;
// Disable "GuestNetwork" on this AP
const overrides = ap.result.ssidOverrides.map(o => ({
...o,
ssidEnable: o.globalSsid === 'GuestNetwork' ? false : o.ssidEnable,
// enable must stay false! (true causes "SSID name already exists" error)
}));
await omada.setEapSsidOverrides('AA-BB-CC-DD-EE-FF', wlanGroupId, overrides);// Channel is set via freq (MHz), NOT the channel field!
// 2.4G: Ch1=2412, Ch6=2437, Ch11=2462
// 5G: Ch36=5180, Ch52=5260, Ch100=5500, Ch132=5660
await omada.setEapChannel('AA-BB-CC-DD-EE-FF', 2437, 5260); // Ch6 + Ch52
// Set TX power and minimum RSSI for roaming
const ap = await omada.getEap('AA-BB-CC-DD-EE-FF');
await omada.updateEap('AA-BB-CC-DD-EE-FF', {
radioSetting2g: { ...ap.result.radioSetting2g, txPower: 14 }, // Medium
radioSetting5g: { ...ap.result.radioSetting5g, txPower: 28 }, // High
rssiSetting2g: { rssiEnable: true, threshold: -75 },
rssiSetting5g: { rssiEnable: true, threshold: -75 },
});const mdns = await omada.apiCall('GET', '/setting/service/mdns');
mdns.result.data.forEach(rule => {
console.log(`${rule.name} — active: ${rule.status}`);
console.log(` Service networks: ${rule.osg.serviceNetworks}`);
console.log(` Client networks: ${rule.osg.clientNetworks}`);
});protocols: []is unreliable — Always set explicit protocols like[6, 17, 1]for TCP+UDP+ICMP- PATCH needs the full payload — GET first, modify, then PATCH with everything
security: 2(WPA2-only) fails on SSID creation — Usesecurity: 3(WPA2/WPA3) instead- Trunk profiles without native VLAN can't be assigned to ports — Always include
nativeNetworkId - SSID overrides need
PUT /eaps/{mac}/config/wlans—PATCH /eaps/{mac}silently ignoresssidOverrides
See PITFALLS.md for all 20 pitfalls with explanations.
The API is not fully documented by TP-Link. The best way to find new endpoints:
- Open the Omada Controller web UI in your browser
- Open DevTools (F12) → Network tab → filter by XHR
- Perform the action you want to automate in the UI
- Copy the request URL and JSON body from the Network tab
- Use
omada.apiCall()with the same method, path, and body
- OC220 Hardware Controller (firmware 6.1.0.19)
- ER707-M2 Router/Firewall
- SG3428XMPP / SG2210XMP-M2 / TL-SG2210P Managed Switches
- EAP650 / EAP650-Outdoor Access Points
The API structure should be similar across Omada Controller versions 5.x and 6.x, but field names may vary between versions. Software controller (Windows/Linux) may use a different default port (8043 instead of 443). Not yet verified — PRs welcome!
Florian Keusch — Web Development & Digital Strategy
MIT