# integration.v1 — FROZEN public integration contract (subset of node-api.yaml). # Generated from the internal spec; guarded by tools/contract-freeze/openapi-subset-check.sh # and pinned in contracts/FROZEN.sha256. Do NOT edit v1 in place. openapi: 3.1.0 info: title: Embedda Integration API version: 1.0.0 description: >- integration.v1 — the FROZEN public integration surface of a Muralink/Embedda node: external event ingest (VMS/NVR webhooks, MQTT), gateway diagnostics, normalization mappings, the incident lifecycle (list/acknowledge/unacknowledge/resolve) and node status/capabilities. Every path here exists verbatim (method+route) in the internal node-api.yaml; tools/contract-freeze/openapi-subset-check.sh enforces that and contracts/FROZEN.sha256 pins this file. v1 never mutates: breaking changes ship as a side-by-side v2. Authentication in v1 is static bearer only (NodeApiBearer / EventGatewayBearer); an interactive login/session flow exists in the node but is NOT part of this frozen surface (label: ROADMAP for integration.v2). servers: - url: /api/v1 paths: /events/gateway/webhook: post: summary: Event Gateway webhook ingest description: > Accepts external VMS, NVR, analytics or automation events with the Event Gateway bearer token, normalizes them through the active mapping profile and appends one durable inbox row for Patrol consumption. security: - EventGatewayBearer: [] requestBody: required: true content: application/json: schema: type: object additionalProperties: true examples: generic: externalValue: ../examples/event-gateway/generic-webhook.json nx-witness: externalValue: ../examples/event-gateway/nx-witness-webhook.json milestone-xprotect: externalValue: ../examples/event-gateway/milestone-xprotect-webhook.json responses: '202': description: Event accepted and queued. content: application/json: schema: $ref: '#/components/schemas/EventGatewayWebhookAccepted' '400': description: Invalid JSON or unmappable payload. '401': description: Missing or invalid Event Gateway bearer token. '413': description: Payload exceeds `NEXUS_EVENT_GATEWAY_MAX_PAYLOAD_BYTES`. '503': description: Event Gateway token is not configured. /events/gateway/status: get: summary: Event Gateway status description: Operational status for inbound webhook, MQTT bridge and mapping storage. responses: '200': description: Current Event Gateway status. content: application/json: schema: $ref: '#/components/schemas/EventGatewayStatus' /events/gateway/mqtt/publish: post: summary: Event Gateway MQTT bridge publish description: > Bridge endpoint for an external MQTT subscriber sidecar. The sidecar posts the MQTT topic and JSON payload with the Event Gateway bearer token; node-api normalizes the payload through the same mapping profiles as webhooks and appends one durable inbox row for Patrol. security: - EventGatewayBearer: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/EventGatewayMqttPublish' responses: '202': description: MQTT payload accepted and queued. content: application/json: schema: $ref: '#/components/schemas/EventGatewayWebhookAccepted' '400': description: Invalid bridge envelope, topic, QoS or unmappable payload. '401': description: Missing or invalid Event Gateway bearer token. '413': description: Payload exceeds `NEXUS_EVENT_GATEWAY_MAX_PAYLOAD_BYTES`. '503': description: Event Gateway token is not configured. /events/gateway/mqtt/status: get: summary: Event Gateway MQTT status (bridge-post or live subscriber) description: > Operational status for MQTT ingestion. mode="bridge-post" (default): the broker_url/topics env are read only for display and events arrive via POST .../mqtt/publish. mode="subscriber" (R2, gated by NEXUS_EVENT_GATEWAY_MQTT_SUBSCRIBE): a live rumqttc EventLoop subscribes the topics and enqueues each Publish into the same durable inbox as the HTTP bridge; the "subscriber" object exposes connected/accepted/dropped/last_error so a fault (bad broker url, dropped payload, lost connection) is diagnosable, never silently degraded. responses: '200': description: Current MQTT bridge status. content: application/json: schema: $ref: '#/components/schemas/EventGatewayMqttStatus' /events/gateway/mappings: get: summary: Event Gateway VMS mappings description: 'Returns the active inbound VMS mapping document used to normalize external webhook payloads into canonical Nexus events. ' responses: '200': description: Current Event Gateway mappings. content: application/json: schema: type: object required: - path - mappings properties: path: type: string mappings: $ref: '#/components/schemas/EventGatewayMappings' put: summary: Replace Event Gateway VMS mappings description: Persists the mapping document used by inbound webhook ingestion. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/EventGatewayMappings' responses: '200': description: Mappings saved. '400': description: Invalid mapping document. /events/gateway/mappings/test: post: summary: Test an inbound VMS mapping description: 'Normalizes a sample payload through a selected profile without writing to the Event Gateway inbox or firing Patrol incidents. ' requestBody: required: true content: application/json: schema: type: object required: - payload properties: profile_id: type: string payload: type: object additionalProperties: true responses: '200': description: Mapping test result. content: application/json: schema: type: object required: - ok - request - mapping additionalProperties: true properties: ok: type: boolean request: type: object additionalProperties: true mapping: type: object additionalProperties: true '400': description: Payload cannot be mapped. '413': description: Payload exceeds `NEXUS_EVENT_GATEWAY_MAX_PAYLOAD_BYTES`. /incidents: get: summary: Incident queue description: Lists persisted incidents with optional lifecycle filters. parameters: - in: query name: state schema: type: string enum: - open - acknowledged - resolved - in: query name: since schema: type: integer format: int64 responses: '200': description: Incident queue envelope. content: application/json: schema: $ref: '#/components/schemas/IncidentEnvelope' /admin/incidents/{id}/acknowledge: post: summary: Acknowledge an incident security: - NodeApiBearer: [] parameters: - $ref: '#/components/parameters/IncidentId' requestBody: required: false content: application/json: schema: type: object additionalProperties: false properties: user_id: type: string responses: '200': description: Acknowledged incident row. content: application/json: schema: $ref: '#/components/schemas/Incident' '404': description: Incident not found. /admin/incidents/{id}/unacknowledge: post: summary: Clear incident acknowledgement security: - NodeApiBearer: [] parameters: - $ref: '#/components/parameters/IncidentId' responses: '200': description: Updated incident row. content: application/json: schema: $ref: '#/components/schemas/Incident' '404': description: Incident not found. /admin/incidents/{id}/resolve: post: summary: Resolve an incident security: - NodeApiBearer: [] parameters: - $ref: '#/components/parameters/IncidentId' responses: '200': description: Incident resolved. content: application/json: schema: $ref: '#/components/schemas/IncidentLifecycleResponse' '404': description: Incident not found. /status: get: summary: Node status description: Basic identity, backend, accelerator, and camera-pool mode. responses: '200': description: Current node status. content: application/json: schema: $ref: '#/components/schemas/WorldSnapshotEnvelope' required: - node_id - display_name - backend - accelerator - mode properties: node_id: type: string display_name: type: string backend: type: string enum: - hailo - gpu - cpu accelerator: type: string mode: type: string enum: - fixed - patrol /capabilities: get: summary: Node capabilities description: 'Monitor discovery endpoint. Reports the node role, backend, camera source capabilities, stream paths, world endpoint, supported operator actions, and factual security posture. ' responses: '200': description: Current node capabilities. content: application/json: schema: $ref: '#/components/schemas/NodeCapabilities' examples: local-npu: externalValue: ../examples/node-capabilities.json components: parameters: IncidentId: in: path name: id required: true schema: type: string minLength: 1 maxLength: 128 schemas: EventGatewayMappingProfile: type: object required: - id - name additionalProperties: true properties: id: type: string name: type: string vendor: type: - string - 'null' enabled: type: boolean field_paths: type: object additionalProperties: type: array items: type: string camera_map: type: object additionalProperties: type: string event_type_map: type: object additionalProperties: type: object required: - event_type additionalProperties: true properties: event_type: type: string severity: type: number minimum: 0 maximum: 1 confidence: type: number minimum: 0 maximum: 1 defaults: type: object additionalProperties: true EventGatewayMappings: type: object required: - schema_version - active_profile - profiles additionalProperties: false properties: schema_version: type: string enum: - nexus.event_gateway.mappings.v1 active_profile: type: string updated_at_ms: type: integer format: int64 profiles: type: array items: $ref: '#/components/schemas/EventGatewayMappingProfile' EventGatewayMqttPublish: type: object required: - topic - payload additionalProperties: false properties: topic: type: string minLength: 1 maxLength: 512 example: vms/site-a/security payload: oneOf: - type: object additionalProperties: true - type: string description: JSON object serialized as an MQTT text payload. profile_id: type: string description: Optional mapping profile override. qos: type: integer minimum: 0 maximum: 2 retain: type: boolean received_at_ms: type: integer format: int64 EventGatewayMqttStatus: type: object required: - schema_version - mode - configured - token_env - publish_path - status_path - auth - max_payload_bytes - inbox_path additionalProperties: true properties: schema_version: type: string enum: - nexus.event_gateway.mqtt_status.v1 mode: type: string enum: - bridge-post - subscriber subscribe_gate_env: type: string enum: - NEXUS_EVENT_GATEWAY_MQTT_SUBSCRIBE subscriber: type: object nullable: true description: 'Present (non-null) when the subscriber gate is ON — live state of the rumqttc EventLoop. Null in bridge-post mode. ' properties: broker: type: string topics: type: array items: type: string connected: type: boolean accepted: type: integer dropped: type: integer last_error: type: string nullable: true configured: type: boolean token_env: type: string enum: - NEXUS_EVENT_GATEWAY_TOKEN publish_path: type: string enum: - /api/v1/events/gateway/mqtt/publish status_path: type: string enum: - /api/v1/events/gateway/mqtt/status auth: type: string enum: - EventGatewayBearer max_payload_bytes: type: integer minimum: 1024 broker_url_env: type: string broker_url_configured: type: boolean topics_env: type: string topics_configured: type: boolean inbox_path: type: string last_accepted_event: type: - object - 'null' additionalProperties: true EventGatewayStatus: type: object required: - schema_version - configured - token_env - inbox_path - max_payload_bytes - mappings_path - ingest_path - mappings_endpoint additionalProperties: true properties: schema_version: type: string enum: - nexus.event_gateway.status.v1 configured: type: boolean token_env: type: string enum: - NEXUS_EVENT_GATEWAY_TOKEN max_payload_bytes: type: integer minimum: 1024 inbox_path: type: string inbox_exists: type: boolean inbox_writable: type: boolean inbox_bytes: type: integer minimum: 0 queued_lines: type: integer minimum: 0 last_accepted_event: type: - object - 'null' additionalProperties: true mappings_path: type: string mappings_exists: type: boolean mappings_readable: type: boolean last_mapping_error: type: - string - 'null' ingest_path: type: string admin_replay_path: type: string mappings_endpoint: type: string EventGatewayWebhookAccepted: type: object required: - accepted - queued - queue_id - inbox_path additionalProperties: true properties: accepted: type: boolean queued: type: boolean queue_id: type: string inbox_path: type: string GridCellId: type: object required: - col - row properties: col: type: integer minimum: 0 row: type: integer minimum: 0 Incident: type: object required: - incident_id - created_at additionalProperties: true properties: incident_id: type: string camera_id: type: - string - 'null' site_id: type: - string - 'null' created_at: type: number acknowledged_at: type: - number - 'null' resolved_at: type: - number - 'null' acknowledged_by: type: - string - 'null' severity: type: - number - 'null' minimum: 0 maximum: 1 threat_level: type: - string - 'null' summary: type: - string - 'null' details: type: - object - 'null' additionalProperties: true IncidentEnvelope: type: object required: - count - incidents additionalProperties: true properties: count: type: integer minimum: 0 incidents: type: array items: $ref: '#/components/schemas/Incident' IncidentLifecycleResponse: type: object required: - ok - incident_id additionalProperties: true properties: ok: type: boolean incident_id: type: string NodeActionCapabilities: type: object required: - activate_camera - set_mode - set_producer - auto_ptz - ptz_presets - perception_settings - brain_profiles - commands_api properties: activate_camera: type: boolean set_mode: type: boolean set_producer: type: boolean auto_ptz: type: boolean ptz_presets: type: boolean perception_settings: type: boolean brain_profiles: type: boolean commands_api: type: boolean NodeCameraCapabilities: type: object required: - count - active_camera_id - mode - rtsp_supported - configured_rtsp_count - onvif_supported - configured_onvif_count - dvr_nvr_supported - auto_ptz_supported - auto_ptz_enabled_count - ptz_config_supported properties: count: type: integer minimum: 0 active_camera_id: type: string mode: type: string enum: - fixed - patrol rtsp_supported: type: boolean configured_rtsp_count: type: integer minimum: 0 onvif_supported: type: boolean configured_onvif_count: type: integer minimum: 0 dvr_nvr_supported: type: boolean description: DVR/NVR channels are supported when exposed as RTSP streams. auto_ptz_supported: type: boolean auto_ptz_enabled_count: type: integer minimum: 0 ptz_config_supported: type: boolean NodeCapabilities: type: object required: - summary - cameras - streams - world - actions - security properties: summary: $ref: '#/components/schemas/NodeSummary' cameras: $ref: '#/components/schemas/NodeCameraCapabilities' streams: $ref: '#/components/schemas/NodeStreamCapabilities' world: $ref: '#/components/schemas/NodeWorldCapabilities' actions: $ref: '#/components/schemas/NodeActionCapabilities' security: $ref: '#/components/schemas/NodeSecurityCapabilities' NodeSecurityCapabilities: type: object required: - profile - transport - key_establishment - node_identity - artifact_signatures - api_bearer_required - admin_bearer_required - descriptor_signing_supported - descriptor_expires_at properties: profile: type: string enum: - classical - hybrid-pqc - pqc-required - unknown profile_self_reported: type: boolean default: false description: > True when the security-profile fields above carry values DECLARED by node config rather than measured — consoles must badge them as "declared, unverified" (honesty rule). The bearer fields reflect real enforced token config and are never self-reported. transport: type: string description: Reported transport profile. `not_reported` means node-api cannot observe negotiated TLS. key_establishment: type: array items: type: string description: Advertised or configured KEM/key-establishment algorithms. node_identity: type: array items: type: string description: Advertised or configured node identity signature algorithms. artifact_signatures: type: array items: type: string description: Advertised or configured artifact signature algorithms. api_bearer_required: type: boolean admin_bearer_required: type: boolean descriptor_signing_supported: type: boolean descriptor_expires_at: type: - string - 'null' NodeStreamCapabilities: type: object required: - events_ws_path - video_ws_path - go2rtc_path - h264_video_supported properties: events_ws_path: type: string video_ws_path: type: string go2rtc_path: type: string h264_video_supported: type: boolean NodeSummary: type: object required: - node_id - display_name - role - backend - accelerator - mode - version properties: node_id: type: string display_name: type: string role: type: string enum: - inference - monitor backend: type: string enum: - hailo - gpu - cpu - unknown accelerator: type: string mode: type: string enum: - fixed - patrol version: type: string NodeWorldCapabilities: type: object required: - endpoint - available - site_frame_configured - snapshot_sync_supported - pheromone_sync_supported properties: endpoint: type: string available: type: boolean space_endpoint: type: string default: '' space_graph_endpoint: type: string default: '' space_frames_endpoint: type: string default: '' space_fields_endpoint: type: string default: '' space_fabric_endpoint: type: string default: '' space_config_supported: type: boolean default: false space_etag_supported: type: boolean default: false site_frame_configured: type: boolean snapshot_sync_supported: type: boolean pheromone_sync_supported: type: boolean WorldCell: type: object required: - id - occupancy - free - unknown - entropy - confidence - risk additionalProperties: true properties: id: $ref: '#/components/schemas/GridCellId' occupancy: type: number free: type: number unknown: type: number entropy: type: number confidence: type: number staleness_s: type: number risk: type: number navigability: type: number eig: type: number last_observed_ms: type: - integer - 'null' format: int64 WorldGrid: type: object required: - v - node_id - seq - observed_at_ms - cols - rows - cells additionalProperties: true properties: v: type: integer node_id: type: string camera_id: type: - string - 'null' seq: type: integer format: int64 observed_at_ms: type: integer format: int64 cols: type: integer minimum: 1 rows: type: integer minimum: 1 extent: type: array minItems: 4 maxItems: 4 items: type: number cells: type: array items: $ref: '#/components/schemas/WorldCell' global_entropy: type: - number - 'null' mean_eig: type: - number - 'null' mean_risk: type: - number - 'null' next_best: type: - object - 'null' WorldSnapshotEnvelope: type: object required: - ts_ms - field_status additionalProperties: true properties: ts_ms: type: integer format: int64 description: Producer timestamp for the snapshot envelope. field_status: type: string enum: - converged - ready - camera_motion - empty - unavailable - degraded description: '`camera_motion` is an honest non-ready state while a PTZ/camera move is in progress; clients should not treat the world grid as stable. ' field_hash: type: - string - 'null' entropy: type: - number - 'null' world: oneOf: - $ref: '#/components/schemas/WorldGrid' - type: 'null' securitySchemes: NodeApiBearer: type: http scheme: bearer bearerFormat: opaque description: Internal/operator token from `NEXUS_NODE_API_TOKEN` or admin token. EventGatewayBearer: type: http scheme: bearer bearerFormat: opaque description: External webhook token from `NEXUS_EVENT_GATEWAY_TOKEN`. WorldApiBearer: type: http scheme: bearer bearerFormat: opaque description: 'Partner token declared in `NEXUS_WORLD_API_KEYS` with Nexus scopes such as `world.read`, `space.read`, `evidence.write`, `intent.write`, or `actuator.execute`. '