# Fleet Inventory Export

**Level:** 30 minutes · the report everyone asks you for

**APIs used:** GET /api/v2/routers/ (expand=group,last_known_location), GET /api/v2/products/, GET /api/v2/firmwares/

---
Read https://cradlepoint.ericsson.com/llms.txt and the OpenAPI document at
https://cradlepoint.ericsson.com/developer/api/openapi.json so you know the
Ericsson NetCloud Manager (NCM) API.

Build me a Python script (standard library only) that exports my full NCM
router inventory to a CSV file whose path I pass as an argument.

Use exactly these CSV columns, in this order:
name, id, mac, serial_number, model, firmware_version, group, state, latitude,
longitude, site_info
(group is the group's name, taken from the expanded group.)

Requirements:
- Start from GET https://www.cradlepointecm.com/api/v2/routers/ with
  expand=group,last_known_location. Those two, plus account, are the only
  relations /routers/ can expand. The product and firmware relations are NOT
  expandable; expand=product or expand=actual_firmware returns HTTP 400.
- For model and firmware, collect the distinct product and actual_firmware IDs
  from the router URLs. Then fetch each set once with
  GET /api/v2/products/?id__in=<comma-separated IDs>&limit=500 and
  GET /api/v2/firmwares/?id__in=<comma-separated IDs>&limit=500, in chunks of
  100 IDs, following meta.next on those too. Join the results client-side.
  Don't make one request per router. A failed product or firmware lookup is
  not fatal: print a warning and use the fallbacks below. Only errors on the
  /routers/ calls stop the script.
  - model = the product's name. If the product lookup fails, use the router's
    full_product_name, unless it is "unknown".
  - firmware_version = the firmware's version. Leave it empty if
    actual_firmware is null (common for routers that have never connected) or
    if the firmware lookup fails.
- NCM stores coordinates, not street addresses. Take latitude/longitude from
  the expanded last_known_location, which is null when a router has never
  reported a location. For site_info, join the router's description, custom1
  and custom2 fields with " | ", skipping empty ones. That's where teams
  usually record a site address.
- v2 paths keep the trailing slash. Auth is four headers (X-CP-API-ID,
  X-CP-API-KEY, X-ECM-API-ID, X-ECM-API-KEY) read from the environment
  variables X_CP_API_ID, X_CP_API_KEY, X_ECM_API_ID and X_ECM_API_KEY. Send a
  descriptive User-Agent header.
- Page with limit=500 by following meta.next exactly as returned until it is
  null. It may point at a regional host such as www.us0.cradlepointecm.com.
  Send the same headers there, but only ever send credentials to https:// URLs
  on cradlepointecm.com or its subdomains.
- Don't follow redirects automatically; treat any 3xx response as an error (a
  301 usually means a missing trailing slash). HTTP clients re-send custom
  headers to wherever a redirect points, which would leak your keys.
- On HTTP 429, wait until X-RateLimit-Reset (a Unix epoch) if that header is
  present, otherwise back off exponentially (1s, 2s, 4s, 8s). Make at most
  5 attempts per request in total, and never wait more than 60s at a time.
  On any other HTTP error, stop with a clear message. Use a 30s request timeout.
- Write the CSV with a header row. Leave cells empty, rather than writing
  "None" or "null", when data is missing, so the file opens cleanly in Excel.

Then tell me how to run it, including which environment variables to set.
