# Ciena MCMS 6.2 REST API — Implementer's Field Guide

Reference for building an HTTP client against a Ciena MicroClimate
Management System (MCMS) 6.2 PON Manager. Distilled from the official
dev guide (`323-1961-306`) plus empirical findings against a working
deployment with GNXS / Tibit MicroPlug ONUs on Interos Everest 5.x
firmware.

Language-agnostic — every quirk listed here was hit in production. If
you skip the gotchas in §3, you will lose an afternoon to one of them.

---

## 1. Connection basics

- **Base URL.** The root of the PON Manager web UI (e.g.
  `https://mcms.example.net`). All REST paths are prefixed with `/api`.
- **Transport.** HTTPS. Internal deployments commonly use self-signed
  certificates on private IPs — your client needs an opt-in to skip
  TLS verification for that case (do **not** disable globally).
- **HTTP version.** HTTP/1.1 works. The server also negotiates HTTP/2
  but the dev-guide examples are 1.1 and the only behavioural
  difference observed is irrelevant to clients.
- **Path normalisation.** The dev guide writes paths as
  `/v1/users/authenticate/` — the real wire path is
  `/api/v1/users/authenticate/`. Normalise once in your client.
- **API versions.** Both `/api/v1/...` and `/api/v3/...` exist and serve
  different endpoints. **Use whichever the dev guide lists for that
  resource — they are not interchangeable.** See §4 endpoint table.
- **Trailing slashes.** Required. `/api/v1/onus/configs/<id>/` works;
  `/api/v1/onus/configs/<id>` does not (Django's `APPEND_SLASH` is
  on, but a few endpoints 500 instead of redirecting).

---

## 2. Login & session

MCMS uses Django session cookies + a CSRF token. Sessions do **not**
auto-expire on idle — log out explicitly when done.

### 2.1 Authenticate

```
POST /api/v1/users/authenticate/
Content-Type: application/json

{ "data": { "email": "user@example.net", "password": "..." } }
```

Response on success (`HTTP 200`):

```json
{ "status": "success" }
```

Set-Cookie headers carry the session. On observed MCMS 6.2 builds:

```
Set-Cookie: __Host-csrftoken=<token>; Path=/; Secure; SameSite=Lax
Set-Cookie: __Host-sessionid=<id>; Path=/; Secure; HttpOnly; SameSite=Lax
Set-Cookie: __Host-sessionexpire=<epoch>; Path=/; Secure; SameSite=Lax
```

→ **The `__Host-` prefix is significant.** Some MCMS builds (older
ones, mostly) use bare `csrftoken` / `sessionid`. Your cookie reader
should accept either prefix.

→ Some build variants accept an unwrapped body too:
`{"email": ..., "password": ...}` with no `data` envelope. If wrapped
returns HTTP 400, retry unwrapped.

### 2.2 Subsequent requests

Every authenticated request must send:

```
Cookie: __Host-csrftoken=<token>; __Host-sessionid=<id>; ...
X-CSRFToken: <token>     # the value from __Host-csrftoken
Referer: <baseURL>/      # MCMS rejects POST/PUT/DELETE without this
User-Agent: <anything>   # see §3.1 — must not be empty
Accept: application/json
```

For PUT/POST/PATCH also:

```
Content-Type: application/json
Content-Length: <bytes>
```

### 2.3 (Optional) Database selection

If the deployment manages multiple databases:

```
PUT /api/v1/databases/selection/
{ "data": "<database_id>" }
```

Most deployments only have one and skip this step.

### 2.4 Logout

```
GET /api/v1/users/logout/
```

Best-effort — don't fail your client if this errors.

---

## 3. The gotchas (read this section twice)

### 3.1 User-Agent header is REQUIRED

If you don't send a `User-Agent`, some MCMS middleware dereferences
`HTTP_USER_AGENT` without a null check and returns **HTTP 500** with
body:

```json
{"status": "fail", "details": {"message": "Internal server error. See server logs for details."}}
```

Reproduce: `curl -A '' https://mcms/api/v3/onus/configs/`. Any non-empty
string works (`PonFleetClient/1.0`, etc.).

**For iOS/Swift**: `URLSession` sets a default User-Agent automatically
(e.g. `<bundle>/1 CFNetwork/... Darwin/...`), so this is unlikely to
bite you there — but if you ever set a custom `URLRequest` with
`setValue(nil, forHTTPHeaderField: "User-Agent")`, the 500s will
return.

### 3.2 CSRF token field name

The token cookie is `__Host-csrftoken` on new builds, bare `csrftoken`
on older. The header you send back is always `X-CSRFToken` (no
prefix, capital `T` in `Token`). Both Django case-fold the header
name, so casing is fine — but the body name is exact.

### 3.3 Pagination: use `next`, NOT `skip`

`/api/v3/onus/configs/?skip=N` has been observed returning HTTP 500.
The working approach uses the last document's `_id` as a cursor:

```
GET /api/v3/onus/configs/?limit=100
   → returns 100 docs, the last with _id = "X"
GET /api/v3/onus/configs/?limit=100&next=X
   → returns the next 100, EXCLUDING all _ids <= X
```

Loop until you get a short page (< limit) or an empty page. Most list
endpoints accept this pattern.

### 3.4 Page size 100 is the safe default

`?limit=1000` against `/api/v3/onus/configs/` on a ~1500-ONU fleet
times out at the Apache front-end before Django logs the request.
Full `ONU-CFG` documents are ~30 KB each, so 1000 × 30 KB = 30 MB,
which exceeds the proxy timeout. **100 per page keeps each response
~3 MB** and well under any default proxy budget.

This applies to all bulk list endpoints (configs, states, OLT states).

### 3.5 Request body envelope: `{"data": payload}`

PUT/POST/PATCH bodies are wrapped:

```json
{ "data": { "_id": "...", "ONU": { ... } } }
```

Response envelope is similar:

```json
{ "status": "success", "data": { ... } }
{ "status": "fail",    "details": { "message": "..." } }
{ "status": "warning", "data": { ... }, "details": { ... } }
```

`"warning"` is treated as success by MCMS (the write happened) but
indicates a schema mismatch you should log.

### 3.6 PUT is full-document replace, NOT patch

`PUT /api/v1/onus/configs/<id>/` replaces the entire `ONU-CFG`
document. You **must** GET the current doc, mutate fields, and PUT
the entire result. There is no PATCH equivalent that works reliably
on MCMS 6.2.

Forgetting this turns into "I PUT'd just the FW Bank fields and now
the ONU has no service config" — the dev guide is explicit but easy
to skim past.

### 3.7 Timestamp format

MCMS emits and accepts timestamps as:

```
"YYYY-MM-DD HH:MM:SS"           # second precision
"YYYY-MM-DD HH:MM:SS.ffffff"    # microsecond precision (for STATE.Time)
```

Always **UTC**, always with a **literal space** between date and time
(not ISO-8601 `T`). Used on:

- `ONU-STATE.Time`
- `OLT-STATE.Time`
- Alarm timestamps
- `AUTO-TASK-CFG.Task["Scheduled Start Time"]`
- `start-time` / `end-time` URL params on stats endpoints

**For Swift**: `ISO8601DateFormatter` will NOT parse this. Use a
plain `DateFormatter` with:

```swift
let fmt = DateFormatter()
fmt.timeZone = TimeZone(identifier: "UTC")
fmt.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSSSS"     // or "yyyy-MM-dd HH:mm:ss"
fmt.locale = Locale(identifier: "en_US_POSIX")
```

The `en_US_POSIX` locale matters — without it, devices set to a
24-hour-format locale will still parse fine but devices set to
12-hour-format locales will fail. POSIX locale forces parse rules.

### 3.8 Server-side query/projection filters are brittle

The dev guide documents `query`, `projection`, `sort`, `limit`,
`skip`, `next`, `distinct` URL parameters. In practice:

- **`query`** (Mongo-style filter) works for simple equality on
  bare field names. Anything with **spaces in field names**, quoted
  values with non-ASCII characters, or projection paths that don't
  exist on every document version is unreliable — you'll get either
  empty results or 400s.
- **`projection`** is *very* picky about paths. Better to fetch full
  docs and project client-side.

**Recommended approach**: load the full fleet (paginated by `next`)
and filter client-side. The data volume is fine for any device with
enough RAM to hold a few thousand 30 KB documents.

### 3.9 URL encoding

Use percent-encoding (`encodeURIComponent` equivalent). MCMS does NOT
accept `+` for spaces in URL parameters — use `%20`.

This matters for query params like `start-time` which carry literal
spaces in the timestamp.

### 3.10 ONU IDs

The `_id` field of an ONU document is:

- The **serial number** for XGS-PON / GPON ONUs (e.g. `GNXS05057fee`)
- The **MAC address** for 10G-EPON ONUs

Treat as opaque strings. They're URL-safe in observed deployments
but always `encodeURIComponent` to be defensive.

### 3.11 Registration status lives on OLT-STATE, not ONU-STATE

This is non-obvious. The 9 registration buckets live as arrays on the
OLT's state document:

```json
{
  "_id": "<OLT MAC>",
  "ONU States": {
    "Registered":          ["GNXS01", "GNXS02", ...],
    "Deregistered":        ["GNXS09", ...],
    "Dying Gasp":          [...],
    "Disabled":            [...],
    "Disallowed Admin":    [...],
    "Disallowed Error":    [...],
    "Disallowed Reg ID":   [...],
    "Unspecified":         [...],
    "Unprovisioned":       [...]
  }
}
```

To resolve "what's the status of ONU X?", you walk every OLT-STATE
doc, find which bucket contains X, and that's your answer. See
Procedure 2 in the dev guide.

An ONU should appear in exactly one bucket on exactly one OLT. In the
rare duplicate case, prefer the non-`Registered` bucket — that's the
interesting signal.

### 3.12 FEC counters live on ONU-STATE.STATS, not on `/onus/stats/`

`GET /api/v1/onus/stats/<id>/?start-time=...` returns historical PM
samples (time-series). For the *current* FEC counters and optical
levels, read the live `ONU-STATE` document:

```
GET /api/v3/onus/states/<id>/
→ data.STATS["ONU-PON"]["RX Pre-FEC BER"]
→ data.STATS["ONU-PON"]["RX Post-FEC BER"]
→ data.STATS["ONU-PON"]["RX Optical Level"]      (dBm)
→ data.STATS["ONU-PON"]["TX Optical Level"]      (dBm)
→ data.STATS["OLT-PON"]["RX Pre-FEC BER"]
→ data.STATS["OLT-PON"]["RX Post-FEC BER"]
```

The MCMS Web UI's `/api/onu/summary` helper returns the same data
under `data.state_collection.STATS.*` (wrapper shape).

### 3.13 Bulk endpoints don't return everything in one shot

Even on a small fleet, MCMS may silently truncate large list
responses. Always paginate — don't trust `?limit=large_number` to be
authoritative.

### 3.14 Verbose error logging matters

When MCMS 500s, the response body sometimes contains useful detail
(missing required param, schema validation failure, etc.). Always log
both the response headers and the FULL body on non-2xx during
development — most "mysterious 500" cases resolve to a one-line hint
in the body.

---

## 4. Endpoints used by the fleet-upgrade app

| Method | Path | Purpose |
|---|---|---|
| `POST` | `/api/v1/users/authenticate/` | Login |
| `GET` | `/api/v1/users/logout/` | Logout |
| `PUT` | `/api/v1/databases/selection/` | (Optional) DB selection |
| `GET` | `/api/v3/onus/configs/` | List ONU configs (paginated) |
| `GET` | `/api/v1/onus/configs/<id>/` | Get single ONU config |
| `PUT` | `/api/v1/onus/configs/<id>/` | Replace ONU config (full doc) |
| `DELETE` | `/api/v1/onus/configs/<id>/` | Delete ONU (Procedure 43) |
| `GET` | `/api/v3/onus/states/` | List ONU states (paginated) |
| `GET` | `/api/v3/onus/states/<id>/` | Get single ONU state |
| `DELETE` | `/api/v1/onus/states/<id>/` | Delete ONU state (best-effort) |
| `GET` | `/api/v3/olts/states/` | List OLT states (paginated; registration) |
| `GET` | `/api/v1/files/onu-firmware/` | List uploaded firmware files |
| `POST` | `/api/v1/files/onu-firmware/<name>/` | Upload firmware |
| `PUT` | `/api/v3/tasks/configs/<id>/` | Create bulk upgrade task (Procedure 8) |
| `GET` | `/api/v3/tasks/configs/<id>/` | Poll bulk task config |
| `GET` | `/api/v1/onus/<id>/upgrade/status/` | (Build-dependent) per-ONU status |

### Other endpoints documented but not (yet) used

| Method | Path | Notes |
|---|---|---|
| `GET` | `/api/v1/onus/stats/<id>/?start-time=...` | Time-series ONU PM data |
| `GET` | `/api/v1/olts/stats/<id>/?start-time=...` | OLT PM data |
| `GET` | `/api/v1/onus/logs/<id>/?start-time=...` | Per-ONU syslog |
| `GET` | `/api/v1/olts/logs/<id>/?start-time=...` | Per-OLT syslog |
| `GET` | `/api/v3/tasks/states/<id>/` | Bulk task progress (good follow-up) |
| `GET` | `/api/v1/onus/automation/states/` | PON automation state |
| `GET` | `/api/v1/onus/cpe-states/` | ONU CPE states (downstream devices) |

---

## 5. Data shapes (observed)

These are not formally specified in the dev guide for MCMS 6.2 — they
were extracted from live traffic.

### 5.1 ONU-CFG

```jsonc
{
  "_id": "GNXS05057fee",
  "ONU": {
    "Name": "Operator-set label",
    "Address": "Street, city",
    "PON Mode": "GPON",                // GPON | XGS-PON | 10G-EPON
    "Equipment ID": "FT-XGS2110",

    "FW Bank Files":    ["...bin", "...bin"],   // [slot 0, slot 1]
    "FW Bank Versions": ["EV05110R", "EV05100R"],
    "FW Bank Ptr": 0,                  // 0 | 1 | 65535 (unset)

    "ONU-ALARM-CFG": "Default",
    "Vendor": "GNXS",
    "Serial Number": "GNXS05057fee",
    "Host MAC Address": "58:00:32:05:7f:ee",
    // ...many more fields - preserve all of them when PUTing
  },
  "OLT-Service 0": { ... },            // service config slots
  "OLT-Service 1": { ... },
  // ...
}
```

**Critical**: when mutating for an upgrade, only change `ONU["FW Bank
Files"]`, `ONU["FW Bank Versions"]`, and `ONU["FW Bank Ptr"]`. Pass
through everything else unchanged.

### 5.2 ONU-STATE

```jsonc
{
  "_id": "GNXS05057fee",
  "Time": "2026-05-04 21:51:57.394127",     // UTC, last update
  "ONU": {
    "Equipment ID": "FT-XGS2110",
    "FW Bank Files":    [...],
    "FW Bank Versions": [...],
    "FW Bank Ptr": 0,
    "FW Version": "EV05110R",                // resolved active version
    "FW Upgrade Status": {
      "Bank": 0,
      "Status": "Success",                   // status string
      "Progress": 100,                       // percent
      "Upgrade Time": "2026-03-18 17:12:43.093893",
      "Total Blocks": 482915,
      "Sent Blocks": 482915,
      "Failures": 0,
      // ...
    },
    "PON Mode": "GPON",
    "Online Time": "2026-03-18 17:13:42.285965",
    // ...
  },
  "STATS": {
    "ONU-PON": {
      "RX Pre-FEC BER":  22388041291,        // cumulative since reset
      "RX Post-FEC BER": 22379111198,
      "RX Optical Level": -17.544,            // dBm
      "TX Optical Level": 5.532               // dBm
    },
    "OLT-PON": {
      "RX Pre-FEC BER":  0,                  // upstream view
      "RX Post-FEC BER": 0,
      "TX Optical Level": 5.6,
      "RX Optical Level": -18.3
    },
    "ONU-EnhancedFecPmHistData-32769": {     // NOT the same as the BER fields above
      "uncorrectable_code_words": 1829,      // (these are codeword counts,
      "corrected_bytes": 181,                //  not bit-error-rate ratios)
      "corrected_code_words": 181
    },
    // ...many more
  },
  "Alarm": {
    "0-EMERG": [], "1-ALERT": [], "2-CRIT": [],
    "3-ERROR": [], "4-WARNING": [], "5-NOTICE": [],
    "6-INFO": [], "7-DEBUG": []
  }
}
```

### 5.3 OLT-STATE (relevant subset)

```jsonc
{
  "_id": "<OLT MAC>",
  "Time": "2026-05-04 21:51:57.394127",
  "ONU States": {
    "Registered":          ["onu1", "onu2", ...],
    "Deregistered":        [...],
    "Dying Gasp":          [...],
    "Disabled":            [...],
    "Disallowed Admin":    [...],
    "Disallowed Error":    [...],
    "Disallowed Reg ID":   [...],
    "Unspecified":         [...],
    "Unprovisioned":       [...]
  }
  // ...
}
```

### 5.4 AUTO-TASK-CFG (bulk firmware task, Procedure 8)

```jsonc
{
  "_id": "<your-task-id>",                  // unique, caller-generated
  "Task": {
    "Device Type": "ONU",
    "Operation": "Firmware Upgrade",
    "Scheduled Start Time": "2026-05-12 03:00:00"  // UTC space format
  },
  "Task Details": {
    "Devices": ["GNXS01", "GNXS02", ...],   // ONU IDs (serial / MAC)
    "ONU": {
      "FW Bank Ptr": 1,                     // target slot (single value!)
      "FW Bank Files":    { "0": "old.bin", "1": "new.bin" },
      "FW Bank Versions": { "0": "EV05110R", "1": "EV05120R" }
    }
  }
}
```

PUT to `/api/v3/tasks/configs/<task-id>/`. Note: `FW Bank Files` and
`FW Bank Versions` here are **objects keyed by slot index as strings**
(`"0"`, `"1"`), not arrays. This is different from the `ONU-CFG`
shape where the same fields are arrays — almost certainly a Mongo
serialisation quirk. Match it as-is.

**Single bank per task.** Because `FW Bank Ptr` accepts one slot, if
your selection spans ONUs with different active banks, bucket them
by target slot and create one task per bucket.

### 5.5 Firmware file entry (from `/api/v1/files/onu-firmware/`)

```jsonc
{
  "_id": "...",
  "filename": "Interos-Everest-5.12.0-R-EV05120R.bin",
  "metadata": {
    "Compatible Manufacturer": "TIBITCOM",
    "Compatible Model": ["MicroPlug ONU"],
    "Version": "EV05120R"
  },
  // GridFS metadata...
}
```

To upload:

```
POST /api/v1/files/onu-firmware/<filename>/
{
  "data": {
    "file": "<base64 binary contents>",
    "metadata": {
      "Compatible Manufacturer": "TIBITCOM",
      "Compatible Model": ["MicroPlug ONU"],
      "Version": "EV05120R"
    }
  }
}
```

---

## 6. Firmware upgrade flow

Two methods documented:

### 6.1 Procedure 7 — per-ONU PUT

For each ONU:

1. `GET /api/v1/onus/configs/<id>/` → full doc.
2. Compute the inactive bank (see §6.3).
3. Set:
   - `ONU["FW Bank Files"][writeSlot] = "<filename>"`
   - `ONU["FW Bank Versions"][writeSlot] = "<version>"`
   - `ONU["FW Bank Ptr"] = writeSlot`  (this initiates the download)
4. `PUT /api/v1/onus/configs/<id>/` with the entire mutated document.

Pros: see errors per ONU live. Cons: slow for large fleets, you own
retries.

### 6.2 Procedure 8 — bulk task (recommended)

Build one `AUTO-TASK-CFG` per (target slot) bucket:

```
PUT /api/v3/tasks/configs/<task-id>/
   (body shape in §5.4)
```

MCMS owns the rollout: scheduled start, retries, pacing.
Monitor progress (build permitting):

```
GET /api/v3/tasks/states/<task-id>/
```

Or just poll the ONU configs and watch `FW Bank Versions` /
`FW Bank Ptr` flip.

### 6.3 Inactive-bank selection

`FW Bank Ptr` identifies the active bank (0, 1, or 65535 = unset).
**Always write to the inactive bank** so you don't blow away the
running image:

| Current `FW Bank Ptr` | Write slot | Why |
|---|---|---|
| 0 | 1 | preserve active |
| 1 | 0 | preserve active |
| 65535 (unset) | 1 | matches Procedure 7 example; slot 0 stays as factory fallback |

MCMS flips `FW Bank Ptr` to the new slot when the download completes.
The ONU reboots into the new image on its next reboot.

---

## 7. FEC + optical interpretation

### 7.1 Four FEC counters

Per the user guide green-LED criteria (`323-1961-302` p149):

| Field | Path | Green | Red |
|---|---|---|---|
| ONU Pre-FEC BER | `STATS["ONU-PON"]["RX Pre-FEC BER"]` | 0 | (informational) |
| ONU Post-FEC BER | `STATS["ONU-PON"]["RX Post-FEC BER"]` | 0 | > 0 |
| OLT Pre-FEC BER | `STATS["OLT-PON"]["RX Pre-FEC BER"]` | 0 | (informational) |
| OLT Post-FEC BER | `STATS["OLT-PON"]["RX Post-FEC BER"]` | 0 | > 0 |

Note: the field name is "BER" but the magnitudes are big integers
(cumulative error byte counts since reset), not ratios in [0,1].
Treat them as counts.

### 7.2 Health bucket rules

Suggested 4-state reduction:

- **OK**: all four counters present and == 0.
- **Pre-FEC errors**: any pre > 0, all post == 0. (Marginal link;
  FEC is correcting.)
- **Post-FEC errors**: any post > 0. (Uncorrected bits reaching the
  user.)
- **Buggy (pre ≈ post)**: ONU pre > 0 AND post > 0 AND
  `post / pre >= 0.95`. Real FEC reduces errors; if post is within
  5% of pre, the firmware is mirroring the counter. Observed on
  Interos Everest ONUs.

### 7.3 Optical levels

```
STATS["ONU-PON"]["RX Optical Level"]   // ONU receive power, dBm
STATS["ONU-PON"]["TX Optical Level"]   // ONU transmit power, dBm
```

User guide thresholds:

| | Green | Red |
|---|---|---|
| RX | ≥ −30 dBm | < −30 dBm |
| TX | ≥ 3 dBm | < 3 dBm |

Operators usually want a margin warning band — e.g. yellow when RX
is between −28 and −30 dBm.

---

## 8. iOS-specific notes

### 8.1 Library stack suggestions

- **HTTP**: `URLSession` is fine. The features you need are standard
  cookie storage, custom headers, and the option to bypass cert
  validation. Don't reach for Alamofire just for this.
- **Cookies**: `HTTPCookieStorage` handles `__Host-` prefixes
  correctly. Get the CSRF value via:
  ```swift
  HTTPCookieStorage.shared.cookies(for: baseURL)?
    .first { $0.name == "__Host-csrftoken" || $0.name == "csrftoken" }?.value
  ```
- **Self-signed certs**: NOT accepted in-app — both clients use system trust only,
  with no "accept self-signed" bypass. A self-signed appliance must have its CA
  installed on the device's OS trust store (MDM/configuration profile). Do not
  add a trust-bypass toggle.
- **JSON**: `JSONSerialization` is simpler than `Codable` for the
  envelope pattern (`{status, data, details}`) because the `data`
  payload is heterogeneous. `Codable` works if you model each
  endpoint individually.

### 8.2 Concurrency

The fleet load is N HTTPS round-trips for per-ONU state (the bulk
endpoint is the optimisation but the per-ID fallback may also be
needed). Use Swift Concurrency (`TaskGroup`) with a concurrency cap
of 8–10 to avoid stampeding the server. The desktop client uses 5
for deletes and 8 for state fetches.

### 8.3 Background uploads

Firmware `.bin` files are 10–50 MB base64-encoded. If you implement
uploads:

- Don't block the main thread.
- Consider `URLSession` background configuration so an upload can
  survive an app suspend.
- The upload is a single POST; no chunking required.

### 8.4 Storing the session

For UX: persist the base URL and username in `UserDefaults`, never
the password. Re-prompt for password on launch.

The session cookie itself goes in the per-session cookie storage
and is automatically discarded — don't try to persist it.

### 8.5 Date handling

See §3.7. Default `DateFormatter` with `en_US_POSIX` locale + UTC
timezone, format `"yyyy-MM-dd HH:mm:ss.SSSSSS"` (microseconds) or
`"yyyy-MM-dd HH:mm:ss"` (seconds). Try the microsecond format
first, fall back to second if it fails.

### 8.6 Cell-vs-Wi-Fi

MCMS APIs are typically reachable only from the operator's internal
network. Test on cellular: many internal hosts won't be reachable,
and your error UI needs to communicate that clearly.

### 8.7 Self-signed certs → install the CA

There is no in-app "accept self-signed" prompt (removed for iOS/Android parity).
The lab MCMS at `https://mcms.lab.svorka.net/` uses a self-signed cert, so it is
unreachable until that CA is installed on the device's OS trust store
(MDM/configuration profile) — or the box is given a CA-trusted cert. Surface the
resulting trust failure clearly in the error UI so it doesn't look like a silent
login break.

---

## 9. Procedures referenced in the dev guide

| # | Title | Notes |
|---|---|---|
| 2 | Determining ONU registration status | OLT-STATE["ONU States"] buckets |
| 7 | Per-ONU firmware upgrade | Full-doc PUT to ONU-CFG |
| 8 | Bulk firmware upgrade | AUTO-TASK-CFG via PUT |
| 38 | Resetting bit error rate values | Names the four FEC fields |
| 43 | Deleting an ONU | DELETE on ONU-CFG |

(Page numbers vary by document revision; cite by procedure number.)

---

## 10. Quick implementation checklist for a new client

A correct first POST-login GET against `/api/v3/onus/configs/?limit=1`
returning HTTP 200 means you've got the eight critical pieces right:

- [ ] HTTPS reachable (with cert exception if needed)
- [ ] Path prefix `/api` added
- [ ] User-Agent set to non-empty value
- [ ] Cookie jar storing `__Host-sessionid` + `__Host-csrftoken`
- [ ] `X-CSRFToken` header pulled from `__Host-csrftoken` cookie value
- [ ] `Referer` header set to base URL
- [ ] `Accept: application/json`
- [ ] Trailing slash on the path

Add for write operations:

- [ ] `Content-Type: application/json`
- [ ] Body wrapped: `{"data": ...}`
- [ ] Full-document PUT (never partial)

Add for list endpoints:

- [ ] Page size ≤ 100
- [ ] `next` cursor (not `skip`)
- [ ] Loop until short page

Once those land, the API is comfortable to work with — the hard parts
are upfront.

---

*Compiled from `323-1961-306` (REST API dev guide), `323-1961-302`
(PON Manager User Guide), and empirical testing against MCMS 6.2 with
Interos Everest 5.11/5.12 firmware on GNXS / Tibit MicroPlug ONUs.*
