L2 Discovery (LLDP/CDP)
List L2 Neighbors by Device
GET /api/v1/l2-neighbors?device_id=<uuid>
Returns LLDP/CDP neighbor adjacencies discovered from a specific device's SNMP walks.
Auth: Any authenticated user.
Query parameters:
device_id (required) -- Device UUID
Response: 200 OK
[
{
"id": "uuid",
"local_device_id": "uuid",
"local_port_name": "Ethernet1",
"remote_chassis_id": "00:11:22:33:44:55",
"remote_port_id": "Gi0/1",
"remote_sys_name": "switch-1",
"remote_mgmt_ip": "10.1.1.1",
"remote_capabilities": 4,
"remote_platform": "Arista EOS",
"protocol": "lldp",
"first_seen": "2026-03-10T12:00:00Z",
"last_seen": "2026-03-11T08:00:00Z"
}
]
Error responses:
400 Bad Request -- Missing device_id parameter
List L2 Neighbors by Network (Paginated)
GET /api/v1/l2-neighbors/network/{networkID}?search=...&sort=...&sort_dir=...&show_hidden=true&limit=...&offset=...
Returns paginated, sorted, searchable L2 neighbor adjacencies within a network scope. JOINs with the device table for local device hostname resolution.
Auth: Any authenticated user.
Path parameters:
networkID (required) -- Network UUID
Optional query parameters:
search -- Substring filter (matches local hostname, local port name, remote sys name, remote chassis ID, remote port ID, remote mgmt IP)
sort -- Sort column (whitelisted: network, localDevice, localPort, remoteDevice, remotePort, remoteMgmtIP, protocol, remotePlatform, remoteChassisID, firstSeen, lastSeen). Default: local device name. Ties break on row ID for stable pagination.
sort_dir -- asc or desc (default asc)
show_hidden -- true to include hidden entries (default: hidden entries excluded)
limit -- Page size (default 100, max 10000)
offset -- Pagination offset (default 0)
Response: 200 OK
{
"data": [
{
"id": "uuid",
"local_device_id": "uuid",
"local_hostname": "rtr-core-01",
"local_router_id": "10.0.0.1",
"local_port_name": "Ethernet1",
"remote_chassis_id": "00:11:22:33:44:55",
"remote_port_id": "Gi0/1",
"remote_sys_name": "switch-1",
"remote_mgmt_ip": "10.1.1.1",
"remote_capabilities": 4,
"remote_platform": "Arista EOS",
"protocol": "lldp",
"first_seen": "2026-03-10T12:00:00Z",
"last_seen": "2026-03-11T08:00:00Z"
}
],
"total": 248,
"limit": 100,
"offset": 0,
"summary": {
"total": 248,
"unique_remotes": 42,
"lldp_count": 180,
"cdp_count": 68,
"hidden_count": 3,
"networks": 1
}
}
Notes:
local_hostname and local_router_id are populated from the device table JOIN, not stored on the L2 neighbor record; network_name comes from the network table JOIN.
local_hostname is resolved via the display.device_name_mode system setting (hostname/router_id/dns).
- Summary stats reflect the search filter (except
hidden_count, which is always the total hidden count for the scoped networks). summary.networks counts the distinct networks in the filtered result set.
List L2 Neighbors by Scope (Paginated, multi-network)
GET /api/v1/l2-neighbors/scoped?area_ids=...&search=...&sort=...&sort_dir=...&show_hidden=true&limit=...&offset=...
The scope-parameterized variant of the per-network list: accepts any standard
topology scope and resolves every network it touches, so a selection
spanning several networks returns all of their L2 adjacencies in one sorted,
paginated set (the CDP/LLDP Neighbors report uses this).
Auth: Any authenticated user.
Scope parameters (exactly one required, same family as /routes):
area_ids -- Comma-separated area UUIDs
area_id -- Single area UUID
pi_id -- Protocol instance UUID (expands to its areas)
rd_id -- Routing domain UUID (expands recursively)
network_id -- Network UUID (short-circuits area resolution; must be a valid UUID or 400)
Optional query parameters: identical to the per-network endpoint above, plus
sort column network (network name).
Response: 200 OK — same envelope as the per-network endpoint. Every row
carries network_id and network_name so multi-network result sets stay
attributable; summary.networks > 1 signals the scope crosses network
boundaries. A scope that resolves to zero networks answers an empty page
(total: 0), never an error.
Error responses:
400 Bad Request -- No scope parameter, or malformed network_id
List L2 Discovered Neighbors
GET /api/v1/l2-discovered?network_id=<uuid>
Returns the L2 discovery crawl queue -- unmanaged switches/devices discovered via LLDP/CDP that are not yet monitored.
Auth: Any authenticated user.
Query parameters:
network_id (required) -- Network UUID
Response: 200 OK
[
{
"id": "uuid",
"chassis_id": "00:11:22:33:44:55",
"sys_name": "unmanaged-switch-1",
"mgmt_ip": "10.1.1.50",
"platform": "Cisco IOS",
"protocol": "cdp",
"reachable": false,
"crawl_depth": 1,
"first_seen": "2026-03-10T12:00:00Z",
"last_seen": "2026-03-11T08:00:00Z"
}
]
Error responses:
400 Bad Request -- Missing network_id parameter
Get L2 Topology (Global)
GET /api/v1/l2/topology
Returns L2 topology across all networks: every hierarchy device plus every L2-only device with at least one visible neighbor, and all non-hidden neighbor links regardless of network (rows with a NULL network scope included). Serves the global-view canvas overlay, where no single network is selected — the per-network endpoint cannot show cross-network uplinks (e.g. a DC gateway's LLDP links into the carrier network) in that view. No area scoping (that is a per-network concept).
Auth: Any authenticated user.
Response: Same shape as the per-network variant below.
Get L2 Topology
GET /api/v1/l2/topology/{networkID}
Returns L2 topology for a network: devices (switches + routers with L2 data) and neighbor links. Without area_ids, returns the full network L2 topology. With area_ids, scopes to devices in those areas and stops at ABR boundaries (devices with area memberships outside the selected set have their L2 neighbors excluded).
Auth: Any authenticated user.
Path parameters:
networkID (required) -- Network UUID
Optional query parameters:
area_ids -- Comma-separated area UUIDs (e.g. area_ids=uuid1,uuid2). Scopes L2 topology to devices in the specified areas, stopping at ABR boundaries. Phantom node IDs (l2-phantom-*) are silently ignored.
Response: 200 OK
{
"devices": [
{
"id": "uuid",
"hostname": "switch-1",
"device_type": "switch",
"network_id": "uuid"
}
],
"neighbors": [
{
"id": "uuid",
"local_device_id": "uuid",
"local_port_name": "Gi0/1",
"remote_device_id": "uuid",
"remote_port_id": "Gi0/2",
"remote_sys_name": "switch-2",
"protocol": "lldp",
"last_seen": "2026-03-13T10:00:00Z"
}
]
}
Error responses:
400 Bad Request -- Invalid network UUID
404 Not Found -- Network does not exist
Set L2 Neighbor Hidden
PUT /api/v1/l2-neighbors/{id}/hidden?network_id=...
Sets the hidden flag on an L2 neighbor entry, scoped to a specific network. Network scoping prevents cross-tenant modification of hidden flags. Hidden entries are excluded from the L2 canvas overlay but remain visible in the CDP/LLDP Neighbors report (with "Show hidden" toggle). The flag survives discovery cycles — re-polls preserve the hidden state.
Auth: Admin or engineer role required.
Path parameters:
id (required) -- L2 neighbor UUID
Query parameters:
network_id (required) -- Network UUID scope. The neighbor must belong to this network.
Request body:
{ "hidden": true }
Response: 200 OK
{ "hidden": true }
Error responses:
400 Bad Request -- Missing network_id parameter or invalid request body
Set Hidden by Chassis ID
PUT /api/v1/l2-neighbors/chassis/hidden?chassis_id=...&network_id=...
Bulk hide/unhide all L2 neighbor entries matching a chassis ID within a network. Useful for hiding all adjacencies from a specific device (e.g., an unmanaged AP) across all local ports that see it.
Auth: Admin or engineer role required.
Query parameters:
chassis_id (required) -- Remote chassis ID to match
network_id (required) -- Network UUID scope
Request body:
{ "hidden": true }
Response: 200 OK
{ "updated": 3 }
Error responses:
400 Bad Request -- Missing chassis_id or network_id parameter
Reset L2 Network Data
DELETE /api/v1/l2/network/{networkID}
Deletes all L2 neighbor and discovered neighbor data for a network. Audited action.
Auth: Admin or engineer role required.
Path parameters:
networkID (required) -- Network UUID
Response: 204 No Content
Reset Crawler Queue
DELETE /api/v1/l2/discovered?network_id=...
Clears the L2 discovery crawler queue for a specific network. Audited action.
Auth: Admin or engineer role required.
Query parameters:
network_id (required) -- Network UUID scope.
Response: 204 No Content
Error responses:
400 Bad Request -- Missing network_id parameter
POST /api/v1/l2/crawl
Triggers an immediate L2 crawl cycle. The crawl runs asynchronously; this endpoint returns immediately. With network_id, the trigger is scoped to the same configured network and candidate policy as preflight. Omitting it retains the legacy full enabled-network pass.
Auth: Admin or engineer role required.
Query parameters:
network_id -- Required with preflight=true; the network UUID whose actual crawler filters supply candidate_cap.
preflight=true -- Return a read-only capacity estimate with 200 OK; it takes no license advisory lock, updates no durable state, and publishes no crawl trigger.
Response: 202 Accepted
{
"message": "L2 crawl triggered"
}
Preflight response: 200 OK
{
"message": "L2 crawl preflight",
"current_nodes": 31,
"node_limit": 32,
"available_new_nodes": null,
"candidate_cap": 8,
"may_start_overage": true,
"would_reject_batch": false,
"would_partially_admit": false,
"prospective_overage_ends_at": "2026-09-30T12:00:00Z"
}
candidate_cap is the unique-chassis count after the crawler's resolved,
hidden, capability, depth and 24-hour failure-backoff filters, capped at the
same 500-candidate pass limit. It remains advisory because SNMP probing can
fail and topology can change before execution. Successful probes still enter a
single admission transaction. The UI requests confirmation whenever any of the
three outcome flags below is set — an overage that would start, a batch that
would be refused as a unit, or one that would only be partly admitted.
At most one of the three outcome flags is set, and which one follows the
entitlement. may_start_overage requires a grace-eligible entitlement — a
signed license that can absorb the crossing by opening a 30-day window, with the
prospective deadline returned alongside. Under a hard cap the crossing cannot be
absorbed, and what happens then differs: a licensed installation past its grace
refuses the batch as a unit (would_reject_batch), while an evaluation fills up
to the remaining allowance in canonical router-id order and refuses only the
excess (would_partially_admit, with admissible_candidates giving how many of
candidate_cap will actually be added). Reporting the licensed outcome for both
would tell an evaluation operator the opposite of what the crawl does.
Error responses:
400 Bad Request -- network_id is not a UUID
403 Forbidden -- Insufficient role (requires admin or engineer)
500 Internal Server Error -- License reconciliation, queue lookup or NATS publication failed
POST /api/v1/snmp/discover-now
Triggers an immediate interface-discovery sweep of every SNMP polling target (the same per-target pass the poller runs on its multi-hour discovery interval: IF-MIB walk, interface rows, OSPFv3 interface creation and link binding — the inputs of the v2/v3 dual-stack link merge). Runs asynchronously via NATS (osprey.snmp.discover_now with all: true); the endpoint returns immediately. The poller also schedules this sweep itself, debounced ~5 min after a topology-discovery trigger burst settles; this endpoint is the manual override (surfaced as Run discovery now on the Interface Discovery card in the network Enrichment panel).
Auth: Admin or engineer role required.
Response: 202 Accepted
{
"message": "interface discovery sweep triggered"
}
Error responses:
403 Forbidden -- Insufficient role (requires admin or engineer)