IN-CONTROL API Issues, Claude write for me :)

We operate a maritime fleet (vessels with Pepwave routers + InControl appliance) and have built an automated usage-reporting and crew internet token system on top of the InControl2 REST API. Overall the API is workable, but we hit several limitations that forced workarounds. Sharing in case these can be improved:

1. POST /rest/o/{org}/g/{group_id}/cp/daily_usage ignores its parameters

The request body fields portal_id and report_date appear to be ignored — every call returns the current day’s data for ALL portals in the group, regardless of what is sent. Verified by sending {"portal_id":"29","report_date":"2026-06-09"}, {}, and other variants: all returned byte-identical responses stamped with today’s date.

Impact: there is no way to query yesterday’s captive portal usage. We were forced to run a scheduled snapshot job at 23:50 every night to capture the day’s data before it disappears. A working report_date parameter (even just yesterday) would remove this fragile workaround.

2. No endpoint returns historical per-client captive portal usage

GET .../d/{device_id}/client?bandwidth=true only returns currently connected clients. If a client is offline at query time, their usage that day is invisible. Combined with point 1, there is no reliable API way to answer “how much data did portal X’s clients use on date Y” — even though the InControl web UI clearly has this data in Captive Portal Reports.

Request: expose the same historical per-client / per-portal usage data the web UI reports, queryable by date range.

3. bandwidthUsed semantics are undocumented

The bandwidthUsed field in cp/daily_usage rows is a per-session cumulative counter that resets when a session restarts (e.g. at the portal’s daily quota reset). This is not documented anywhere — we had to reverse-engineer it from sample sequences. Consumers who treat it as a daily total will silently over- or under-count. Please document the counter semantics, or better, provide a pre-computed per-day delta.

4. captive_portal_user is lookup-only — no list mode

GET .../cp/{cp_id}/captive_portal_user requires token= or username= and returns a single record. There is no way to list all users/accounts of a captive portal with their quota and usage. For operators managing crew accounts across a fleet, a paginated list endpoint would be extremely useful.

5. No write operations for captive portal users

There are no PUT/POST/DELETE endpoints for captive portal user accounts — no way to create accounts, change passwords, adjust quotas, or disable users via API. Everything must be done manually in the web UI. For fleet operators with hundreds of crew accounts this is the single biggest gap. Even a password-change endpoint alone would enable crew self-service portals.

6. captive_portal_user_info is marketing-data only

The endpoint name suggests user account info, but it returns only social/email login marketing fields (email, gender, country, visit_count). No quota or usage. The naming is confusing next to captive_portal_user.

7. Minor: API doc gaps

The Swagger doc for cp/daily_usage describes the request body as just data (Raw, String) with no schema, no field list, and no response model. Several captive portal endpoints share this. Documenting request/response schemas would save a lot of trial and error.

Summary of requests, in priority order for us:

  1. Captive portal user write API (create / update password / set quota / disable)
  2. Historical per-client portal usage queryable by date (report_date honored on cp/daily_usage, or a new report endpoint)
  3. List-all mode for captive_portal_user
  4. Documented counter semantics for bandwidthUsed

Happy to provide request/response samples or test against beta endpoints. Setup: InControl appliance, OAuth2 client_credentials, org-level API access.

6 Likes

Thank you for raising these points. I just can’t understand why Peplinks captive portal is so underdeveloped. Quota amounts for each day in the history should be easily visible through api calls and even on IC2, but they are very limited as you have highlighted. The captive portal could give us so much better user data as it exists already, we just can’t access it! Please pep let’s get better portal reporting and api behavior.

1 Like

And endless bug, that is the biggest push for me to leave Peplink (if not earned some money on Peplink stonks:)

I would also like to note that the API documentation is wrong in most places with what the request should be and what the response returned is. It also is not updated for all the possibilities of a schema of what can be returned with latest features and firmware. As well as missing other features like setting up SpeedFusion VPN tunnels in IC2.

Thank you all for the feedback. No excuses, the issues raised here are valid and we are taking this seriously. I will make sure this reaches the right people!

3 Likes

Hi, thanks for the thorough write-up, the reproduction details and priority ordering are very helpful.

I’m on the InControl2 development team. We’ve reviewed your points and are queuing them for development, with the captive portal user write API.

No firm timeline yet, but we expect most of these to land in the next release. We’ve noted your offer to provide samples and test beta endpoints, and we may follow up. Thanks again for the constructive feedback.

2 Likes

I have asked Fabel 5 (unfortunately, it is already gone), how could it possible Peplink make such great router but a problematic captive portal.
He (or she) has has point out the root cause is your portal design is for marketing and did not for billing like marine applications, where we need serious billing. Yes, indeed if we use this in hotel or some bar, no one will bother on the data lost of reset, but for marine, Starlink/Oneweb charge us per GB (1000MB) and we also need to sell per GB (Not 1024MB), and we need the counting very correct as it is real money.

Here is his assessment and I feel it is make sense:

The original sin: captive portal was designed for marketing, not billing. Peplink’s portal clearly grew out of the “guest Wi-Fi at a café/hotel” use case — collect an email, show a branded page, maybe cap freeloaders at 500MB/day. In that world, a quota is a courtesy limit. If it occasionally resets or miscounts, nobody loses money and nobody complains. Then operators like you started using tokens as a prepaid billing product, where every byte is money. The accounting layer was never built to billing-grade standards because it never needed to be. Same code, completely different correctness requirement.

From that follows the real technical root cause: distributed state with no single source of truth. Think about where a token’s “remaining data” lives in their system. The router counts bytes in real time. InControl holds the token definition and aggregates usage. So the same state exists in two places, connected by a sync protocol over a WAN link that — on ships especially — drops constantly. Now every classic distributed-systems failure becomes a billing bug:

  • Device reboots or loses InControl → re-syncs → pulls the token’s original definition instead of current consumed state → token “tops up.” That’s almost certainly your 3GB-became-10GB bug.
  • Sync conflict resolved by last-writer-wins or “cloud wins” → consumed counters overwritten → usage records “disappear.”
  • The Feb 2025 “fix” patched one sync path, but if the architecture is replicate-and-reconcile, new paths keep regressing. Which matches what you’ve seen.

Compounding it: they store counters, not ledgers. My strong suspicion is the token record is literally something like used_bytes: 2917483921 — a mutable counter that gets overwritten on sync. The billing-grade design is an append-only ledger: immutable usage events (device X consumed Y bytes for token Z at time T, with a sequence number), with “remaining quota” always derived by summing. Ledgers make resets impossible (you can’t un-append), make conflicts mergeable (events from two sources just interleave), and give you an audit trail when a customer disputes. Counters give you exactly the bug zoo you’ve collected. The fact that data can vanish without trace tells me there’s no event log underneath — if there were, support could replay it.

And the unit thing is a symptom of the same culture. Using 1024-based GB is the engineer’s convention (memory, filesystems). 1000-based is the telecom billing convention. Choosing 1024 for a data quota feature is a small tell that nobody on that team was thinking “this number appears on an invoice.”

So if I were writing the high-level guidance for their developers, it’d be roughly: (1) decide explicitly that token quotas are billing state, and hold them to billing correctness standards; (2) make the device the single authoritative byte-counter and the ledger append-only — InControl should aggregate events, never write back consumed state to devices; (3) make sync idempotent with sequence numbers so replays and reconnects can’t double-count or reset; (4) on sync failure, fail closed for quota (keep enforcing last-known remaining) rather than fail open; (5) expose the ledger so operators can audit.

We hear you. Our team will completely review the captive portal in a different way to address the need.

4 Likes