Complete reference for all CGI endpoints. All endpoints are under /cgi-bin/quecmanager/.
All authenticated endpoints require a valid qm_session cookie (auto-sent by the browser). A 401 response means the session is expired or missing.
All endpoints return JSON with a consistent structure:
// Success
{ "success": true, ... }
// Error
{ "success": false, "error": "error_code", "detail": "Human-readable message" }Check if first-time setup is required and rate limit status.
Response:
{
"setup_required": true,
"rate_limited": false,
"retry_after": 0
}Login or first-time password setup.
Login Request:
{ "password": "user_password" }Setup Request (first-time):
{ "password": "new_password", "confirm": "new_password" }Success Response:
{ "success": true }Sets qm_session (HttpOnly) and qm_logged_in=1 cookies.
Error Response:
{
"success": false,
"error": "invalid_password",
"detail": "Invalid password",
"retry_after": 30
}Destroy current session.
Response:
{ "success": true }Clears session cookies.
Change password. Requires authentication.
Request:
{
"current_password": "old_password",
"new_password": "new_password"
}Response:
{ "success": true }Destroys all sessions (forces re-login).
Main polling endpoint. Returns the cached modem status JSON (built by qmanager_poller).
Response: Full ModemStatus object (see types/modem-status.ts)
{
"timestamp": 1710700000,
"system_state": "normal",
"modem_reachable": true,
"last_successful_poll": 1710700000,
"errors": [],
"network": {
"type": "5G-NSA",
"sim_slot": 1,
"carrier": "T-Mobile",
"service_status": "optimal",
"ca_active": true,
"ca_count": 2,
"nr_ca_active": false,
"nr_ca_count": 0,
"total_bandwidth_mhz": 135,
"bandwidth_details": "B66: 20 MHz + B2: 15 MHz + N41: 100 MHz",
"apn": "fast.t-mobile.com",
"wan_ipv4": "10.0.0.1",
"wan_ipv6": "",
"primary_dns": "8.8.8.8",
"secondary_dns": "8.8.4.4",
"carrier_components": [...]
},
"lte": {
"state": "connected",
"band": "B66",
"earfcn": 66486,
"bandwidth": 20,
"pci": 123,
"cell_id": 12345678,
"enodeb_id": 48225,
"sector_id": 78,
"tac": 12345,
"rsrp": -95,
"rsrq": -11,
"sinr": 15,
"rssi": -65,
"ta": 3
},
"nr": {
"state": "connected",
"band": "N41",
"arfcn": 520110,
"pci": 456,
"rsrp": -100,
"rsrq": -12,
"sinr": 18,
"scs": 30,
"ta": null
},
"device": {
"temperature": 45,
"cpu_usage": 12,
"memory_used_mb": 85,
"memory_total_mb": 256,
"uptime_seconds": 86400,
"conn_uptime_seconds": 43200,
"firmware": "RM520NGLAAR03A04M4GA",
"build_date": "Jun 25 2025",
"manufacturer": "Quectel",
"model": "RM520N-GL",
"imei": "123456789012345",
"imsi": "310260123456789",
"iccid": "89012345678901234567",
"phone_number": "+15551234567",
"lte_category": "20",
"mimo": "LTE 1x4 | NR 2x4",
"supported_lte_bands": "B1:B2:B3:B5:B7:...",
"supported_nsa_nr5g_bands": "N41:N71:N77:...",
"supported_sa_nr5g_bands": "N41:N71:N77:..."
},
"traffic": {
"rx_bytes_per_sec": 1562500,
"tx_bytes_per_sec": 125000,
"total_rx_bytes": 1073741824,
"total_tx_bytes": 134217728
},
"connectivity": {
"internet_available": true,
"status": "connected",
"latency_ms": 34.2,
"avg_latency_ms": 38.5,
"min_latency_ms": 22.1,
"max_latency_ms": 89.3,
"jitter_ms": 4.8,
"packet_loss_pct": 0,
"ping_target": "8.8.8.8",
"latency_history": [34.2, 36.1, 38.0, ...],
"history_interval_sec": 5,
"history_size": 60,
"during_recovery": false
},
"signal_per_antenna": {
"lte_rsrp": [-95, -97, -102, null],
"lte_rsrq": [-11, -12, -13, null],
"lte_sinr": [15, 14, 12, null],
"nr_rsrp": [-100, -103, null, null],
"nr_rsrq": [-12, -13, null, null],
"nr_sinr": [18, 16, null, null]
},
"watchcat": {
"enabled": true,
"state": "monitor",
"current_tier": 0,
"failure_count": 0,
"last_recovery_time": null,
"last_recovery_tier": null,
"total_recoveries": 0,
"cooldown_remaining": 0,
"reboots_this_hour": 0
},
"sim_failover": {
"active": false,
"original_slot": null,
"current_slot": null,
"switched_at": null
},
"sim_swap": {
"detected": false,
"matching_profile_id": null,
"matching_profile_name": null
}
}Returns network events as a JSON array.
Response:
[
{
"timestamp": 1710700000,
"type": "band_change",
"message": "LTE band changed from B2 to B66",
"severity": "info"
}
]Returns signal history entries as a JSON array.
Response:
[
{
"ts": 1710700000,
"lte_rsrp": [-95, -97, -102, null],
"lte_rsrq": [-11, -12, -13, null],
"lte_sinr": [15, 14, 12, null],
"nr_rsrp": [-100, -103, null, null],
"nr_rsrq": [-12, -13, null, null],
"nr_sinr": [18, 16, null, null]
}
]Returns ping history entries as a JSON array.
Response:
[
{
"ts": 1710700000,
"lat": 34.2,
"avg": 38.5,
"min": 22.1,
"max": 89.3,
"loss": 0,
"jit": 4.8
}
]Execute a raw AT command.
Request:
{ "command": "AT+QENG=\"servingcell\"" }Response:
{ "success": true, "response": "+QENG: \"servingcell\",..." }Start the cell scanner daemon.
Response:
{ "success": true }Get cell scan results.
Response:
{
"success": true,
"status": "complete",
"cells": [...]
}Same pattern as cell scanner for neighbor cells.
Start speed test, check results, and check if speedtest binary is available.
GET Response:
{
"success": true,
"mode_pref": "AUTO",
"nr5g_disable_mode": 0,
"roam_pref": 255,
"sim_slot": 1,
"ambr_dl": "1000",
"ambr_ul": "500"
}POST Request:
{
"mode_pref": "NR5G",
"nr5g_disable_mode": 0,
"roam_pref": 1,
"sim_slot": 1
}GET Response:
{
"success": true,
"profiles": [
{
"cid": 1,
"apn": "fast.t-mobile.com",
"pdp_type": "IPV4V6",
"is_data": true
}
],
"active_cid": 1
}POST Request (create/update):
{
"action": "set",
"cid": 1,
"apn": "fast.t-mobile.com",
"pdp_type": "IPV4V6"
}POST Request (delete):
{
"action": "delete",
"cid": 3
}GET Response:
{
"success": true,
"profiles": [
{ "name": "Commercial-TMO", "active": true }
],
"auto_sel": true
}POST Actions: "apply_profile", "auto_sel", "reboot"
GET Response:
{
"success": true,
"imei": "123456789012345",
"backup": { "enabled": true, "imei": "123456789012345" }
}POST Actions: "set_imei", "save_backup", "reboot"
GET Response:
{
"success": true,
"mode_pref": "AUTO",
"nr5g_disable_mode": 0
}GET Response:
{
"success": true,
"has_entries": true
}POST Request:
{ "action": "clear" }SMS inbox and send functionality. Backed by sms_tool -d /dev/smd11, serialized against qcmd/atcli_smd11 via the shared /var/lock/qmanager.lock.
GET Response:
{
"success": true,
"messages": [
{
"indexes": [0, 1],
"sender": "+14155550100",
"content": "Concatenated multi-part message body",
"timestamp": "25/03/14,15:27:04+08"
}
],
"storage": {
"used": 3,
"total": 25
}
}Multi-part messages (same sender + reference) are merged into a single entry; indexes lists every storage slot so delete can clear them all at once.
POST (send):
{
"action": "send",
"phone": "+14155551234",
"message": "Hello from QManager"
}Phone-number handling: the endpoint strips a leading + before calling sms_tool and does nothing else. There is no IMSI lookup, no MCC-to-country-code table, and no local-number rewriting — users are responsible for providing the full international number (with or without a leading +).
POST (delete one or more storage slots):
{ "action": "delete", "indexes": [0, 1] }POST (delete everything):
{ "action": "delete_all" }Current locked band configuration.
POST Request:
{
"lte_bands": "B2:B66",
"nr_bands": "N41:N71"
}Band failover daemon status.
Enable/disable band failover automation.
POST Request:
{
"earfcn": 66486,
"pci": 123
}Current frequency lock state.
POST Request:
{
"lte_pci": 123,
"nr_pci": 456,
"lte_earfcn": 66486,
"nr_arfcn": 520110
}Current tower lock state.
Tower locking general settings.
Tower failover daemon status.
Scheduled tower lock changes (time-based).
GET Response:
{
"success": true,
"operstate": "up",
"speed": 1000,
"duplex": "full",
"autoneg": "on",
"speed_limit": "auto"
}POST Request:
{ "speed_limit": "auto" }Values: "auto", "10", "100", "1000"
GET Response:
{
"success": true,
"ttl": 65,
"hl": 65,
"autostart": true
}POST Request:
{ "ttl": 65, "hl": 65 }0 = disabled.
GET Response:
{
"success": true,
"mtu": 1500,
"active": true
}POST Request:
{ "mtu": 1500 }"disable" POST to remove MTU override.
Custom DNS override settings.
IP passthrough mode configuration.
{
"success": true,
"profiles": [
{
"id": "abc123",
"name": "T-Mobile Optimized",
"active": true,
"created_at": 1710700000
}
]
}Full profile details including APN, TTL/HL, and optional IMEI.
Create or update a profile.
{ "id": "abc123" }Start the 3-step async apply process.
{ "id": "abc123" }{
"success": true,
"status": "running",
"step": 2,
"total_steps": 3,
"message": "Applying TTL/HL settings..."
}Status values: "idle", "running", "complete", "error"
Deactivate the currently active profile.
Get current modem settings for pre-filling profile creation forms.
List all saved connection scenarios (preset templates).
Create or update a scenario.
Delete a scenario.
Activate a scenario (applies it as a profile).
Get the currently active scenario.
GET Response:
{
"success": true,
"settings": {
"enabled": true,
"sender_email": "alerts@gmail.com",
"recipient_email": "admin@example.com",
"app_password_set": true,
"threshold_minutes": 5
}
}POST (save settings):
{
"action": "save_settings",
"enabled": true,
"sender_email": "alerts@gmail.com",
"recipient_email": "admin@example.com",
"app_password": "xxxx xxxx xxxx xxxx",
"threshold_minutes": 5
}app_password only sent when changed. Backend returns app_password_set: boolean (never the actual password).
POST (send test):
{ "action": "send_test" }{
"success": true,
"entries": [
{
"timestamp": 1710700000,
"trigger": "downtime_recovery",
"status": "sent",
"recipient": "admin@example.com"
}
],
"total": 5
}GET Response:
{
"success": true,
"settings": {
"enabled": true,
"recipient_phone": "14155551234",
"threshold_minutes": 5
}
}POST (save settings):
{
"action": "save_settings",
"enabled": true,
"recipient_phone": "+14155551234",
"threshold_minutes": 5
}POST (send test):
{ "action": "send_test" }Validation notes:
recipient_phoneis required whenenabled=true- Accepts E.164 format with or without a leading
+on input. The CGI strips a leading+exactly once before writingsms_alerts.json, so storage and GET responses always return raw digits. The send path passes the value verbatim tosms_tool. threshold_minutesrange is1..60- Test-send failures return
"error":"send_failed"with a static"detail":"sms_tool send failed — check logread for details". Full context (modem state,sms_toolstderr) is logged viaqlog_error.
{
"success": true,
"entries": [
{
"timestamp": "2026-04-10 15:27:04",
"trigger": "Connection down 5m 2s",
"status": "sent",
"recipient": "14155551234"
}
],
"total": 3
}Note: recipient mirrors the stored form in sms_alerts.json — raw digits, no leading +.
GET Response:
{
"success": true,
"enabled": true,
"state": "monitor",
"config": {
"check_interval": 10,
"suspect_threshold": 3,
"recovery_timeout": 60,
"cooldown_period": 120,
"max_tier": 4,
"sim_failover_enabled": true,
"reboot_enabled": true
},
"status": {
"current_tier": 0,
"failure_count": 0,
"total_recoveries": 0,
"reboots_this_hour": 0
}
}Device hardware and firmware information.
System log output.
System preferences, scheduled reboot, and low power mode.
GET Response:
{
"success": true,
"settings": {
"wan_guard_enabled": true,
"temp_unit": "celsius",
"distance_unit": "km",
"timezone": "UTC0",
"zonename": "UTC"
},
"scheduled_reboot": {
"enabled": false,
"time": "04:00",
"days": [0, 1, 2, 3, 4, 5, 6]
},
"low_power": {
"enabled": false,
"start_time": "23:00",
"end_time": "06:00",
"days": [0, 1, 2, 3, 4, 5, 6]
}
}POST (save_settings):
{
"action": "save_settings",
"wan_guard_enabled": true,
"temp_unit": "celsius",
"distance_unit": "km",
"timezone": "EST5EDT,M3.2.0,M11.1.0",
"zonename": "America/New_York"
}temp_unit:"celsius"or"fahrenheit"distance_unit:"km"or"miles"wan_guard_enabled: toggles init.d symlink (enable/disable)hostname/timezone/zonename: written to UCIsystem.@system[0]. Handler compares each incoming value to the current UCI value and only writes when changed. When any of these three actually change, the handler backgrounds/etc/init.d/system reloadto republish/tmp/TZ,/tmp/localtime, and kernel hostname. Whentimezoneorzonenamechanges, it additionally backgrounds/etc/init.d/cron restartso busybox crond (which caches TZ at startup) picks up the new zone forqmanager_scheduled_rebootandqmanager_low_powerentries. Both spawns are fire-and-forget so the HTTP response returns promptly.
POST (save_scheduled_reboot):
{
"action": "save_scheduled_reboot",
"enabled": true,
"time": "04:00",
"days": [0, 1, 2, 3, 4, 5, 6]
}days: array of integers 0-6 (0=Sunday, 6=Saturday)- Manages cron entries for
/usr/bin/qmanager_scheduled_reboot - Config persisted in UCI
quecmanager.settings.sched_reboot_*
POST (save_low_power):
{
"action": "save_low_power",
"enabled": true,
"start_time": "23:00",
"end_time": "06:00",
"days": [0, 1, 2, 3, 4, 5, 6]
}- Creates two cron entries:
enterat start_time on selected days,exitat end_time on all 7 days - Exit cron fires on all days to handle overnight windows (e.g., 23:00-06:00) — no-ops if flag absent
- Enables/disables
qmanager_low_power_checkinit.d (boot-time window check) - Disabling while active immediately triggers
qmanager_low_power exit(restores CFUN=1)
Triggers a device reboot. POST-only, no request body required.
Response:
{ "success": true }The HTTP response is flushed before the device reboots asynchronously. The connection will drop shortly after.
Collects the plaintext sections selected by the user, ready for browser-side encryption into a .qmbackup file. No crypto runs on the server — the response is plaintext JSON over the localhost HTTP boundary.
Query parameters:
sections— comma-separated list of section keys. Valid keys:sms_alerts,watchdog,network_mode_apn,bands,tower_lock,ttl_hl,imei,profiles. Unknown keys return 400.
Response:
{
"schema": 1,
"header": {
"magic": "QMBACKUP",
"version": 1,
"created_at": "2026-04-13T10:30:00Z",
"device": {
"model": "RM520N-GL",
"firmware": "RM520NGLAAR03A07M4G",
"imei": "860000000000000",
"qmanager_version": "0.1.16"
},
"sections_included": ["network_mode_apn", "bands"]
},
"payload": {
"schema": 1,
"sections": {
"network_mode_apn": { /* section-specific shape */ },
"bands": { /* section-specific shape */ }
}
}
}The browser uses the header as the canonical Associated Data when encrypting payload, then writes the result into a .qmbackup envelope. The CGI emits a config_backup_collected event on successful return.
Error responses:
400 {"error":"no_sections_selected"}— empty or missingsectionsquery param400 {"error":"unknown_section","key":"<key>"}— unrecognized section key500 {"error":"collect_fragment_invalid"}— pre-flightjq -evalidation of the assembledSECTIONS_JSONfailed (a section'scollect_*function emitted invalid JSON)500 {"error":"collect_failed","key":"<key>"}— a section's collect function returned non-zero
Accepts a decrypted backup payload and spawns the detached qmanager_config_restore worker. POST-only.
Request body: the plaintext payload object from a successfully-decrypted .qmbackup file:
{
"schema": 1,
"sections": {
"network_mode_apn": { ... },
"bands": { ... }
}
}- Body size cap: 256 KiB (enforced via
CONTENT_LENGTHinspection before reading stdin) Content-Typemust beapplication/json- All section keys must be in the known-keys list
Success response (202):
{ "status": "started", "job_id": "1712990400" }The worker is spawned via double-fork (no setsid on OpenWRT). The frontend should immediately begin polling apply_status.sh at 500ms.
Error responses:
405 {"error":"method_not_allowed"}— non-POST request415 {"error":"unsupported_content_type"}— Content-Type notapplication/json413 {"error":"payload_too_large"}— body exceeds 256 KiB400 {"error":"invalid_json"}— body is not parseable JSON400 {"error":"wrong_schema"}—schema != 1400 {"error":"unknown_section","key":"<key>"}— unrecognized section key in payload400 {"error":"no_sections"}— payloadsectionsobject is empty409 {"error":"restore_in_progress","pid":<n>}— concurrency guard (worker PID file alive)
Returns the current restore progress JSON. Cheap; safe to poll at 500ms.
Response (idle):
{ "status": "idle" }Response (running / done / cancelled):
{
"job_id": "1712990400",
"status": "running",
"started_at": 1712990400,
"completed_at": null,
"sections": [
{ "key": "sms_alerts", "status": "success", "attempts": 1, "message": "" },
{ "key": "bands", "status": "retrying:2", "attempts": 2, "message": "" },
{ "key": "imei", "status": "pending", "attempts": 0, "message": "" }
],
"summary": null,
"reboot_required": false
}Section status values: pending, running, retrying:N (N is the retry attempt number, 1-3), success, failed, skipped:incompatible, skipped:not_in_backup, skipped:sim_mismatch.
Final-state fields: when status becomes done or cancelled, completed_at is set and summary is populated:
{
"summary": { "success": 5, "failed": 1, "skipped": 1 },
"reboot_required": true
}reboot_required is true if any applied section queued a reboot-pending change (IMEI write or profile activation). The frontend uses this to show the post-restore "Reboot now or later" dialog.
Signals the running worker to cancel after the current section completes. POST-only.
Response:
{ "status": "cancel_requested" }Writes /tmp/qmanager_config_restore.cancel as a sentinel. The worker checks this file between sections and exits cleanly with final_status: "cancelled". Cannot abort mid-section — AT commands and applier functions run to completion. The cancel is best-effort.
Error responses:
405 {"error":"method_not_allowed"}— non-POST request (important: a stray authenticated GET would otherwise silently abort a running restore)
The DPI Settings page manages two features through a single CGI endpoint: Video Optimizer (SNI splitting for video throttle bypass) and Traffic Masquerade (fake TLS ClientHello with spoofed SNI). Both share the nfqws binary and kernel module but run as separate nfqws instances on different NFQUEUE numbers.
Read video optimizer settings and service status.
Response:
{
"success": true,
"enabled": true,
"status": "running",
"uptime": "2h 34m",
"packets_processed": 48291,
"domains_loaded": 22,
"binary_installed": true,
"kernel_module_loaded": true
}Status values: running, stopped, restarting, error
Read traffic masquerade settings and service status.
Response:
{
"success": true,
"enabled": true,
"status": "running",
"uptime": "1h 12m",
"packets_processed": 15320,
"sni_domain": "speedtest.net",
"binary_installed": true,
"kernel_module_loaded": true
}Status values: running, stopped
Poll verification test progress/results.
Response (running):
{"success": true, "status": "running"}Response (complete):
{
"success": true,
"status": "complete",
"timestamp": "2026-03-24T14:30:00Z",
"without_bypass": {"speed_mbps": 2.4, "throttled": true},
"with_bypass": {"speed_mbps": 47.2, "throttled": false},
"improvement": "19.7x"
}Poll nfqws installation progress/results.
Response (idle — no install started):
{"success": true, "status": "idle"}Response (running):
{"success": false, "status": "running", "message": "Downloading zapret v69...", "detail": ""}Response (complete):
{"success": true, "status": "complete", "message": "nfqws installed successfully", "detail": "v69"}Response (error):
{"success": false, "status": "error", "message": "Binary not found in archive", "detail": "No nfqws for linux-arm64 in tarball"}Save video optimizer settings:
{"action": "save", "enabled": true}Save traffic masquerade settings:
{"action": "save_masquerade", "enabled": true, "sni_domain": "speedtest.net"}enabled(boolean, required): Enable or disable traffic masquerade.sni_domain(string, optional): Domain to spoof in fake TLS ClientHello. Must contain at least one dot, max 253 characters. Defaults tospeedtest.netif not provided.
Saving masquerade settings restarts the entire qmanager_dpi service (both instances) to apply changes.
Start verification:
{"action": "verify"}Install nfqws binary (downloads from zapret GitHub releases):
{"action": "install"}Returns {"success": true, "status": "started"} if the installer was spawned, or {"success": true, "status": "running"} if an install is already in progress. Poll ?action=install_status for progress.
Tailscale VPN status and configuration.