- Base path:
/api/with.jsonsuffix on all endpoints - Auth: Token passed as query parameter (
?token=xxx) - Request format: Form-encoded POST bodies (
Monitor[Function]=Modect) - Response wrapping: Objects nested inside
{ monitors: [{ Monitor: {...}, Monitor_Status: {...} }] } - Naming: PascalCase field names (
MonitorId,StartDateTime,CaptureFPS) - IDs: Returned as strings, coerced to numbers client-side via Zod
- Base path:
/api/v3/with no suffix - Auth: JWT Bearer token in
Authorizationheader - Request format: JSON bodies
- Response wrapping: Flat objects, no nested wrappers (
{ id, name, ... }directly) - Naming: snake_case field names (
monitor_id,start_date_time,capture_fps) - IDs: Native integers
| zmNg Function | Current Endpoint | V3 Endpoint | Notes |
|---|---|---|---|
| Login | POST /host/login.json (form-encoded) |
POST /api/v3/auth/login (JSON) |
V3 uses JSON body, returns TokenResponse with token_type, expire_in (single int, not separate access/refresh expires). V3 login response is a oneOf (Token or Code for 2FA). |
| Refresh Token | POST /host/login.json (with refresh token) |
POST /api/v3/auth/refresh (JSON {token}) |
Dedicated refresh endpoint in v3 |
| Logout | N/A (zmNg doesn't call logout) | GET /api/v3/auth/logout |
New capability |
| Get Version | GET /host/getVersion.json |
GET /api/v3/host/getVersion |
V3 adds db_version field |
| Get Timezone | GET /host/getTimeZone.json |
MISSING | No timezone endpoint in v3 spec |
| List Monitors | GET /monitors.json |
GET /api/v3/monitors |
V3 returns flat MonitorResponse[], no Monitor_Status inline |
| Get Monitor | GET /monitors/{id}.json |
GET /api/v3/monitors/{id} |
Same |
| Update Monitor | POST /monitors/{id}.json (form: Monitor[key]=val) |
PATCH /api/v3/monitors/{id} (JSON) |
Method changes POST→PATCH, format changes form→JSON |
| Monitor Status | Embedded in monitor response (Monitor_Status) |
GET /api/v3/monitor-status or /api/v3/monitor-status/{monitor_id} |
Separate endpoint in v3 |
| Alarm On/Off/Status | GET /monitors/alarm/id:{id}/command:{cmd}.json |
PATCH /api/v3/monitors/{id}/alarm (JSON body AlarmControlRequest) |
GET→PATCH, command in URL→body |
| Daemon Status (zmc/zma) | GET /monitors/daemonStatus/id:{id}/daemon:{name}.json |
GET /api/v3/daemons or /api/v3/daemons/{id} |
Different model: v3 lists all daemons centrally, no per-monitor daemon query |
| PTZ Controls | GET /controls/{id}.json |
GET /api/v3/controls/{id} |
Same concept |
| List Events | GET /events/index.json (CakePHP query params) |
GET /api/v3/events (query params: monitor_id, start_time, end_time, page, page_size) |
Simplified query params; no sort/direction in v3 spec |
| Get Event | GET /events/{id}.json |
GET /api/v3/events/{id} |
Same |
| Update Event | PUT /events/{id}.json (form: Event[Archived]=1) |
PATCH /api/v3/events/{id} (JSON EventUpdateRequest) |
PUT→PATCH, limited fields in v3 (name, cause, notes, orientation - no Archived) |
| Delete Event | DELETE /events/{id}.json |
DELETE /api/v3/events/{id} |
Same |
| Console Events | GET /events/consoleEvents/{interval}.json |
GET /api/v3/events/counts/{hours} |
Returns EventCountsResponse with {count, date}[] instead of Record<MonitorId, count> |
| List States | GET /states.json |
GET /api/v3/states |
Same |
| Change State | POST /states/change/{name}.json |
POST /api/v3/states/change/{action} |
Same pattern |
| List Tags | GET /tags.json |
GET /api/v3/tags |
V3 adds event_count field |
| Tags for Events | GET /tags/index/Events.Id:{id1},{id2}.json |
GET /api/v3/events-tags |
Different model: v3 uses separate events-tags resource |
| List Zones | GET /zones.json?MonitorId={id} |
GET /api/v3/monitors/{id}/zones |
Nested under monitor in v3 |
| List Groups | GET /groups.json |
GET /api/v3/groups + GET /api/v3/groups-monitors |
V3 separates group-monitor mapping into its own resource |
| List Servers | GET /servers.json |
GET /api/v3/servers |
V3 ServerResponse is simpler (no CpuLoad, TotalMem, FreeMem inline) |
| Daemon Check | GET /host/daemonCheck.json |
GET /api/v3/system/status |
V3 returns SystemStatusResponse with running boolean + daemon list + stats |
| Server Load | GET /host/getLoad.json |
GET /api/v3/system/status → stats.cpu_load |
Folded into system status |
| Disk Usage | GET /host/getDiskPercent.json |
MISSING | No disk usage endpoint in v3 spec |
| List Configs | GET /configs.json |
GET /api/v3/configs |
Same |
| Get Config By Name | GET /configs/viewByName/{name}.json |
GET /api/v3/configs/{name} |
Same, by name |
| List Logs | GET /logs.json |
GET /api/v3/logs |
V3 LogResponse drops TimeKey, ServerId, Pid, File, Line fields |
| Go2RTC/ZMS paths | Config lookups (ZM_GO2RTC_PATH, ZM_PATH_ZMS) |
GET /api/v3/streams/{id} returns StreamEndpoints |
V3 has a streaming registry with WebRTC/HLS/MJPEG endpoints per stream |
These are capabilities zmNg currently uses that have no direct equivalent in the v3 spec:
-
Timezone endpoint —
GET /host/getTimeZone.jsonhas no v3 equivalent. zmNg uses this during bootstrap to sync server time. Would need to be added to v3 or obtained from system status. -
Disk usage —
GET /host/getDiskPercent.jsonhas no v3 equivalent. zmNg shows this on the dashboard. -
Event archive/unarchive — The v3
EventUpdateRequestonly supportsname,cause,notes,orientation. Noarchivedfield. zmNg usesEvent[Archived]=1|0for archive toggling. -
Event sorting/filtering — V3
EventQueryParamssupportsmonitor_id,start_time,end_time,page,page_size. Missing:AlarmFramesfilter (min alarm frames),sortfield,direction(asc/desc). zmNg uses all of these. -
Console events per monitor — Current API returns
Record<MonitorId, EventCount>. V3 returns{count, date}[]— appears to be aggregate counts by date, not per-monitor breakdown. zmNg depends on per-monitor counts for the console view. -
Log filtering by component — Current:
GET /logs/index/Component:{name}.json. V3: no component filter parameter visible in the spec. -
Log pagination — V3 log endpoint doesn't show pagination params in the spec.
-
Log detail fields — V3
LogResponsedropsTimeKey,ServerId,Pid,File,Linethat zmNg currently displays. -
Server stats inline — Current
ServerResponseincludesCpuLoad,TotalMem,FreeMem,State_Id. V3ServerResponseonly hasid,name,hostname,port,status. Server stats are a separate resource (/api/v3/server-stats). -
ZMS streaming URLs — The legacy CGI-based streaming (
nph-zms) has no direct v3 equivalent. V3 replaces this with MSE segments + WebRTC + HLS + MJPEG via/api/v3/streams/{id}and/api/v3/mse/streams/{camera_id}. This is a fundamental streaming architecture change. -
PTZ via index.php — Current PTZ sends commands via
GET /index.php?view=request&request=control&id=X&control=cmd. No equivalent in v3 spec (v3 has controls CRUD but no PTZ command dispatch endpoint). -
Event image/video URLs — Current image/video URLs go through
index.php(view=image,view=view_video). V3 has no equivalent — this will likely need a different streaming/media approach.
| Aspect | Current | V3 |
|---|---|---|
| Token delivery | Query param ?token=xxx |
Authorization: Bearer xxx header |
| Login format | Form-encoded | JSON |
| Refresh | Reuses login endpoint | Dedicated /auth/refresh |
| Token response | access_token, access_token_expires, refresh_token, refresh_token_expires, version, apiversion |
token_type, access_token, refresh_token, expire_in (single expiry) |
| 2FA | N/A | Supported via LoginResponse.Code variant |
| Logout | N/A | GET /auth/logout |
- Current:
{ monitors: [{ Monitor: {...}, Monitor_Status: {...} }] }— nested with PascalCase - V3:
[{ id, name, ... }]— flat arrays with snake_case
- Current: Form-encoded (
Monitor[Function]=Modect) - V3: JSON bodies with PATCH method
- Current: Bundled with monitor response
- V3: Separate
/api/v3/monitor-status/{monitor_id}endpoint
- Current: ZMS CGI (
nph-zms) + Go2RTC (optional) - V3: First-class MSE/WebRTC/HLS/MJPEG via
/api/v3/streamsand/api/v3/mse/streams
┌─────────────────────────────┐
│ App Logic (hooks, stores, │
│ components) │ ← No changes needed
├─────────────────────────────┤
│ API Adapter Interface │ ← New: defines the contract
│ (app/src/api/adapter.ts) │
├──────────┬──────────────────┤
│ V1 Impl │ V3 Impl │ ← Two implementations
│ (legacy) │ (new) │
└──────────┴──────────────────┘
Create app/src/api/adapter.ts that defines an ApiAdapter interface matching the function signatures zmNg already uses internally. This interface represents the application's data needs, not any specific API version.
interface ApiAdapter {
// Auth
login(username: string, password: string): Promise<LoginResult>;
refreshToken(refreshToken: string): Promise<LoginResult>;
getVersion(): Promise<VersionResult>;
getTimezone(): Promise<string | null>;
// Monitors
getMonitors(): Promise<MonitorData[]>;
getMonitor(id: string): Promise<MonitorData>;
updateMonitor(id: string, changes: Record<string, unknown>): Promise<void>;
getMonitorStatus(id: string): Promise<MonitorStatus>;
triggerAlarm(id: string): Promise<void>;
cancelAlarm(id: string): Promise<void>;
getAlarmStatus(id: string): Promise<AlarmStatus>;
getDaemonStatus(id: string, daemon: string): Promise<DaemonStatus>;
// Events
getEvents(params: EventQueryParams): Promise<EventsResult>;
getEvent(id: string): Promise<Event>;
updateEvent(id: string, changes: Record<string, unknown>): Promise<void>;
deleteEvent(id: string): Promise<void>;
getConsoleEvents(interval: string): Promise<Record<string, number>>;
// States, Groups, Tags, Zones, Servers, Configs, Logs
// ... (same pattern)
// Server health
daemonCheck(): Promise<boolean>;
getServerLoad(): Promise<number[]>;
getDiskUsage(): Promise<{ usage: number; percent: number } | null>;
// Streaming URL construction
getStreamEndpoints(monitorId: string): Promise<StreamEndpoints>;
}The return types here are the internal types that zmNg already uses (or close to them). Each adapter implementation transforms the API-specific response into these types.
Move the current API logic from app/src/api/auth.ts, monitors.ts, events.ts, etc. into a v1/ subfolder that implements the ApiAdapter interface.
app/src/api/
adapter.ts ← Interface + factory
types-internal.ts ← Internal types (renamed from current types.ts)
v1/
auth.ts ← Current auth.ts logic
monitors.ts ← Current monitors.ts logic
events.ts ← Current events.ts logic
...
types.ts ← V1-specific Zod schemas (current types.ts schemas)
index.ts ← V1 adapter factory
v3/
auth.ts ← V3 auth implementation
monitors.ts ← V3 monitors implementation
events.ts ← V3 events implementation
...
types.ts ← V3-specific Zod schemas
index.ts ← V3 adapter factory
client.ts ← Shared (needs minor changes for Bearer auth in v3)
index.ts ← Exports active adapter based on detected version
The client.ts needs to support two auth modes:
- V1: Append
?token=xxxto query params - V3: Set
Authorization: Bearer xxxheader
Add a flag to ApiClient config or detect based on the active adapter.
For each module, implement the adapter interface:
- Auth: JSON body, dedicated refresh endpoint, handle
oneOflogin response (Token vs Code for 2FA) - Monitors: Map
snake_case→PascalCasefor internal types; fetch status separately from/monitor-status/{id} - Events: Map query params, handle simplified pagination, map
snake_caseresponse - Console Events: Transform
{count, date}[]intoRecord<MonitorId, count>if the v3 endpoint supports per-monitor breakdown (needs clarification) - Groups: Combine
/groups+/groups-monitorsinto the current grouped structure - Tags: Map
/events-tagsto the current batched tag lookup pattern - Server/System: Combine
/system/statusfor daemon check, load, and daemon statuses - Streaming: Map
/streams/{id}endpoints to the URL builder's expectations
During bootstrap/discovery:
- Try
GET /api/v3/host/getVersion— if it returns successfully, use V3 adapter - Fall back to
GET /api/host/getVersion.json— use V1 adapter - Store the detected version, create the appropriate adapter instance
- Pass the adapter to the app via context or store
For features missing from v3, the V3 adapter should:
| Missing Feature | Strategy |
|---|---|
| Timezone | Return Intl.DateTimeFormat().resolvedOptions().timeZone as fallback, or request it be added to v3 |
| Disk usage | Return null/unavailable, hide UI element, or request endpoint be added |
| Event archive | Check if v3 adds this field later; for now, disable archive UI when on v3 |
| Event sort/filter | Use client-side sorting if v3 doesn't support it server-side |
| Console events per monitor | Fetch all events and aggregate client-side, or request v3 add per-monitor grouping |
| ZMS streaming | Not needed if v3 MSE/WebRTC covers all cases; fall back to v3 streaming endpoints |
| PTZ dispatch | Request endpoint be added to v3 spec, or use controls API differently |
| Event image/video URLs | Use v3 streaming endpoints or request media URL construction be documented |
| Log detail fields | Display only available fields; gracefully degrade UI |
The url-builder.ts constructs ZMS, image, and video URLs from base URL + token query params. For v3:
- Stream URLs come from the
/streams/{id}endpoint (StreamEndpointswithwebrtc,hls,mjpegURLs) - Image URLs may need a new v3 endpoint or convention
- Token is no longer a query param — streaming endpoints likely use cookies or the JWT is embedded differently
Create a StreamUrlResolver interface with V1 and V3 implementations.
The work can be done incrementally per module:
- Auth (enables all other work)
- Monitors + Monitor Status (most visible)
- Events
- Server/System status
- Groups, Tags, Zones, States, Configs, Logs
- Streaming (largest change)
Each module can be migrated independently since the adapter interface keeps the app logic stable.
-
Console events format change — The v3
EventCountsResponsereturns{count, date}[]which doesn't match the current per-monitor breakdown. Needs clarification: does v3 support per-monitor filtering via query params? -
Streaming architecture — The move from ZMS CGI to MSE/WebRTC is not just an API change — it may require changes to the video player components. The adapter layer can abstract URL construction, but the playback mechanism itself may differ.
-
Event image/video access — No clear v3 equivalent for
index.php?view=imageorview=view_video. This is a gap that needs to be addressed in the v3 spec or via a different media serving approach. -
2FA support — V3's
LoginResponsecan return aCodevariant for 2FA. zmNg has no 2FA UI currently. The adapter should handle this gracefully (pass through or error). -
Permissions model — V3 adds
groups-permissionsandmonitors-permissionswhich don't exist in v1. The adapter interface should be designed to accommodate these later. -
Single
expire_invs separate access/refresh expiry — V3TokenResponsehas oneexpire_in. Need to determine if this applies to the access token only (likely) and what the refresh token lifetime is.