Skip to content

Latest commit

 

History

History
404 lines (320 loc) · 19.9 KB

File metadata and controls

404 lines (320 loc) · 19.9 KB

Matter API Contract — Domio ↔ Indigo Plugin

Status: Authoritative for v1 Version: 1.3 Last updated: 2026-06-10

This document is the single source of truth for the HTTP API between Domio (iOS) and the indigo-matter plugin running on the Indigo Server. Both Domio and the plugin implementations MUST conform to this spec. Any change requires a co-ordinated update on both sides — treat as a versioned interface, not an implementation detail.

This is not the matter-server protocol — that's a separate seam owned by the plugin (see plugin Implementation Notes for details).

Transport (v1.1, changed from v1.0): the plugin does not run its own HTTP server. It is served by the Indigo Web Server (IWS) as a set of hidden-action handlers, reached at …/message/com.simons-plugins.indigo-matter/{handler}/…. This is the idiomatic Indigo mechanism (the same one Domio already uses for the device-history and HTML-pages APIs — see domio-code DeviceHistoryService / HtmlPageService), and it rides the Reflector for remote access with the same Bearer API key Domio already uses. Consequences vs v1.0: base path is /message/{bundleId}/{handler} (not /matter/…); only GET and POST are available (so decommission is a POST, not DELETE); and authentication is enforced by Indigo/Reflector before the handler runs — the plugin never sees or validates the credential. The job/state/error semantics below are unchanged from v1.0.

IWS path-arg quirk (important): IWS populates action.props["file_path"] (the trailing path components) only for GET requests, never for POST. So a POST endpoint cannot receive an id in the URL path — it must take the id as a query item. This is why decommission uses ?nodeId=… (v1.2), not /decommission/{nodeId} as v1.1 specified.

Note for the Domio implementer: build these calls exactly like the existing IndigoRESTService.pluginRequest() / DeviceHistoryService.performRequest() — resolve the reflector, build {base}/message/com.simons-plugins.indigo-matter/{handler}, add Authorization: Bearer {credential}. Pass all parameters, including the nodeId for POST handlers, as URL query items; a trailing /{pathArg} works only for GET (e.g. GET …/commission/{jobId}).

1. Conventions

  • All responses use JSON (Content-Type: application/json).
  • Request parameters are passed as URL query items (for both GET and POST), matching the existing Indigo plugin-handler convention. Bodies are not required; the plugin reads url_query_args (and body_params if form-encoded).
  • Timestamps are ISO 8601 with timezone offset (e.g. 2026-05-15T14:23:45+01:00).
  • IDs:
    • nodeId — string, the Matter node identifier as managed by the Indigo fabric. Stored as a string to avoid 64-bit JSON number issues.
    • jobId — string UUID v4, generated by the plugin.
    • indigoDeviceId — integer, the standard Indigo device ID.
  • Base path: every endpoint is rooted at …/message/com.simons-plugins.indigo-matter/{handler}.
    • Local: https://{indigo-host}:8176/message/com.simons-plugins.indigo-matter/{handler} — IWS serves HTTPS on 8176 (self-signed cert); local requests authenticate with Indigo's Digest auth.
    • Remote (Reflector): https://{reflector}.indigodomo.net/message/com.simons-plugins.indigo-matter/{handler} — Bearer API key. This is the path Domio uses.
  • Trailing path components after {handler} are positional arguments on GET only — the plugin reads them from action.props["file_path"], which IWS populates for GET but not POST. E.g. GET …/commission/{jobId} puts jobId as the path arg. A POST endpoint must instead carry its id as a query item (see §3.4).

2. Authentication

Because the endpoints are served by IWS over the Reflector, they reuse Indigo's existing Reflector / API-key authentication. Domio includes the same Authorization: Bearer {key} header it already sends for Indigo REST/WebSocket calls. Indigo enforces the credential before the request reaches the plugin handler — the plugin does not implement its own auth check. A missing/invalid credential is rejected by Indigo with its standard 401 Unauthorized (not generated by this plugin). No new credential type for v1.

3. Endpoints

3.1 GET …/message/com.simons-plugins.indigo-matter/status

Health check. Domio calls this at the start of every commissioning flow as a precheck.

Request: no parameters.

Response 200:

{
  "ready": true,
  "controllerVersion": "2026.0.1",
  "matterServerReachable": true,
  "matterServerVersion": "0.6.2",
  "fabricId": "0x0123456789ABCDEF",
  "nodeCount": 7,
  "bleAvailable": false,
  "uptime": 14523
}
Field Type Notes
ready bool True only if the plugin is fully initialised and matter-server is reachable.
controllerVersion string The indigo-matter plugin version (YYYY.R.P).
matterServerReachable bool False if the WebSocket to matter-server is currently down.
matterServerVersion string matter-server's reported version. May be unknown if WS just connected.
fabricId string Hex-encoded 64-bit fabric ID.
nodeCount int Number of Matter nodes currently on the Indigo fabric.
bleAvailable bool Whether the host Mac has BLE available to matter-server. Affects fallback commissioning paths but not the standard Domio flow.
uptime int Plugin uptime in seconds.

Response 503: if the plugin is loaded but matter-server is unreachable.

{
  "ready": false,
  "matterServerReachable": false,
  "error": "matter_server_unreachable",
  "message": "Cannot reach matter-server at ws://localhost:5580"
}

3.2 POST …/message/com.simons-plugins.indigo-matter/commission

Submit a setup code for commissioning into the Indigo fabric as a second admin (multi-admin handoff; workspace ADR-0004). The setup code Domio forwards here can come from either supported flow:

  • C4 — Apple Home / Alexa share (default). The device is commissioned as admin 1 by the user's existing ecosystem (Apple Home, Alexa, …), which opens a pairing/commissioning window from its own fabric. Domio captures the resulting setup code (manual entry or scan) and forwards it here; domioNodeId and expectedFabricSlots are absent.
  • C3 — Domio direct commissioning. Domio commissions the factory-fresh device onto its own transient fabric via MatterSupport/MatterExtension, then opens a commissioning window itself and auto-forwards the fresh setup code — no third-party ecosystem involved, and the device never joins Apple Home. Domio additionally sends domioNodeId (its own fabric's node id, for log correlation) and may send expectedFabricSlots (a hint that the device should still have at least N fabric slots free after Indigo joins).

Either way the plugin's join is identical: a setup code is a setup code, indistinguishable by source, and joins over IP as the Indigo fabric's second (or third, under C3) admin.

Request parameters (URL query items):

Param Type Required Notes
setupCode string yes Either MT:... QR payload or 11-digit numeric pairing code, as returned by Domio's OpenCommissioningWindow.
discriminator int no Matter discriminator. If absent, the plugin extracts it from the setup code.
suggestedName string yes User-chosen friendly name. Applied to all created Indigo devices (with " (endpoint N)" suffix for multi-endpoint nodes).
suggestedRoom string no Indigo room name. Devices are placed here on creation. If the room doesn't exist, the plugin creates it.
domioNodeId string no Domio's nodeId for this device on its own fabric. Logged for correlation only.
expectedFabricSlots int no Hint from Domio (C3 only) that the device should still have at least N fabric slots free after this join. The plugin computes available slots as the interviewed node's Operational Credentials SupportedFabrics − CommissionedFabrics and logs a warning (job still succeeds) if that's fewer than N, or stays silent if the node's interview snapshot didn't expose those attributes.

Response 202: commissioning started.

{
  "jobId": "8c9d3a14-7e1b-4f8e-9b2a-1d5c8e9f3a7b",
  "estimatedDurationSeconds": 30
}

Response 409: an identical request is already in flight (deduplication by setupCode).

{
  "error": "duplicate",
  "existingJobId": "8c9d3a14-7e1b-4f8e-9b2a-1d5c8e9f3a7b",
  "message": "Commissioning for this setup code is already in progress"
}

Response 400: malformed request.

{
  "error": "invalid_setup_code",
  "message": "Setup code must be either MT:... QR payload or 11-digit numeric"
}

Response 503: matter-server is unreachable (v1.3). The job is not created — the request would otherwise be accepted (202) only for the worker to fail with a generic internal_error at its first WebSocket call. Domio should treat this like the status-precheck 503 and prompt the user to retry.

{
  "error": "matter_server_unreachable",
  "message": "Not connected to matter-server; check it is running and reachable, then retry"
}

3.3 GET …/message/com.simons-plugins.indigo-matter/commission/{jobId}

Poll commissioning job status. jobId is the trailing path component. Domio polls every 1s, with a 120s soft timeout.

Response 200 (in progress):

{
  "jobId": "8c9d3a14-7e1b-4f8e-9b2a-1d5c8e9f3a7b",
  "status": "reading_descriptors",
  "progress": 0.6,
  "message": "Discovering device capabilities…",
  "startedAt": "2026-05-15T14:23:45+01:00",
  "elapsedSeconds": 12
}

status is one of:

Status Meaning progress range
pending Job accepted, not yet started 0.0
commissioning matter-server is performing PASE/CASE handshake 0.1–0.5
reading_descriptors Interviewing the node for clusters/endpoints 0.5–0.7
creating_devices Plugin is creating Indigo devices and subscribing 0.7–0.95
success Done 1.0
failed Terminal failure

Response 200 (success):

{
  "jobId": "8c9d3a14-7e1b-4f8e-9b2a-1d5c8e9f3a7b",
  "status": "success",
  "progress": 1.0,
  "result": {
    "nodeId": "0xABCDEF0123456789",
    "vendorId": 4488,
    "productId": 4660,
    "vendorName": "TP-Link",
    "productName": "Tapo P125M",
    "indigoDeviceIds": [1234567890],
    "primaryDeviceId": 1234567890,
    "endpointCount": 1
  }
}

result.primaryDeviceId is nullable and result.indigoDeviceIds may be empty (v1.3): primaryDeviceId is null when the device joined the fabric but exposes no cluster the plugin maps yet — the device is on the fabric and will gain Indigo devices when support is added. The job still reports success. Domio MUST handle a null primaryDeviceId (e.g. skip navigating to a device detail view).

Response 200 (failed):

{
  "jobId": "8c9d3a14-7e1b-4f8e-9b2a-1d5c8e9f3a7b",
  "status": "failed",
  "progress": 0.4,
  "error": {
    "code": "commissioning_failed",
    "message": "Device did not respond to PASE handshake within 30 seconds",
    "matterErrorCode": 50
  }
}

Response 404: unknown jobId (expired or never created). Plugin retains jobs for 15 minutes after terminal state.

{ "error": "job_not_found" }

matterErrorCode is matter-server's reported error code, which for v0.6.2 may or may not match Matter spec error codes — verify against matter-server's source. The code and message fields are the authoritative parts; matterErrorCode is advisory.

Standard error codes:

Code Meaning
commissioning_failed matter-server returned a failure during commissioning
commissioning_timeout matter-server did not finish commissioning within the plugin's commission timeout (300s). The device may still join — matter-server keeps commissioning in the background. Check Indigo before retrying; see the reconcile note below. (v1.3, additive)
device_unreachable Device couldn't be reached on the network
pase_failed PASE handshake failed (usually wrong setup code or device already commissioned)
case_failed CASE session establishment failed
interview_failed Couldn't read device descriptors after commissioning
unsupported_device Device has no clusters mappable by v1 plugin
internal_error Plugin or matter-server internal error; check logs

Timeout reconcile (v1.3): a job that failed with commissioning_timeout is not necessarily over — matter-server keeps commissioning in the background (observed ~124s real-world vs the old 60s client timeout). If the node joins within ~5 minutes of the timeout, the plugin reconciles: it applies the job's suggestedName/ suggestedRoom to the created Indigo devices and flips the job back to success with the full result payload. A client still polling past its own soft timeout will therefore see failed → (possibly creating_devices) → success. Clients that stopped polling should treat commissioning_timeout as "may still finish — check Indigo", which matches Domio's existing 120s soft-timeout message. The plugin's job-level commission deadline is 300s and may legitimately outlive Domio's 120s poll window.

3.4 POST …/message/com.simons-plugins.indigo-matter/decommission?nodeId={nodeId}

Decommission a device. nodeId is a query parameter. Removes the plugin's fabric from the device (sends RemoveFabric via matter-server) and deletes the associated Indigo devices. In the share model this removes only Indigo's administration — the device remains commissioned in any other fabric (e.g. Apple Home).

v1.0 specified this as DELETE /matter/devices/{nodeId}. IWS exposes only GET/POST to plugin handlers, so it became a POST in v1.1. In v1.2 the id moved from a path component to a query param (?nodeId=…): IWS does not deliver trailing path components to POST handlers, so the v1.1 /{nodeId} path form could never work. Semantics are identical and idempotent. (The plugin still accepts a /{nodeId} path component as a fallback for any transport that supplies one.)

Request parameters (URL query items):

Param Type Required Notes
nodeId string yes Matter node id (0x… hex or decimal), as returned by commission.

Response 200:

{
  "nodeId": "0xABCDEF0123456789",
  "removedIndigoDeviceIds": [1234567890],
  "fabricRemoved": true
}

fabricRemoved is false if the device was unreachable; in that case the plugin removes its local state but the device may retain stale fabric credentials until factory-reset.

Response 404: unknown nodeId.

3.5 GET …/message/com.simons-plugins.indigo-matter/diagnostics/{nodeId}

Diagnostic snapshot for a single device. nodeId is the trailing path component (a ?nodeId=… query param is also accepted). Used by a future Domio device-detail view.

Response 200:

{
  "nodeId": "0xABCDEF0123456789",
  "reachable": true,
  "lastSeen": "2026-05-15T14:25:01+01:00",
  "vendorId": 4488,
  "productId": 4660,
  "vendorName": "TP-Link",
  "productName": "Tapo P125M",
  "softwareVersion": "1.2.3",
  "hardwareVersion": "1.0",
  "fabrics": [
    { "fabricIndex": 1, "fabricId": "0x0123456789ABCDEF", "label": "Indigo" },
    { "fabricIndex": 2, "fabricId": "0xFEDCBA9876543210", "label": "Domio" }
  ],
  "network": {
    "type": "wifi",
    "rssi": -52,
    "ipv6": ["fe80::1234:5678:9abc:def0"]
  },
  "endpoints": [
    {
      "endpointId": 1,
      "deviceType": "OnOffPlugInUnit",
      "clusters": ["OnOff", "Descriptor", "Identify"],
      "indigoDeviceId": 1234567890
    }
  ]
}

For Thread devices, network.type is "thread" with fields routerId, linkQualityIn, linkQualityOut, borderRouter.

4. Polling Semantics

  • Domio SHOULD poll at 1s intervals during active commissioning.
  • Domio SHOULD reduce to 5s intervals if the same status value repeats for >10s.
  • Plugin MUST NOT rate-limit polling in v1.
  • Plugin MUST handle concurrent polls for the same jobId without race conditions.

5. Idempotency

  • POST …/commission is idempotent on setupCode: identical setup code in flight returns the existing jobId (409).
  • After a job reaches terminal state, the same setup code is treated as new (since the device's commissioning window has likely expired anyway).
  • POST …/decommission/{nodeId} is idempotent: decommissioning an unknown node returns 404 but is harmless.

6. Rate Limits

None in v1. Single-user system. Plugin SHOULD log abnormal traffic patterns but not enforce limits.

7. Versioning

This contract is v1.3. Changes from v1.2 (all clarifications/additive, no transport change):

  • §3.3: new additive error code commissioning_timeout — the commission RPC to matter-server timed out (300s) but the device may still join; a job that failed this way can later flip back to success when the node arrives (timeout reconcile, see §3.3).
  • §3.3: result.primaryDeviceId is documented as nullable and result.indigoDeviceIds as possibly emptynull/empty when the commissioned device exposes no cluster the plugin maps yet (the job still reports success).
  • §3.2: POST …/commission returns 503 matter_server_unreachable (instead of accepting the job) when the plugin's WebSocket to matter-server is down.
  • §3.1: the status 503 body's error/message envelope fields are confirmed as required (they were documented in v1.2 but not emitted by the plugin).

Changes from v1.1 (v1.2): decommission's nodeId moved from a path component to a ?nodeId= query param (the path form never worked — IWS drops trailing path components on POST); diagnostics additionally accepts ?nodeId=; the local base URL is HTTPS, not HTTP. v1.1 itself was a transport change from v1.0 (semantics unchanged). Breaking changes require a version bump and a transition period where both versions are supported. Additive changes (new optional params, new handlers) do not require a version bump.

Domio includes X-Matter-API-Version: 1.3 in every request. The plugin SHOULD warn (HTTP 200, with warning field) if Domio's version is unrecognised.

8. Future Endpoints (Not in v1)

For forward planning, these handlers are reserved:

  • POST …/open-commissioning-window/{nodeId} — generate a new setup code for sharing to another fabric.
  • POST …/identify/{nodeId} — trigger Matter Identify cluster (blink the device).
  • GET …/fabrics — list fabrics across all nodes.
  • POST …/backup — create a fabric backup zip.

9. Example End-to-End Sequence

Domio                                              Plugin (via IWS / Reflector)

GET …/message/com.simons-plugins.indigo-matter/status
                                                    200 { ready: true, ... }

[Domio invokes MatterAddDeviceRequest, iOS handles
 commissioning into Domio's fabric. Result: nodeId 0x1234...]

[Domio sends OpenCommissioningWindow command to device.
 Setup code generated: "12345678901". Window valid 180s.]

POST …/message/com.simons-plugins.indigo-matter/commission
     ?setupCode=12345678901&suggestedName=Office%20Fan&suggestedRoom=Office
                                                    202 { jobId: "8c9d..." }

GET …/message/com.simons-plugins.indigo-matter/commission/8c9d...
                                                    200 { status: "commissioning", progress: 0.3 }
GET …/commission/8c9d...                            (1s later)
                                                    200 { status: "reading_descriptors", progress: 0.6 }
GET …/commission/8c9d...                            (1s later)
                                                    200 { status: "creating_devices", progress: 0.9 }
GET …/commission/8c9d...                            (1s later)
                                                    200 { status: "success", result: { ... } }

[Domio refreshes Indigo device list, navigates to indigoDeviceId 1234567890]