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:

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:

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:

Optional query parameters:

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:

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):

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:

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:

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:

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:

Optional query parameters:

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:

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:

Query parameters:

Request body:

{ "hidden": true }

Response: 200 OK

{ "hidden": true }

Error responses:


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:

Request body:

{ "hidden": true }

Response: 200 OK

{ "updated": 3 }

Error responses:


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:

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:

Response: 204 No Content

Error responses:


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:

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:


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: