{"components":{"schemas":{"AnalyticsFieldsConfig":{"description":"Field-level analytics configuration for grouping and aggregation.","properties":{"avg":{"type":["string","null"]},"groupBy":{"type":["string","null"]},"peak":{"type":["string","null"]},"steps":{"description":"Ordered event types defining a session funnel (`sessionFunnel`\nmetric only). 2-8 steps; a session counts toward step N when it\nemitted all steps 1..N in order within `windowSeconds`.","example":["PRODUCT_VIEW","CART_ADD","CHECKOUT","PURCHASE"],"items":{"type":"string"},"type":["array","null"]},"sum":{"type":["string","null"]},"sumEventTypes":{"items":{"type":"string"},"type":["array","null"]},"windowSeconds":{"description":"Funnel time window in seconds (`sessionFunnel` metric only).\n60-86400, default 3600.","example":3600,"format":"int32","maximum":86400,"minimum":60,"type":["integer","null"]}},"type":"object"},"AnalyticsGranularity":{"description":"Time granularity for analytics queries.","enum":["day","hour"],"type":"string"},"AnalyticsInteractionType":{"description":"Interaction type filter for analytics queries.\n\n`All` (the default when omitted) matches every event; `Interactive`\nrestricts to events produced by a user interaction.","enum":["all","interactive"],"type":"string"},"AnalyticsQueryMeta":{"description":"Metadata returned alongside analytics query results.","properties":{"dateFrom":{"type":"string"},"dateTo":{"type":"string"},"granularity":{"type":"string"},"metrics":{"items":{"type":"string"},"type":"array"},"mode":{"$ref":"#/components/schemas/AnalyticsQueryMode"}},"required":["dateFrom","dateTo","granularity","mode"],"type":"object"},"AnalyticsQueryMode":{"description":"How a query's metrics were resolved.","enum":["explicit","schema"],"type":"string"},"AnalyticsQueryRequest":{"description":"Request body for the unified analytics query endpoint.","properties":{"cardIds":{"description":"Per-card opt-in filter. Each value is a 16-char hex card id derived\nfrom the card schema. Cards whose id is NOT in the set come back with\nschema metadata + `cardData: null` and are not processed server-side.\nAbsent = process every card in the active groups.","example":["ce964eefdf55858b"],"items":{"type":"string"},"type":["array","null"]},"cardPagination":{"additionalProperties":{"$ref":"#/components/schemas/CardPaginationParams"},"description":"Per-card pagination keyed by `cardId`.\n\nOpt-in pagination for list-shaped cards (e.g. DataMatrix variants).\nWhen a card's `cardId` appears in this map, the response `cardData`\nfor that card will contain a page slice plus a `pagination` meta\nobject. Absent or unknown `cardId`s return the full result set.\n\n`cardId` is a stable 16-char hex derived from the card schema on the\nserver; treat it as opaque and source it from a prior\n`schema=true` response.","propertyNames":{"type":"string"},"type":["object","null"]},"dateFrom":{"description":"Start date (YYYY-MM-DD)","example":"2026-03-01","type":"string"},"dateTo":{"description":"End date (YYYY-MM-DD)","example":"2026-03-31","type":"string"},"eventType":{"description":"Event type filter","type":["string","null"]},"fields":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/AnalyticsFieldsConfig","description":"Field-level analytics configuration"}]},"granularity":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/AnalyticsGranularity","description":"Time granularity"}]},"groupIndex":{"description":"Positional tab selector (0-based). Same semantics as `groups` but\naddressed by index, so the caller can send it without knowing any\ngroup IDs yet.\n\nMutually exclusive with `groups`: if both are supplied, `groupIndex`\nwins. Out-of-range indices fall back to the `groups` filter (or\nprocess-all) rather than erroring\nout -- a primer that picks the wrong tab should still render.\n\nDefensive upper bound: 50. Realistic schemas have <10 groups.","example":0,"format":"int32","maximum":50,"minimum":0,"type":["integer","null"]},"groups":{"description":"Opt-in tab filter. Each value is a 16-char hex group id derived from\nthe schema group's `name`.\n\nWhen present, groups whose id is NOT in the set appear in the\nresponse with their schema metadata (name, groupId, columnsCount,\netc.) but every card has `cardData: null`. This lets the caller render\ntab navigation while skipping aggregation + enrichment for the\nnon-active tabs. Absent means \"process all groups\".","example":["0efc2e6be4c23b9a"],"items":{"type":"string"},"type":["array","null"]},"hasRealtimeCard":{"description":"Hint that the request targets a realtime card. Shortens the response\ncache TTL so live device status is not masked. Defaults to `false`\nwhen absent.","type":["boolean","null"]},"include":{"description":"Opt-in card kinds the caller wants rendered inline. Cards not in the\nlist return `cardData: null`, keeping the default `/query` fast for\ntenants where only a subset of tabs are actually viewed.\n\nRecognized values (more may be added over time): `\"monitoring\"`.\nUnknown values return HTTP 400.","example":["monitoring"],"items":{"type":"string"},"type":["array","null"]},"interactionType":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/AnalyticsInteractionType","description":"Interaction type filter"}]},"locationFanout":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/LocationFanoutParams","description":"Server-side per-LOCATION fanout for the Store Breakdown tab. When\npresent (tenant scope + `schema: true`), the server resolves the\nrequested LOCATION spaces and renders the designated group's cards\nonce per location. The response's fanout group carries a `locations`\nblock alongside its `cards`."}]},"metrics":{"description":"Explicit list of metrics to fetch (mutually exclusive with schema).\n\nCore metrics (Cosmos day aggregates, ClickHouse with `source: \"live\"`):\n`events`, `sessions`, `nps`, `eventsFlow`, `products`, `categories`,\n`purchases`, `purchaseProducts`, `qrCodes`, `media`.\n\nField analytics (require `source: \"live\"` plus a `fields` object):\n`fieldBreakdown`, `fieldBreakdownByDay`, `sessionFunnel`,\n`fieldWeighted`, `fieldTrend`, `fieldRawEvents`, `multiEventByHour`,\n`hourlySummary`.\n\nMoney scale: the `revenue` (purchases) and `price` (purchaseProducts)\nfields are integers in 1/1000 of the major currency unit (e.g.\n`15990` = 15.99 EUR). Divide by 1000 for display; the `currency`\nfield on each row names the ISO 4217 currency.","items":{"type":"string"},"type":["array","null"]},"schema":{"description":"If true, resolve metrics from the installation's analytics schema","type":["boolean","null"]},"source":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/AnalyticsSource","description":"Data source routing"}]},"timezone":{"description":"Optional client-supplied timezone override. When present, the server\nuses this IANA zone verbatim for all hour/date bucketing (ClickHouse\n`toTimezone`) and skips the tenant/space resolver. When absent, the\nserver resolves the effective timezone via the tenant + space\nhierarchy and reports it back in `resolvedTimezone`.","example":"Asia/Tokyo","maxLength":64,"type":["string","null"]}},"required":["dateFrom","dateTo"],"type":"object"},"AnalyticsQueryResponse":{"description":"Response from the unified analytics query endpoint.\n\nKept for OpenAPI documentation via `ToSchema`. The gateway does not\ndeserialize upstream responses — they are passed through as raw bytes.","properties":{"data":{"description":"Metric results keyed by metric name"},"errors":{"description":"Per-metric failures from partial-success responses. Present only when\nat least one requested metric failed to resolve; keys are metric names,\nvalues are error messages."},"meta":{"$ref":"#/components/schemas/AnalyticsQueryMeta","description":"Query metadata"}},"required":["data","meta"],"type":"object"},"AnalyticsRankKeyColumn":{"description":"Which row-id column the orderedKeys reference. Maps directly to the\nDataMatrix card's `dataSource`.","enum":["spaceId","installationId","deviceId","rootSpaceId"],"type":"string"},"AnalyticsRankMeta":{"description":"Metadata returned alongside an analytics rank response.","properties":{"dateFrom":{"type":"string"},"dateTo":{"type":"string"},"direction":{"$ref":"#/components/schemas/AnalyticsSortDirection"},"sortField":{"type":"string"},"source":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/AnalyticsSource","description":"Echoed `source`; `null` when omitted by the caller."}]},"total":{"format":"int64","minimum":0,"type":"integer"}},"required":["dateFrom","dateTo","total","sortField","direction"],"type":"object"},"AnalyticsRankRequest":{"description":"Request body for the `/analytics/rank` endpoint.\n\nGiven a single DataMatrix card and a sort spec, returns the full ordered\nlist of row keys for the requested range, plus a `total`. Subsequent\npaged `/analytics/query` requests for the same `(cardId, sortField,\ndirection)` tuple reuse the result so only the first call pays the\ncompute cost.","properties":{"cardId":{"description":"Stable 16-char hex card id (`computeCardId` output) identifying the\nDataMatrix card to rank. Caller sources it from a prior `schema=true`\nresponse.","example":"ce964eefdf55858b","maxLength":16,"minLength":16,"type":"string"},"dateFrom":{"description":"Start date (YYYY-MM-DD).","example":"2026-03-01","type":"string"},"dateTo":{"description":"End date (YYYY-MM-DD).","example":"2026-03-31","type":"string"},"groupId":{"description":"Optional 16-char hex group id; narrows the schema walk to the named\ngroup when the same `cardId` exists in multiple groups (rare).","example":"0efc2e6be4c23b9a","maxLength":16,"minLength":16,"type":["string","null"]},"groupIndex":{"description":"Positional alternative to `groupId`. Mutually informative — when both\nare sent, `groupId` wins (matches the `/query` route's precedence).","example":0,"format":"int32","maximum":50,"minimum":0,"type":["integer","null"]},"sort":{"$ref":"#/components/schemas/AnalyticsSort","description":"Sort spec — required. Defines the order of the returned keys."},"source":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/AnalyticsSource","description":"Optional data source routing (currently `live` only)."}]},"timezone":{"description":"Optional client-supplied IANA timezone override; mirrors the same\nfield on `/analytics/query`.","example":"Asia/Tokyo","maxLength":64,"type":["string","null"]}},"required":["dateFrom","dateTo","cardId","sort"],"type":"object"},"AnalyticsRankResponse":{"description":"Response from the `/analytics/rank` endpoint. The `keyColumn` names the\ncolumn the returned `orderedKeys` reference (e.g. `spaceId` for Space\nrollup, `deviceId` for Device matrices), so the caller can pass them\nthrough to a follow-up `/query`.","properties":{"cardId":{"description":"Echoed back so the caller can disambiguate concurrent rank calls\nwithout keeping extra state.","type":"string"},"keyColumn":{"$ref":"#/components/schemas/AnalyticsRankKeyColumn","description":"Column the orderedKeys reference on the underlying DataMatrix rows."},"meta":{"$ref":"#/components/schemas/AnalyticsRankMeta","description":"Rank metadata. `total` is the size of `orderedKeys`."},"orderedKeys":{"description":"Ordered list of row IDs, capped at 5000. The first entry is the\nhighest-ranked under the requested sort.","items":{"type":"string"},"type":"array"}},"required":["cardId","keyColumn","orderedKeys","meta"],"type":"object"},"AnalyticsSort":{"description":"Sort specification for a paginated card.","properties":{"direction":{"$ref":"#/components/schemas/AnalyticsSortDirection","description":"Sort direction."},"field":{"description":"Column id to sort on. Unknown fields fall back to id ascending.","type":"string"}},"required":["field","direction"],"type":"object"},"AnalyticsSortDirection":{"description":"Sort direction.","enum":["asc","desc"],"type":"string"},"AnalyticsSource":{"description":"Data source routing for analytics queries.\n\nOnly `Live` is currently accepted. Omit to use the default precomputed path.","enum":["live"],"type":"string"},"ApiResponse_AppBuildDetailResponse":{"properties":{"data":{"description":"Full per-app-build payload returned by the detail endpoint.\n\nIncludes the JSON-Schema pair that drives the settings form (`settingsSchema` +\n`metaSchema`). Fields confirmed unused across console / cli / mobile-api are\ndeliberately omitted; see the plan doc for the full list.","example":{"appId":"5fd8dfe9a1d0eb0a63536096","createdAt":"2026-01-10T09:00:00.000Z","filename":"vision-ai-1.4.2.tar.gz","iconUrl":"https://media.omborigrid.com/.../icon.png","id":"5fd8dfe9a1d0eb0a63536101","metaSchema":{"ui:order":["title"]},"releaseNotes":"Added depth-of-field detector","settingsSchema":{"properties":{},"type":"object"},"status":"published","url":"5fd8dfe9a1d0eb0a63536096/3c5f8a1b-...","version":"1.4.2"},"properties":{"appId":{"description":"ID of the parent App. Echoed so the client doesn't need a round-trip\nto resolve it from the build.","type":"string"},"createdAt":{"type":"string"},"filename":{"type":"string"},"iconUrl":{"type":["string","null"]},"id":{"type":"string"},"metaSchema":{"description":"Optional RJSF `uiSchema` companion to `settingsSchema`. Always present\nin responses — `null` when the build has no meta-schema."},"releaseNotes":{"type":["string","null"]},"settingsSchema":{"description":"JSON Schema that drives the settings form."},"status":{"$ref":"#/components/schemas/AppBuildStatus"},"url":{"description":"Blob-storage path for the build artifact (`{appId}/{fileUuid}`).","type":"string"},"version":{"description":"Free-form version label. `null` when the build's `result` sub-doc\nhasn't been populated yet (status `processing` / `error`).","type":["string","null"]}},"required":["id","status","createdAt","appId","filename","url","settingsSchema"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_AppBuildUploadToken":{"properties":{"data":{"description":"One-time blob-upload credentials for submitting an app build. PUT the\n`.gridapp` archive to `<uri>/<uuid>.gridapp?<sas>` (the `uri` already\nincludes the container), then call create with `fileUuid = uuid`.","properties":{"container":{"type":"string"},"sas":{"description":"SAS query string (without leading `?`).","type":"string"},"uri":{"description":"Blob base URL, container included.","type":"string"},"uuid":{"description":"Unique upload id; the blob must be named `<uuid>.gridapp`.","type":"string"}},"required":["sas","uri","container","uuid"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_AppCreateResponse":{"properties":{"data":{"allOf":[{"$ref":"#/components/schemas/AppResponse"},{"properties":{"cloudApp":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/CloudAppSecret"}]}},"type":"object"}],"description":"Create response: the app plus, for CLOUD apps only, one-time credentials."}},"required":["data"],"type":"object"},"ApiResponse_AppResponse":{"properties":{"data":{"description":"An app template in the Phystack platform.","example":{"createdAt":"2025-01-10T09:00:00.000Z","description":"Edge computer vision with real-time object detection, tracking, and analytics","displayName":"Vision AI","id":"507f1f77bcf86cd799439055","isPublished":true,"packageName":"@phystack/vision-ai","type":"CONTAINER","updatedAt":"2025-01-10T09:00:00.000Z"},"properties":{"access":{"$ref":"#/components/schemas/AppAccess","description":"App access level."},"createdAt":{"type":"string"},"description":{"type":["string","null"]},"displayName":{"type":"string"},"iconUrl":{"type":["string","null"]},"id":{"type":"string"},"isPublished":{"description":"Whether the app is published in the catalog.","type":"boolean"},"packageName":{"description":"Package identifier for the app (e.g. `@phystack/virtual-queues`).","type":"string"},"type":{"$ref":"#/components/schemas/AppType","description":"App category."},"updatedAt":{"type":"string"}},"required":["id","packageName","displayName","type","isPublished","createdAt","updatedAt"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_ContentTagResponse":{"properties":{"data":{"description":"A content tag — a tenant-scoped label (optionally multi-select) used to\nclassify content.","example":{"createdAt":"2025-01-10T09:00:00.000Z","description":"Seasonal campaign tag","id":"650abc1234ef567890123456","name":"Season","slug":"season","tenantId":"507f1f77bcf86cd799439011","type":"multi-select","updatedAt":"2025-01-10T09:00:00.000Z","values":["spring","summer","autumn","winter"]},"properties":{"createdAt":{"type":["string","null"]},"description":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"slug":{"description":"URL-safe identifier derived from `name`.","type":"string"},"tenantId":{"type":"string"},"type":{"description":"Input type discriminator — present only for tags carrying `values`.","type":["string","null"]},"updatedAt":{"type":["string","null"]},"values":{"description":"Allowed values for a multi-select tag. Absent for free-form tags.","items":{"type":"string"},"type":["array","null"]}},"required":["id","tenantId","name","slug","description"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_DescriptorResponse":{"properties":{"data":{"description":"A descriptor — the typed, published, versioned contract a peripheral derives\nits shape from. Owned by a tenant, visibility-scoped, author-stamped.","example":{"author":"Acme","createdAt":"2026-01-01T00:00:00.000Z","description":"A synthetic scanner descriptor","displayName":"Test Scanner","id":"507f1f77bcf86cd799439012","latestVersion":"1.0.0","name":"test-scanner","tenantId":"507f1f77bcf86cd799439011","updatedAt":"2026-01-02T00:00:00.000Z","validation":"advisory","visibility":"own"},"properties":{"author":{"description":"Author display name (stamped from the owning tenant on create).","type":"string"},"createdAt":{"type":"string"},"description":{"type":"string"},"displayName":{"type":"string"},"id":{"type":"string"},"latestVersion":{"description":"Highest published version (semver); empty until the first version exists.","type":"string"},"name":{"description":"Tenant-local descriptor name (kebab-case).","type":"string"},"tenantId":{"description":"Owning tenant.","type":"string"},"updatedAt":{"type":"string"},"validation":{"description":"Property-validation policy: `off` | `advisory` | `strict` (defaults to\n`advisory`). Drives the downstream descriptor-property validation gate.","type":"string"},"visibility":{"description":"Visibility: `public` | `child-tenants` | `own`.","type":"string"}},"required":["id","name","displayName","description","tenantId","visibility","validation","author","latestVersion","createdAt","updatedAt"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_DescriptorVersionResponse":{"properties":{"data":{"description":"One immutable published version of a descriptor (its schema payload). Returned\nby `GET /descriptors/{name}/versions/{version}` — also the phyhub S2S source.","example":{"createdAt":"2026-01-01T00:00:00.000Z","metaSchema":{},"releaseNotes":"initial","schema":{},"updatedAt":"2026-01-01T00:00:00.000Z","version":"1.0.0"},"properties":{"createdAt":{"type":"string"},"metaSchema":{"description":"Companion UI meta-schema — always an object on the wire (`{}` when the\nstored doc has none: Mongoose minimize prunes hint-less sidecars on save)."},"releaseNotes":{"type":"string"},"schema":{"description":"JSON-schema document (actions/events/desiredProperties/reportedProperties)."},"updatedAt":{"type":"string"},"version":{"type":"string"}},"required":["version","schema","metaSchema","releaseNotes","createdAt","updatedAt"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_DeviceCommandResponse":{"properties":{"data":{"description":"Result of a device command (e.g. reboot): which devices accepted the command\nand which failed to be queued.","properties":{"failedDeviceIds":{"items":{"type":"string"},"type":"array"},"successDeviceIds":{"items":{"type":"string"},"type":"array"}},"required":["successDeviceIds","failedDeviceIds"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_DeviceResponse":{"properties":{"data":{"description":"A device in the Phystack platform.","example":{"createdAt":"2025-01-10T09:00:00.000Z","deviceSerial":"PHY-2025-A7X3","displayName":"edge-node-alpha-7","id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","installationIds":["507f1f77bcf86cd799439044"],"spaceId":"507f1f77bcf86cd799439022","status":"ONLINE","tags":[],"tenantId":"507f1f77bcf86cd799439011","timezone":"America/Los_Angeles","updatedAt":"2025-01-10T09:00:00.000Z"},"properties":{"createdAt":{"type":"string"},"dailyRebootTime":{"type":["string","null"]},"descriptors":{"items":{},"type":["array","null"]},"deviceSerial":{"description":"Hardware serial number.","type":["string","null"]},"displayName":{"description":"Human-readable device name.","type":"string"},"externalId":{"description":"Human-friendly external identifier / slug (e.g. `hobart-cbd7012`).\nSurfaced to clients that render the slug alongside the display name\n(monitoring history rows, device matrix cells).","type":["string","null"]},"id":{"type":"string"},"installationIds":{"items":{"type":"string"},"type":"array"},"installationsInfo":{},"isTimezoneOverridden":{"type":["boolean","null"]},"monitoring":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/DeviceMonitoring"}]},"notes":{"type":["string","null"]},"os":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/DeviceOs"}]},"reportedTelemetry":{},"screen":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/DeviceScreen","description":"Display properties (only present for SCREEN devices)."}]},"screenshotUrl":{"type":["string","null"]},"spaceId":{"description":"Space the device is assigned to.","type":["string","null"]},"status":{"$ref":"#/components/schemas/DeviceStatus","description":"Device connection status."},"statusHistory":{"type":["string","null"]},"statusUpdatedAt":{"type":["string","null"]},"tags":{"items":{"$ref":"#/components/schemas/Tag"},"type":"array"},"tenantId":{"description":"Tenant that owns this device.","type":"string"},"timezone":{"type":["string","null"]},"updatedAt":{"type":"string"}},"required":["id","tenantId","displayName","status","createdAt","updatedAt"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_DeviceShellResponse":{"properties":{"data":{"description":"Connection info for a device shell tunnel.","properties":{"auth":{"description":"Bearer token for the device-streams WebSocket tunnel.","type":"string"},"isPhyos":{"description":"Whether the target runs PhyOS — selects the SSH username on the client.","type":"boolean"},"url":{"description":"WebSocket URL the client connects to (the client end of the tunnel).","type":"string"}},"required":["auth","url","isPhyos"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_InstallationResponse":{"properties":{"data":{"description":"An installation in the Phystack platform (an App deployed to a tenant).","example":{"appId":"5fd8dfe9a1d0eb0a63536096","appType":"EDGE","createdAt":"2025-01-10T09:00:00.000Z","defaultLanguage":"en","displayName":"Perimeter Vision AI","iconUrl":"https://media.omborigrid.com/.../icon.png","id":"507f1f77bcf86cd799439044","installationSlug":"perimeter-vision-ai-3f8d","multiLanguageSupport":false,"sku":"GAC001","supportedLanguages":["en"],"tenantId":"507f1f77bcf86cd799439011","updatedAt":"2025-01-10T09:00:00.000Z","version":3},"properties":{"analyticsSchema":{"description":"Analytics dashboard schema (only present when `include=analyticsSchema`)."},"appId":{"description":"ID of the App this installation is an instance of.","type":["string","null"]},"appType":{"$ref":"#/components/schemas/AppType","description":"App platform / runtime category."},"createdAt":{"type":"string"},"defaultLanguage":{"type":"string"},"displayName":{"type":"string"},"externalId":{"type":["string","null"]},"iconUrl":{"description":"Resolved icon URL for the installation. Falls back to a generic icon\nwhen the App does not define one.","type":"string"},"id":{"type":"string"},"installationGroupId":{"type":["string","null"]},"installationSlug":{"description":"Auto-generated unique slug.","type":"string"},"multiLanguageSupport":{"type":"boolean"},"sku":{"description":"Product SKU code identifying the App variant.","type":["string","null"]},"supportedLanguages":{"items":{"type":"string"},"type":"array"},"tenantId":{"type":"string"},"updatedAt":{"type":"string"},"version":{"description":"OCC version; `null` for pre-2026-03 docs (echo back on `PUT`).","format":"int32","minimum":0,"type":["integer","null"]}},"required":["id","tenantId","installationSlug","displayName","appType","iconUrl","multiLanguageSupport","defaultLanguage","createdAt","updatedAt"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_InstallationSettingsResponse":{"properties":{"data":{"description":"Schema-driven settings for an installation — the pinned `appBuildId` and\nform `values` validated against the build's settings schema.","example":{"appBuildId":"65a14f3d8e7c2a0008d4e1b9","updatedAt":"2026-05-04T14:22:00.000Z","values":{"showQrCode":true,"theme":"dark","welcomeMessage":"Hi there!"},"version":5},"properties":{"appBuildId":{"description":"ID of the App build the installation is pinned to.","type":"string"},"updatedAt":{"type":"string"},"values":{"description":"Schema-driven form data, validated against the build's settings schema."},"version":{"description":"Current resource version. Pass this back on `PUT` calls for optimistic\nconcurrency — a mismatch returns `409 Conflict`.","format":"int32","minimum":0,"type":"integer"}},"required":["appBuildId","values","version","updatedAt"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_InvitationResponse":{"properties":{"data":{"description":"A pending invitation to join one or more tenants.","properties":{"createdAt":{"type":["string","null"]},"email":{"type":"string"},"expiresAt":{"type":["string","null"]},"id":{"type":"string"},"status":{"description":"`PENDING | ACCEPTED | EXPIRED`. A pending invite past its expiry is\nreported as `EXPIRED`.","type":"string"},"tenantId":{"description":"Convenience: the first membership's tenant. The full set lives in `tenants`.","type":["string","null"]},"tenants":{"items":{"$ref":"#/components/schemas/InvitationTenantMembership"},"type":"array"},"updatedAt":{"type":["string","null"]}},"required":["id","email","status","tenants"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_InvoiceResponse":{"properties":{"data":{"description":"A system-generated invoice for a tenant.","properties":{"amount":{"format":"double","type":["number","null"]},"billingReason":{"type":["string","null"]},"currency":{"type":["string","null"]},"date":{"type":["string","null"]},"description":{"type":["string","null"]},"documentUrl":{"description":"Document download path, or null when the invoice has no document.","type":["string","null"]},"id":{"type":"string"},"status":{"type":["string","null"]},"tenantId":{"type":["string","null"]}},"required":["id"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_OverrideResponse":{"properties":{"data":{"description":"The stored override for a Space or Device — a JSON-Pointer delta plus its\noptimistic-concurrency version.","example":{"ops":[{"op":"set","path":"/theme/color","value":"red"},{"op":"remove","path":"/legacy/flag"}],"version":3},"properties":{"ops":{"description":"The override operations layered over the installation base.","items":{"$ref":"#/components/schemas/OverrideOp"},"type":"array"},"version":{"description":"Snapshot version; `0` when no override exists yet. Pass it back on\n`PUT` for optimistic concurrency (409 on mismatch).","format":"int32","minimum":0,"type":"integer"}},"required":["ops","version"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_PeripheralActionResult":{"properties":{"data":{"description":"The result of invoking an action on a peripheral — the owner's `respond()`\npayload, echoed back over the twin-message bus.","example":{"data":{"jobId":"job-42"},"message":"Printed","status":"success"},"properties":{"data":{"description":"Optional structured return value, shaped by the action's descriptor schema."},"message":{"description":"Human-readable message from the owner.","type":["string","null"]},"status":{"description":"Outcome reported by the peripheral owner (e.g. `success` / `error`).","type":"string"}},"required":["status"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_PeripheralResponse":{"properties":{"data":{"description":"A peripheral — a logical device-attached component (scanner, printer, …)\nwhose shape is derived from its descriptor(s). Settings are operator-writable\n(schema-validated); telemetry is read-only.","example":{"createdAt":"2026-01-02T03:04:05.000Z","descriptors":{"acme.test-scanner":"1.0.0"},"deviceId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","hardwareId":"hw-777","id":"650abc1234ef567890123456","name":"Front Scanner","settings":{"acme.test-scanner":{"beepVolume":7}},"status":"OK","telemetry":{"status":"ready"},"tenantId":"507f1f77bcf86cd799439011","updatedAt":"2026-01-03T03:04:05.000Z"},"properties":{"createdAt":{"type":"string"},"descriptors":{"additionalProperties":{"type":"string"},"description":"Map of descriptor uniqueName → pinned version.","propertyNames":{"type":"string"},"type":"object"},"deviceId":{"type":"string"},"hardwareId":{"type":"string"},"id":{"type":"string"},"name":{"description":"Logical peripheral name.","type":"string"},"settings":{"description":"Operator-writable settings, validated against the descriptor schema on write."},"status":{"type":"string"},"telemetry":{"description":"Read-only telemetry (the peripheral's reported state)."},"tenantId":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","deviceId","tenantId","name","hardwareId","descriptors","status","settings","telemetry","createdAt","updatedAt"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_PeripheralSchemaResponse":{"properties":{"data":{"description":"The peripheral's full contract, merged from its pinned descriptor versions.\nFacets keep the descriptor vocabulary: `desiredProperties` is the settings\ncontract (identity-excluded and strict — the SAME schema the settings PUT\nvalidates against), `reportedProperties` is the telemetry contract, and\n`actions`/`events` describe the invokable surface. `metaSchema` is the\nmatching four-facet UI sidecar (`ui:*` hints).","example":{"metaSchema":{"actions":{},"desiredProperties":{"acme.test-scanner":{"beepVolume":{"ui:widget":"range"}}},"events":{},"reportedProperties":{}},"schema":{"properties":{"actions":{"properties":{"acme.test-scanner":{"properties":{"beep":{"properties":{"params":{"type":"object"}},"type":"object"}},"type":"object"}}},"desiredProperties":{"additionalProperties":false,"properties":{"acme.test-scanner":{"additionalProperties":false,"properties":{"beepVolume":{"type":"integer"}},"type":"object"}},"type":"object"},"events":{"properties":{}},"reportedProperties":{"properties":{"acme.test-scanner":{"properties":{"paperLevel":{"type":"integer"}},"type":"object"}}}},"type":"object"}},"properties":{"metaSchema":{"description":"Four-facet UI sidecar (`ui:*` hints); facets are `{}` when hint-less."},"schema":{"description":"Merged JSON-schema contract with the four facets under `properties`."}},"required":["schema","metaSchema"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_RoleResponse":{"properties":{"data":{"description":"A tenant-scoped role (permission set).","properties":{"allowedRoleIdsToManage":{"items":{"type":"string"},"type":"array"},"createdAt":{"type":["string","null"]},"id":{"type":"string"},"manageAllRoles":{"type":"boolean"},"name":{"type":"string"},"permissions":{"items":{"type":"string"},"type":"array"},"tenantId":{"type":"string"},"updatedAt":{"type":["string","null"]}},"required":["id","tenantId","name","permissions","manageAllRoles","allowedRoleIdsToManage"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_SettingsMigrationResponse":{"properties":{"data":{"description":"Schema delta between the installation's currently pinned build and a target\nbuild. Powers the safe-version-migration flow: a caller inspects the delta,\nthen submits a migration patch (via `PUT /settings`) that makes the result\nvalid against the target build's schema.","example":{"added":["newTheme"],"currentValuesValidAgainstTarget":false,"fromAppBuildId":"65a14f3d8e7c2a0008d4e1b9","issues":["/timeoutSeconds must be string"],"removed":["legacyFlag"],"retyped":[{"from":"integer","key":"timeoutSeconds","to":"string"}],"toAppBuildId":"66b2510e9f8d3b1109e5f2ca"},"properties":{"added":{"description":"Top-level property keys present in target but not in current.","items":{"type":"string"},"type":"array"},"currentValuesValidAgainstTarget":{"description":"Whether the installation's current `values` already validate against the\ntarget schema. `true` → a bare version bump is schema-safe.","type":"boolean"},"fromAppBuildId":{"description":"The installation's currently pinned build (source of the diff).","type":"string"},"issues":{"description":"Human-readable validation issues when the current values don't fit.","items":{"type":"string"},"type":"array"},"removed":{"description":"Top-level property keys present in current but not in target.","items":{"type":"string"},"type":"array"},"retyped":{"description":"Properties whose declared type changed (current → target).","items":{"$ref":"#/components/schemas/RetypedProperty"},"type":"array"},"toAppBuildId":{"description":"The target build the delta is computed against.","type":"string"}},"required":["fromAppBuildId","toAppBuildId","added","removed","retyped","currentValuesValidAgainstTarget","issues"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_ShortUrlResponse":{"properties":{"data":{"description":"A tenant-managed custom short URL.","example":{"analyticsEnabled":true,"createdAt":"2026-01-10T09:00:00.000Z","displayName":"Summer Sale Landing","friendly":false,"id":"summer-sale","redirectUrl":"https://example.com/promotions/summer","shortUrl":"https://qr.run/summer-sale","tenantId":"507f1f77bcf86cd799439011","updatedAt":"2026-01-10T09:00:00.000Z"},"properties":{"analyticsEnabled":{"description":"Whether analytics tracking is attached to this short URL.","type":"boolean"},"createdAt":{"type":["string","null"]},"displayName":{"type":["string","null"]},"friendly":{"description":"Whether the shortener generated a friendly (word-based) slug.","type":"boolean"},"id":{"description":"Slug identifier (the shortener's `customId`). Caller-chosen, stable.","type":"string"},"redirectUrl":{"description":"Destination URL the short link redirects to.","type":"string"},"shortUrl":{"description":"Fully-qualified short URL the shortener minted.","type":["string","null"]},"tenantId":{"description":"Owning tenant (`organizationId`).","type":["string","null"]},"updatedAt":{"type":["string","null"]}},"required":["id","redirectUrl","friendly","analyticsEnabled"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_SpaceResponse":{"properties":{"data":{"description":"A space in the Phystack platform (store, venue, area, etc.).","example":{"address":"800 Mission Street","city":"San Francisco","country":"US","createdAt":"2025-01-10T09:00:00.000Z","displayName":"Building A - Research Lab","id":"507f1f77bcf86cd799439022","latitude":37.7855,"longitude":-122.3965,"notes":"Edge compute research facility with 12 vision nodes","postcode":"94105","state":"CA","tags":[],"tenantId":"507f1f77bcf86cd799439011","timezone":"America/Los_Angeles","type":"LOCATION","updatedAt":"2025-01-10T09:00:00.000Z"},"properties":{"address":{"type":["string","null"]},"billingAddress":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/BillingAddress","description":"Billing address — present only when billing details were set on the\nspace (settable at create). Same shape as tenant billing."}]},"billingContact":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/SpaceBillingContact","description":"Billing contact — present only when billing details were set on the\nspace (settable at create)."}]},"children":{"description":"Transitive descendant spaces (all levels, flattened — descendants do\nnot nest their own `children`). Present only on list rows when\n`include=children` is requested; narrowed by `filter[childrenType]`.\n`no_recursion` stops utoipa's schema generation from walking the\nself-reference forever (stack overflow at doc build otherwise).","items":{"$ref":"#/components/schemas/SpaceResponse"},"type":["array","null"]},"city":{"type":["string","null"]},"country":{"type":["string","null"]},"createdAt":{"description":"ISO 8601 creation timestamp.","type":"string"},"displayName":{"description":"Human-readable space name.","type":"string"},"externalId":{"type":["string","null"]},"id":{"description":"Unique space identifier.","type":"string"},"latitude":{"format":"double","type":["number","null"]},"longitude":{"format":"double","type":["number","null"]},"notes":{"type":["string","null"]},"parentSpaceId":{"description":"Parent space ID for hierarchical nesting.","type":["string","null"]},"postcode":{"type":["string","null"]},"state":{"type":["string","null"]},"tags":{"items":{"$ref":"#/components/schemas/Tag"},"type":"array"},"tenantId":{"description":"Tenant that owns this space.","type":"string"},"timezone":{"description":"IANA timezone (e.g. Europe/Stockholm).","type":["string","null"]},"type":{"$ref":"#/components/schemas/SpaceType","description":"Space type in the hierarchy."},"updatedAt":{"description":"ISO 8601 last update timestamp.","type":"string"},"weeklySchedule":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/WeeklySchedule"}]}},"required":["id","tenantId","displayName","type","createdAt","updatedAt"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_TenantResponse":{"properties":{"data":{"description":"A tenant in the Phystack platform.","example":{"allowedUserEmailDomains":[],"billingAddress":{"addressLine1":"123 Main St","city":"Chicago","country":"US","name":"Nexus Technologies","postal":"60601"},"billingContactEmail":"billing@nexus-tech.com","billingEntity":"phystack-ie","billingType":"credit-card","country":"US","createdAt":"2025-01-10T09:00:00.000Z","currency":"USD","dataResidency":"US","enabled":true,"hasActiveSubscription":true,"id":"507f1f77bcf86cd799439011","restrictUserEmailDomains":false,"tenantDisplayName":"Nexus Technologies","tenantName":"Nexus Technologies","tenantSlug":"nexus-tech-4821","timezone":"America/Chicago","updatedAt":"2025-01-10T09:00:00.000Z","vatId":"SE556677889901"},"properties":{"allowedUserEmailDomains":{"items":{"type":"string"},"type":"array"},"billingAddress":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/BillingAddressResponse","description":"Billing address for invoicing."}]},"billingContactEmail":{"description":"Billing contact email address.","type":["string","null"]},"billingEntity":{"description":"Billing entity for invoicing.","type":["string","null"]},"billingType":{"description":"Billing type (e.g. credit-card, azure-saas, direct-invoice).","type":["string","null"]},"country":{"type":"string"},"createdAt":{"type":"string"},"currency":{"type":"string"},"dataResidency":{"$ref":"#/components/schemas/DataResidency","description":"Data storage region."},"enabled":{"description":"Whether the tenant account is active.","type":"boolean"},"hasActiveSubscription":{"description":"Whether the tenant has a current active subscription. Read-only, derived from subscription data.","type":"boolean"},"id":{"type":"string"},"parentTenantId":{"description":"Parent tenant ID for sub-tenant hierarchy.","type":["string","null"]},"restrictUserEmailDomains":{"type":"boolean"},"tenantDisplayName":{"description":"Human-readable tenant name.","type":"string"},"tenantName":{"description":"Human-readable tenant name (alias of `tenantDisplayName`).","type":"string"},"tenantSlug":{"description":"Auto-generated unique slug (e.g. acme-7294).","type":"string"},"timezone":{"type":"string"},"updatedAt":{"type":"string"},"vatId":{"description":"VAT identification number.","type":["string","null"]}},"required":["id","tenantSlug","tenantDisplayName","tenantName","country","currency","timezone","dataResidency","restrictUserEmailDomains","createdAt","updatedAt","enabled","hasActiveSubscription"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_TokenCreateResponse":{"properties":{"data":{"allOf":[{"$ref":"#/components/schemas/TokenResponse"},{"properties":{"token":{"description":"The plaintext token — shown ONCE; cannot be retrieved again.","type":"string"}},"required":["token"],"type":"object"}],"description":"Create response: the metadata plus the one-time plaintext `token`."}},"required":["data"],"type":"object"},"ApiResponse_UserResponse":{"properties":{"data":{"description":"A platform user.","properties":{"createdAt":{"type":["string","null"]},"email":{"type":"string"},"firstName":{"type":["string","null"]},"id":{"type":"string"},"isSysadmin":{"type":"boolean"},"lastName":{"type":["string","null"]},"lastSeenAt":{"type":["string","null"]},"phoneNumber":{"type":["string","null"]},"preferredLanguage":{"type":["string","null"]},"tenants":{"items":{"$ref":"#/components/schemas/UserTenantMembership"},"type":"array"},"updatedAt":{"type":["string","null"]}},"required":["id","email","isSysadmin","tenants"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_Vec_CloudInstanceResponse":{"properties":{"data":{"items":{"description":"A cloud-app instance — the slim, public-API projection of a phyhub cloud\ntwin. One row per `(installationId, spaceId)` pair. The deeper twin doc\nstays internal to phyhub; consumers only see the fields below.","example":{"appId":"5fd8dfe9a1d0eb0a63536096","appName":"acme/cloud-app","appVersion":"1.0.0","buildId":"65a14f3d8e7c2a0008d4e1b9","createdAt":"2026-03-01T09:00:00.000Z","id":"650abc1234ef567890123456","installationId":"507f1f77bcf86cd799439044","spaceId":"507f1f77bcf86cd799439022","status":"Online","tenantId":"507f1f77bcf86cd799439011","updatedAt":"2026-03-01T09:00:00.000Z"},"properties":{"appId":{"type":"string"},"appName":{"type":"string"},"appVersion":{"type":"string"},"buildId":{"type":["string","null"]},"createdAt":{"type":"string"},"id":{"type":"string"},"installationId":{"type":"string"},"lastStatusUpdatedAt":{"type":["string","null"]},"spaceId":{"type":"string"},"status":{"type":"string"},"tenantId":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","installationId","appId","appName","appVersion","spaceId","status","createdAt","updatedAt"],"type":"object"},"type":"array"}},"required":["data"],"type":"object"},"ApiResponse_WebEndpointCodeResponse":{"properties":{"data":{"description":"A claim code plus its ready-to-open session link, minted by an authorized\noperator without a device (TECH-1471) — the exact credential a QR scan\ncarries. The link is LIVE for `expiresIn` seconds: treat it like the QR\nitself — show it once, never log it.","example":{"code":"K7KQ3ZDM2P","expiresIn":300,"url":"https://web.phystack.com/amco-5119/office-remote/#code=K7KQ3ZDM2P"},"properties":{"code":{"description":"The claim code (also embedded in `url`), redeemable per the endpoint's\n`codeMode` until it expires.","type":"string"},"expiresIn":{"description":"Seconds until the code expires (the endpoint's `codeTtlSeconds`).","format":"int64","minimum":0,"type":"integer"},"url":{"description":"Ready-to-open session link: `{webAppsRoot}/{urlId}/#code={code}`.","type":"string"}},"required":["code","url","expiresIn"],"type":"object"}},"required":["data"],"type":"object"},"ApiResponse_WebEndpointResponse":{"properties":{"data":{"description":"A web endpoint — the slim, public-API projection of a phyhub Web twin.\nOne row per endpoint; multiple endpoints per installation are allowed\n(uniqueness key is `urlId` — the composite `{tenantSlug}/{name}` public\npath, unique per region including deleted endpoints: urlIds are\npermanent, TECH-1469).","example":{"appName":"acme/queue-status-web","appVersion":"1.4.0","codeMode":"single-use","codeTtlSeconds":120,"createdAt":"2026-08-01T09:00:00.000Z","displayName":"Queue status — Oxford Street","enabled":true,"id":"3f9a2b1c-7d4e-4c2a-9f6b-1e8d5a3c7b90","installationId":"6512aa34bcf86cd799439aa1","maxSessionDurationSeconds":14400,"maxSessions":200,"scopes":["twin:read","twin:message"],"sessionTtlSeconds":900,"spaceId":"6512bb34bcf86cd799439bb2","status":"Online","updatedAt":"2026-08-01T09:00:00.000Z","url":"https://web.phystack.com/acme/queue-status-oxford-street/","urlId":"acme/queue-status-oxford-street"},"properties":{"appName":{"type":"string"},"appVersion":{"type":"string"},"codeMode":{"$ref":"#/components/schemas/WebEndpointCodeMode"},"codeTtlSeconds":{"description":"Claim-code lifetime in seconds.","format":"int32","minimum":0,"type":"integer"},"createdAt":{"type":"string"},"displayName":{"type":"string"},"enabled":{"description":"Disabled endpoints reject session minting; live sessions expire\nwithin one session TTL.","type":"boolean"},"id":{"description":"The endpoint's deviceId — a uuid minted at create, NOT the Web twin's\nMongo `_id`. Keys the per-endpoint settings override; opaque string.","type":"string"},"installationId":{"type":"string"},"maxSessionDurationSeconds":{"description":"Refresh-grant ceiling in seconds — total session lifetime across renewals.","format":"int32","minimum":0,"type":"integer"},"maxSessions":{"description":"Maximum concurrent web sockets for this endpoint.","format":"int32","minimum":0,"type":"integer"},"scopes":{"description":"Operation classes granted to minted sessions\n(e.g. `twin:read`, `twin:message`, `media:view`).","items":{"type":"string"},"type":"array"},"sessionTtlSeconds":{"description":"Session JWT lifetime in seconds.","format":"int32","minimum":0,"type":"integer"},"spaceId":{"description":"The Space this endpoint serves — one Space per endpoint.","type":"string"},"status":{"description":"Twin status (e.g. `\"Online\"`, `\"Offline\"`).","type":"string"},"updatedAt":{"type":"string"},"url":{"description":"Public URL the endpoint serves at (`{web hosting root}/{urlId}/`) —\nderived from the region's web hosting domain, not stored.","type":"string"},"urlId":{"description":"The composite public path `{tenantSlug}/{name}` (kebab-case segments,\nimmutable after create, never reused — deleted endpoints keep their\nurlId forever). Not a credential — sessions always require a live\nclaim code or refresh token.","type":"string"}},"required":["id","installationId","spaceId","appName","appVersion","status","urlId","url","displayName","enabled","scopes","codeMode","codeTtlSeconds","maxSessions","sessionTtlSeconds","maxSessionDurationSeconds","createdAt","updatedAt"],"type":"object"}},"required":["data"],"type":"object"},"AppAccess":{"description":"App access level.","enum":["PUBLIC","PRIVATE","SUBTENANTS","SPECIFIC_TENANTS"],"type":"string"},"AppBuildCreateRequest":{"description":"Request body to submit an app build (after uploading the archive to blob).\n\n`image`, `createOptions`, and `registryCredential` are optional, additive\nfields for EDGE/IOT apps: the client builds and pushes the container image,\nthen supplies the image ref + a pull-only credential here instead of baking\ncredentials into the `.gridapp` manifest. When omitted, the upstream legacy\nmanifest-extraction path runs unchanged.","properties":{"createOptions":{"description":"Docker `createOptions` (object or JSON string) for the edge container."},"fileUuid":{"description":"UUID from the upload-token endpoint; the uploaded blob is `<fileUuid>.gridapp`.","type":"string"},"filename":{"type":"string"},"image":{"description":"Container image ref (`registry/repo:version`) for an EDGE/IOT build.","type":["string","null"]},"registryCredential":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/AppBuildRegistryCredential","description":"Pull-only credential for a private edge image."}]},"size":{"format":"int64","minimum":0,"type":"integer"}},"required":["fileUuid","filename","size"],"type":"object"},"AppBuildDetailResponse":{"description":"Full per-app-build payload returned by the detail endpoint.\n\nIncludes the JSON-Schema pair that drives the settings form (`settingsSchema` +\n`metaSchema`). Fields confirmed unused across console / cli / mobile-api are\ndeliberately omitted; see the plan doc for the full list.","example":{"appId":"5fd8dfe9a1d0eb0a63536096","createdAt":"2026-01-10T09:00:00.000Z","filename":"vision-ai-1.4.2.tar.gz","iconUrl":"https://media.omborigrid.com/.../icon.png","id":"5fd8dfe9a1d0eb0a63536101","metaSchema":{"ui:order":["title"]},"releaseNotes":"Added depth-of-field detector","settingsSchema":{"properties":{},"type":"object"},"status":"published","url":"5fd8dfe9a1d0eb0a63536096/3c5f8a1b-...","version":"1.4.2"},"properties":{"appId":{"description":"ID of the parent App. Echoed so the client doesn't need a round-trip\nto resolve it from the build.","type":"string"},"createdAt":{"type":"string"},"filename":{"type":"string"},"iconUrl":{"type":["string","null"]},"id":{"type":"string"},"metaSchema":{"description":"Optional RJSF `uiSchema` companion to `settingsSchema`. Always present\nin responses — `null` when the build has no meta-schema."},"releaseNotes":{"type":["string","null"]},"settingsSchema":{"description":"JSON Schema that drives the settings form."},"status":{"$ref":"#/components/schemas/AppBuildStatus"},"url":{"description":"Blob-storage path for the build artifact (`{appId}/{fileUuid}`).","type":"string"},"version":{"description":"Free-form version label. `null` when the build's `result` sub-doc\nhasn't been populated yet (status `processing` / `error`).","type":["string","null"]}},"required":["id","status","createdAt","appId","filename","url","settingsSchema"],"type":"object"},"AppBuildListItem":{"description":"One row in the version-selector dropdown.\n\nLightweight on purpose: schemas live on [`AppBuildDetailResponse`] and are\nonly fetched when the user actually selects a version.","example":{"createdAt":"2026-01-10T09:00:00.000Z","id":"5fd8dfe9a1d0eb0a63536101","status":"published","version":"1.4.2"},"properties":{"createdAt":{"type":"string"},"id":{"type":"string"},"status":{"$ref":"#/components/schemas/AppBuildStatus","description":"Lifecycle status."},"version":{"description":"Free-form version label set by the build pipeline (commonly semver).\n`null` when the build's `result` sub-doc hasn't been populated yet\n(status `processing` / `error`).","type":["string","null"]}},"required":["id","status","createdAt"],"type":"object"},"AppBuildRegistryCredential":{"description":"Pull-only registry credential for a private edge image. The API accepts ONLY\na pull credential — never a push credential (the client pushes the image\nitself via `docker buildx --push`). Stamped into the build's\n`metadata.credentials` and delivered to the device via the edge twin.","properties":{"password":{"type":"string"},"registry":{"description":"Registry host the credential authenticates against (informational; the\nimage ref already carries the host).","type":["string","null"]},"username":{"type":"string"}},"required":["username","password"],"type":"object"},"AppBuildStatus":{"description":"Lifecycle status of an app build.","enum":["processing","draft","error","published"],"type":"string"},"AppBuildUploadToken":{"description":"One-time blob-upload credentials for submitting an app build. PUT the\n`.gridapp` archive to `<uri>/<uuid>.gridapp?<sas>` (the `uri` already\nincludes the container), then call create with `fileUuid = uuid`.","properties":{"container":{"type":"string"},"sas":{"description":"SAS query string (without leading `?`).","type":"string"},"uri":{"description":"Blob base URL, container included.","type":"string"},"uuid":{"description":"Unique upload id; the blob must be named `<uuid>.gridapp`.","type":"string"}},"required":["sas","uri","container","uuid"],"type":"object"},"AppCreateRequest":{"description":"Request body to create an app. All fields use 2026-03 naming; the owning\nservice maps them to the internal model at the boundary.","properties":{"access":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/AppAccess","description":"App access level (defaults to PRIVATE)."}]},"description":{"type":["string","null"]},"displayName":{"type":"string"},"packageName":{"description":"Package identifier (lowercase, kebab-style; e.g. `vision-ai`).","type":"string"},"sku":{"type":["string","null"]},"tenantAccessIds":{"description":"Tenants granted access (used with `SPECIFIC_TENANTS`).","items":{"type":"string"},"type":["array","null"]},"tenantId":{"description":"Owning Tenant id.","type":"string"},"type":{"$ref":"#/components/schemas/AppType","description":"App category."}},"required":["tenantId","packageName","displayName","type"],"type":"object"},"AppCreateResponse":{"allOf":[{"$ref":"#/components/schemas/AppResponse"},{"properties":{"cloudApp":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/CloudAppSecret"}]}},"type":"object"}],"description":"Create response: the app plus, for CLOUD apps only, one-time credentials."},"AppResponse":{"description":"An app template in the Phystack platform.","example":{"createdAt":"2025-01-10T09:00:00.000Z","description":"Edge computer vision with real-time object detection, tracking, and analytics","displayName":"Vision AI","id":"507f1f77bcf86cd799439055","isPublished":true,"packageName":"@phystack/vision-ai","type":"CONTAINER","updatedAt":"2025-01-10T09:00:00.000Z"},"properties":{"access":{"$ref":"#/components/schemas/AppAccess","description":"App access level."},"createdAt":{"type":"string"},"description":{"type":["string","null"]},"displayName":{"type":"string"},"iconUrl":{"type":["string","null"]},"id":{"type":"string"},"isPublished":{"description":"Whether the app is published in the catalog.","type":"boolean"},"packageName":{"description":"Package identifier for the app (e.g. `@phystack/virtual-queues`).","type":"string"},"type":{"$ref":"#/components/schemas/AppType","description":"App category."},"updatedAt":{"type":"string"}},"required":["id","packageName","displayName","type","isPublished","createdAt","updatedAt"],"type":"object"},"AppType":{"description":"App category.","enum":["SCREEN","EDGE","WEB","CLOUD","QUEUE_LEGACY"],"type":"string"},"AppUpdateRequest":{"description":"Request body to update an app. All fields optional; `type` is immutable.","properties":{"access":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/AppAccess"}]},"author":{"type":["string","null"]},"description":{"type":["string","null"]},"displayName":{"type":["string","null"]},"packageName":{"type":["string","null"]},"screenshots":{"items":{"type":"string"},"type":["array","null"]},"sku":{"type":["string","null"]},"tenantAccessIds":{"items":{"type":"string"},"type":["array","null"]}},"type":"object"},"AssignDeviceRequest":{"description":"Request body to assign a device to an installation.","properties":{"deviceId":{"description":"The device to assign to this installation.","type":"string"}},"required":["deviceId"],"type":"object"},"BillingAddress":{"description":"Billing address, shared by tenant billing and space (LOCATION) billing —\nadmin-api persists both through the same `BillingAddressSchema`. The five\nrequired fields are enforced by that Mongo schema on every write path\n(`required: true` rejects absent AND empty-string values; the route Joi is\nlooser but Mongoose has the final say — verified live 2026-07-08). The\noptional fields default to `\"\"` on persist, so responses echo every key\nwhenever the address is set.","properties":{"addressLine1":{"type":"string"},"addressLine2":{"type":["string","null"]},"city":{"type":"string"},"country":{"type":"string"},"email":{"type":["string","null"]},"name":{"type":"string"},"notes":{"description":"Free-text billing notes (max 250 chars upstream).","type":["string","null"]},"postal":{"type":"string"},"state":{"type":["string","null"]},"vatId":{"type":["string","null"]}},"required":["name","addressLine1","city","country","postal"],"type":"object"},"BillingAddressResponse":{"description":"Billing address in a tenant response.","properties":{"addressLine1":{"type":"string"},"addressLine2":{"type":["string","null"]},"city":{"type":"string"},"country":{"type":"string"},"email":{"type":["string","null"]},"name":{"type":["string","null"]},"notes":{"type":["string","null"]},"postal":{"type":"string"},"state":{"type":["string","null"]},"vatId":{"type":["string","null"]}},"required":["addressLine1","city","country","postal"],"type":"object"},"CardPaginationParams":{"description":"Per-card pagination parameters.\n\nSort field mapping is handled server-side; callers pass the visible column\nidentifier (e.g. `displayName`, `totalTransactions`). Unknown fields fall\nback to a stable id-ascending order.","properties":{"filter":{"additionalProperties":{"$ref":"#/components/schemas/FilterValue"},"description":"Optional filters keyed by column id. Each value is either a single\nstring (equality match) or an array of strings (IN-list match).\nAn empty array is treated as \"no filter\".","propertyNames":{"type":"string"},"type":["object","null"]},"page":{"description":"1-based page number (default 1).","example":1,"format":"int32","minimum":1,"type":["integer","null"]},"pageSize":{"description":"Rows per page (default 25, max 100).","example":25,"format":"int32","maximum":100,"minimum":1,"type":["integer","null"]},"sort":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/AnalyticsSort","description":"Optional sort spec."}]}},"type":"object"},"CloneInstallationRequest":{"additionalProperties":false,"description":"Request body for cloning an installation.\n\nDeep-copies an Edge (IOT) or Screen (PWA) installation under a new display\nname. Optionally place the clone in an installation group.","example":{"displayName":"Weighing Scale (copy)"},"properties":{"displayName":{"description":"Display name for the cloned installation.","type":"string"},"installationGroupId":{"description":"Optional installation group to place the clone in.","type":["string","null"]}},"required":["displayName"],"type":"object"},"CloudAppSecret":{"description":"One-time cloud-app credentials, returned only when creating a CLOUD app.\nThe plaintext `secret` is never stored and never retrievable again.","properties":{"appRegistrationId":{"type":"string"},"coreApiUrl":{"type":"string"},"phyhubUrl":{"type":"string"},"region":{"type":"string"},"secret":{"type":"string"}},"required":["appRegistrationId","secret","coreApiUrl","phyhubUrl","region"],"type":"object"},"CloudInstanceResponse":{"description":"A cloud-app instance — the slim, public-API projection of a phyhub cloud\ntwin. One row per `(installationId, spaceId)` pair. The deeper twin doc\nstays internal to phyhub; consumers only see the fields below.","example":{"appId":"5fd8dfe9a1d0eb0a63536096","appName":"acme/cloud-app","appVersion":"1.0.0","buildId":"65a14f3d8e7c2a0008d4e1b9","createdAt":"2026-03-01T09:00:00.000Z","id":"650abc1234ef567890123456","installationId":"507f1f77bcf86cd799439044","spaceId":"507f1f77bcf86cd799439022","status":"Online","tenantId":"507f1f77bcf86cd799439011","updatedAt":"2026-03-01T09:00:00.000Z"},"properties":{"appId":{"type":"string"},"appName":{"type":"string"},"appVersion":{"type":"string"},"buildId":{"type":["string","null"]},"createdAt":{"type":"string"},"id":{"type":"string"},"installationId":{"type":"string"},"lastStatusUpdatedAt":{"type":["string","null"]},"spaceId":{"type":"string"},"status":{"type":"string"},"tenantId":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","installationId","appId","appName","appVersion","spaceId","status","createdAt","updatedAt"],"type":"object"},"ContentTagCreateRequest":{"additionalProperties":false,"description":"Request body for creating a content tag.","example":{"description":"Seasonal campaign tag","name":"Season","tenantId":"507f1f77bcf86cd799439011","type":"multi-select","values":["spring","summer","autumn","winter"]},"properties":{"description":{"type":["string","null"]},"name":{"type":"string"},"tenantId":{"type":"string"},"type":{"description":"Input type. Currently only `multi-select`.","type":["string","null"]},"values":{"description":"Allowed values for a multi-select tag.","items":{"type":"string"},"type":["array","null"]}},"required":["tenantId","name"],"type":"object"},"ContentTagResponse":{"description":"A content tag — a tenant-scoped label (optionally multi-select) used to\nclassify content.","example":{"createdAt":"2025-01-10T09:00:00.000Z","description":"Seasonal campaign tag","id":"650abc1234ef567890123456","name":"Season","slug":"season","tenantId":"507f1f77bcf86cd799439011","type":"multi-select","updatedAt":"2025-01-10T09:00:00.000Z","values":["spring","summer","autumn","winter"]},"properties":{"createdAt":{"type":["string","null"]},"description":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"slug":{"description":"URL-safe identifier derived from `name`.","type":"string"},"tenantId":{"type":"string"},"type":{"description":"Input type discriminator — present only for tags carrying `values`.","type":["string","null"]},"updatedAt":{"type":["string","null"]},"values":{"description":"Allowed values for a multi-select tag. Absent for free-form tags.","items":{"type":"string"},"type":["array","null"]}},"required":["id","tenantId","name","slug","description"],"type":"object"},"ContentTagUpdateRequest":{"additionalProperties":false,"description":"Request body for updating a content tag. Tenant is derived from the existing\ntag, so `tenantId` is not accepted here.","example":{"description":"Seasonal campaign tag","name":"Season","type":"multi-select","values":["spring","summer"]},"properties":{"description":{"type":["string","null"]},"name":{"type":"string"},"type":{"type":["string","null"]},"values":{"items":{"type":"string"},"type":["array","null"]}},"required":["name"],"type":"object"},"CountDeviceResult":{"properties":{"deviceId":{"type":"string"},"groups":{"description":"One entry per group; empty when no events matched.","items":{"$ref":"#/components/schemas/CountGroup"},"type":"array"}},"required":["deviceId","groups"],"type":"object"},"CountGroup":{"properties":{"count":{"format":"int64","minimum":0,"type":"integer"},"key":{"description":"Map of `{groupByField: value}`. `null` when the request had no\n`groupBy` (single-bucket mode)."},"sampleEvent":{"description":"Chronologically first event in the group, with `int1..5` / `str1..5`\n/ `eventTime` / `sessionId` / `interaction` / `eventId` populated."}},"required":["key","count","sampleEvent"],"type":"object"},"CurrencyAwareValue":{"description":"A monetary value in the platform's standard price shape.","example":{"currency":"EUR","value":1200000,"valueDecimal":12.0},"properties":{"currency":{"description":"ISO 4217 currency code.","type":"string"},"value":{"description":"Integer price scaled by 100000 (the platform-wide decimal scale).","format":"int64","type":"integer"},"valueDecimal":{"description":"Decimal price ready for display (`value / 100000`).","format":"double","type":"number"}},"required":["currency","value","valueDecimal"],"type":"object"},"DailyBusinessHours":{"description":"Business hours for a single day.","properties":{"from":{"description":"Opening time (HH:MM format).","type":"string"},"isOpen":{"description":"Whether the space is open on this day.","type":"boolean"},"to":{"description":"Closing time (HH:MM format).","type":"string"}},"required":["from","to","isOpen"],"type":"object"},"DataResidency":{"description":"Data storage region for a tenant.","enum":["EU","US","AU","UAE","IN","QA","DEV"],"type":"string"},"DayAvailability":{"description":"Monitoring time window for a single day.","properties":{"endTime":{"description":"End time (HH:MM format).","type":"string"},"startTime":{"description":"Start time (HH:MM format).","type":"string"}},"required":["startTime","endTime"],"type":"object"},"DeletionBlocker":{"description":"A resource that blocks tenant deletion.","example":{"count":3,"label":"spaces","resource":"spaces"},"properties":{"count":{"description":"Number of blocking resources of this type.","format":"int32","minimum":0,"type":"integer"},"label":{"description":"Human-readable label for display.","type":"string"},"resource":{"description":"Machine-readable resource type (e.g. `spaces`, `users`, `subscription`).","type":"string"}},"required":["resource","count","label"],"type":"object"},"DeletionBlockersResponse":{"description":"Pre-flight check result for tenant deletion.","example":{"blockers":[{"count":5,"label":"users","resource":"users"},{"count":3,"label":"spaces","resource":"spaces"}],"canDelete":false},"properties":{"blockers":{"description":"Resources that must be removed before deletion is possible.","items":{"$ref":"#/components/schemas/DeletionBlocker"},"type":"array"},"canDelete":{"description":"Whether the tenant can be deleted right now.","type":"boolean"}},"required":["canDelete","blockers"],"type":"object"},"DescendantsResponse":{"description":"Response listing descendant space IDs for a given space.","properties":{"data":{"items":{"type":"string"},"type":"array"}},"required":["data"],"type":"object"},"DescriptorCreateRequest":{"description":"Request body for creating a descriptor (with its first immutable version).\n`version` must be valid semver; the version payload is immutable once written.","example":{"description":"A synthetic scanner descriptor","displayName":"Test Scanner","metaSchema":{},"name":"test-scanner","releaseNotes":"initial","schema":{},"validation":"advisory","version":"1.0.0","visibility":"own"},"properties":{"description":{"type":["string","null"]},"displayName":{"type":"string"},"metaSchema":{"description":"Companion UI meta-schema (`ui:*` hints). Required — send `{}` when there\nare no hints; the behavioral contract is `schema`."},"name":{"type":"string"},"releaseNotes":{"type":["string","null"]},"schema":{"description":"JSON-schema document for the descriptor (actions/events/desired/reported)."},"validation":{"description":"Property-validation policy: `off` | `advisory` | `strict` (defaults to\n`advisory`). Drives the downstream descriptor-property validation gate.","type":["string","null"]},"version":{"description":"Semantic version of the first descriptor version.","type":"string"},"visibility":{"description":"Visibility: `public` | `child-tenants` | `own` (defaults to `own`).","type":["string","null"]}},"required":["name","displayName","version","schema","metaSchema"],"type":"object"},"DescriptorResponse":{"description":"A descriptor — the typed, published, versioned contract a peripheral derives\nits shape from. Owned by a tenant, visibility-scoped, author-stamped.","example":{"author":"Acme","createdAt":"2026-01-01T00:00:00.000Z","description":"A synthetic scanner descriptor","displayName":"Test Scanner","id":"507f1f77bcf86cd799439012","latestVersion":"1.0.0","name":"test-scanner","tenantId":"507f1f77bcf86cd799439011","updatedAt":"2026-01-02T00:00:00.000Z","validation":"advisory","visibility":"own"},"properties":{"author":{"description":"Author display name (stamped from the owning tenant on create).","type":"string"},"createdAt":{"type":"string"},"description":{"type":"string"},"displayName":{"type":"string"},"id":{"type":"string"},"latestVersion":{"description":"Highest published version (semver); empty until the first version exists.","type":"string"},"name":{"description":"Tenant-local descriptor name (kebab-case).","type":"string"},"tenantId":{"description":"Owning tenant.","type":"string"},"updatedAt":{"type":"string"},"validation":{"description":"Property-validation policy: `off` | `advisory` | `strict` (defaults to\n`advisory`). Drives the downstream descriptor-property validation gate.","type":"string"},"visibility":{"description":"Visibility: `public` | `child-tenants` | `own`.","type":"string"}},"required":["id","name","displayName","description","tenantId","visibility","validation","author","latestVersion","createdAt","updatedAt"],"type":"object"},"DescriptorVersionCreateRequest":{"description":"Request body for publishing a new immutable version of an existing descriptor.\n`version` must be valid semver and strictly greater than the latest.","example":{"metaSchema":{},"releaseNotes":"fix","schema":{},"version":"1.1.0"},"properties":{"metaSchema":{"description":"Companion UI meta-schema (`ui:*` hints). Required — send `{}` when there\nare no hints; the behavioral contract is `schema`."},"releaseNotes":{"type":["string","null"]},"schema":{},"version":{"type":"string"}},"required":["version","schema","metaSchema"],"type":"object"},"DescriptorVersionResponse":{"description":"One immutable published version of a descriptor (its schema payload). Returned\nby `GET /descriptors/{name}/versions/{version}` — also the phyhub S2S source.","example":{"createdAt":"2026-01-01T00:00:00.000Z","metaSchema":{},"releaseNotes":"initial","schema":{},"updatedAt":"2026-01-01T00:00:00.000Z","version":"1.0.0"},"properties":{"createdAt":{"type":"string"},"metaSchema":{"description":"Companion UI meta-schema — always an object on the wire (`{}` when the\nstored doc has none: Mongoose minimize prunes hint-less sidecars on save)."},"releaseNotes":{"type":"string"},"schema":{"description":"JSON-schema document (actions/events/desiredProperties/reportedProperties)."},"updatedAt":{"type":"string"},"version":{"type":"string"}},"required":["version","schema","metaSchema","releaseNotes","createdAt","updatedAt"],"type":"object"},"DeviceApproveRequest":{"description":"Request from the console to approve a pending device flow.","properties":{"tenantId":{"description":"Optional tenant to scope the minted token to (multi-tenant selection).","type":["string","null"]},"userCode":{"description":"The `userCode` the user typed on the approval page.","type":"string"}},"required":["userCode"],"type":"object"},"DeviceApproveResponse":{"description":"Response confirming the approval, echoing the client label for display.","properties":{"clientName":{"description":"The client label from the original `authorize` request, if any.","type":["string","null"]},"ok":{"type":"boolean"}},"required":["ok"],"type":"object"},"DeviceAuthorizeRequest":{"description":"Request to start a device-authorization flow.","properties":{"clientName":{"description":"Human-readable label for the requesting client, shown on the approval\npage (e.g. \"phy CLI on frz-mbp\"). Optional.","type":["string","null"]}},"type":"object"},"DeviceAuthorizeResponse":{"description":"Response that bootstraps the CLI poll loop (RFC 8628 §3.2).","properties":{"deviceCode":{"description":"Opaque code the CLI presents when polling `device/token`. This is the\nsecret — keep it off the screen.","type":"string"},"expiresIn":{"description":"Seconds until the flow expires.","format":"int64","minimum":0,"type":"integer"},"interval":{"description":"Minimum seconds the CLI must wait between `device/token` polls.","format":"int64","minimum":0,"type":"integer"},"userCode":{"description":"Short, human-typable code the user enters on the console approval page.","type":"string"},"verificationUri":{"description":"URL the user visits to approve (the console `/device` page).","type":"string"},"verificationUriComplete":{"description":"`verification_uri` with the `user_code` pre-filled, for browser-open.","type":"string"}},"required":["deviceCode","userCode","verificationUri","verificationUriComplete","expiresIn","interval"],"type":"object"},"DeviceCommandResponse":{"description":"Result of a device command (e.g. reboot): which devices accepted the command\nand which failed to be queued.","properties":{"failedDeviceIds":{"items":{"type":"string"},"type":"array"},"successDeviceIds":{"items":{"type":"string"},"type":"array"}},"required":["successDeviceIds","failedDeviceIds"],"type":"object"},"DeviceCreateRequest":{"description":"Request body for creating a new device.","example":{"deviceSerial":"PHY-2025-V4K1","displayName":"vision-processor-04","spaceId":"507f1f77bcf86cd799439022","tenantId":"507f1f77bcf86cd799439011"},"properties":{"deviceSerial":{"description":"Hardware serial number or provisioning code.","type":["string","null"]},"displayName":{"type":"string"},"env":{"description":"Deployment environment.","example":"prod","type":["string","null"]},"notes":{"type":["string","null"]},"spaceId":{"description":"Space to assign the device to.","type":["string","null"]},"tags":{"items":{"$ref":"#/components/schemas/Tag"},"type":"array"},"tenantId":{"type":"string"},"timezone":{"type":["string","null"]}},"required":["displayName","tenantId"],"type":"object"},"DeviceMonitoring":{"description":"Device monitoring configuration.","properties":{"expectedAvailability":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/ExpectedAvailability"}]},"isEnabled":{"type":"boolean"}},"required":["isEnabled"],"type":"object"},"DeviceOs":{"description":"Device operating system information.","properties":{"arch":{"type":["string","null"]},"type":{"type":["string","null"]},"version":{"type":["string","null"]}},"type":"object"},"DeviceResponse":{"description":"A device in the Phystack platform.","example":{"createdAt":"2025-01-10T09:00:00.000Z","deviceSerial":"PHY-2025-A7X3","displayName":"edge-node-alpha-7","id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","installationIds":["507f1f77bcf86cd799439044"],"spaceId":"507f1f77bcf86cd799439022","status":"ONLINE","tags":[],"tenantId":"507f1f77bcf86cd799439011","timezone":"America/Los_Angeles","updatedAt":"2025-01-10T09:00:00.000Z"},"properties":{"createdAt":{"type":"string"},"dailyRebootTime":{"type":["string","null"]},"descriptors":{"items":{},"type":["array","null"]},"deviceSerial":{"description":"Hardware serial number.","type":["string","null"]},"displayName":{"description":"Human-readable device name.","type":"string"},"externalId":{"description":"Human-friendly external identifier / slug (e.g. `hobart-cbd7012`).\nSurfaced to clients that render the slug alongside the display name\n(monitoring history rows, device matrix cells).","type":["string","null"]},"id":{"type":"string"},"installationIds":{"items":{"type":"string"},"type":"array"},"installationsInfo":{},"isTimezoneOverridden":{"type":["boolean","null"]},"monitoring":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/DeviceMonitoring"}]},"notes":{"type":["string","null"]},"os":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/DeviceOs"}]},"reportedTelemetry":{},"screen":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/DeviceScreen","description":"Display properties (only present for SCREEN devices)."}]},"screenshotUrl":{"type":["string","null"]},"spaceId":{"description":"Space the device is assigned to.","type":["string","null"]},"status":{"$ref":"#/components/schemas/DeviceStatus","description":"Device connection status."},"statusHistory":{"type":["string","null"]},"statusUpdatedAt":{"type":["string","null"]},"tags":{"items":{"$ref":"#/components/schemas/Tag"},"type":"array"},"tenantId":{"description":"Tenant that owns this device.","type":"string"},"timezone":{"type":["string","null"]},"updatedAt":{"type":"string"}},"required":["id","tenantId","displayName","status","createdAt","updatedAt"],"type":"object"},"DeviceScreen":{"description":"Display properties for SCREEN devices.","properties":{"scale":{"type":["string","null"]},"screenOrientation":{"type":["string","null"]},"screenResolution":{"type":["string","null"]},"soundInput":{"type":["string","null"]},"soundOutput":{"type":["string","null"]}},"type":"object"},"DeviceShellRequest":{"description":"Request body to open a shell tunnel to a device.","properties":{"key":{"description":"SSH public key the device authorizes for this session.","type":"string"}},"required":["key"],"type":"object"},"DeviceShellResponse":{"description":"Connection info for a device shell tunnel.","properties":{"auth":{"description":"Bearer token for the device-streams WebSocket tunnel.","type":"string"},"isPhyos":{"description":"Whether the target runs PhyOS — selects the SSH username on the client.","type":"boolean"},"url":{"description":"WebSocket URL the client connects to (the client end of the tunnel).","type":"string"}},"required":["auth","url","isPhyos"],"type":"object"},"DeviceStatus":{"description":"Device connection status.","enum":["ONLINE","OFFLINE","OK"],"type":"string"},"DeviceTokenError":{"description":"Pending/error response while the flow is not yet complete. Uses RFC 8628\nerror strings verbatim so the CLI poll loop is textbook.","properties":{"error":{"description":"RFC 8628 §3.5 error code: `authorization_pending` (user has not yet\napproved), `slow_down` (poll less often), `access_denied` (user\nrejected), or `expired_token` (flow timed out).","type":"string"}},"required":["error"],"type":"object"},"DeviceTokenRequest":{"description":"Request the CLI sends when polling for the minted token. The `deviceCode`\nis the credential — no other auth is required or accepted.","properties":{"deviceCode":{"description":"The `deviceCode` returned by `device/authorize`.","type":"string"}},"required":["deviceCode"],"type":"object"},"DeviceTokenResponse":{"description":"Success response once the user has approved: the minted Personal Access\nToken, shown once.","properties":{"expiry":{"description":"ISO 8601 expiry.","type":"string"},"token":{"description":"The plaintext PAT — shown ONCE; cannot be retrieved again.","type":"string"},"tokenId":{"description":"ObjectId hex of the minted token (for later listing/revocation).","type":"string"}},"required":["token","tokenId","expiry"],"type":"object"},"DeviceUpdateRequest":{"description":"Request body for updating an existing device.","example":{"displayName":"edge-node-alpha-7-v2","notes":"Upgraded to 32GB RAM, CUDA 12"},"properties":{"dailyRebootTime":{"type":["string","null"]},"displayName":{"type":["string","null"]},"notes":{"type":["string","null"]},"spaceId":{"type":["string","null"]},"tags":{"items":{"$ref":"#/components/schemas/Tag"},"type":["array","null"]},"timezone":{"type":["string","null"]}},"type":"object"},"DisplayNameFacet":{"description":"A single distinct displayName and the number of spaces carrying it.","properties":{"count":{"format":"int64","minimum":0,"type":"integer"},"value":{"type":"string"}},"required":["value","count"],"type":"object"},"DisplayNameFacetsResponse":{"description":"Response envelope for the display-name-facets aggregate.","properties":{"data":{"items":{"$ref":"#/components/schemas/DisplayNameFacet"},"type":"array"}},"required":["data"],"type":"object"},"EffectiveTimezoneResponse":{"description":"Response envelope for `GET /spaces/{id}/effective-timezone`.","properties":{"source":{"$ref":"#/components/schemas/EffectiveTimezoneSource","description":"Where the timezone came from."},"sourceSpaceId":{"description":"Space that supplied the timezone (only when `source = \"space\"`).","type":["string","null"]},"tenantTimezone":{"description":"The tenant's own timezone — returned even when a space ancestor wins,\nso callers can display the fallback that would otherwise have applied.","type":"string"},"timezone":{"description":"The resolved IANA timezone to use for hour/date bucketing on this space.","type":"string"}},"required":["timezone","source","tenantTimezone"],"type":"object"},"EffectiveTimezoneSource":{"description":"Where `effective-timezone` came from.","enum":["space","tenant","fallback"],"type":"string"},"ErrorBody":{"properties":{"code":{"description":"Machine-readable error code (e.g. `not_found`, `bad_request`).","example":"not_found","type":"string"},"details":{"description":"Structured, machine-readable error details — e.g.\n`{ \"validationErrors\": [\"/field must be <= 10\"] }` on a 422. Omitted\nentirely when there are none (additive for existing consumers)."},"message":{"description":"Human-readable error message.","example":"Tenant not found","type":"string"},"requestId":{"description":"Unique identifier for this request — include when contacting support.","example":"018f1a2b-3c4d-7abc-def0-123456789abc","type":"string"}},"required":["code","message","requestId"],"type":"object"},"ErrorResponse":{"description":"Standard error envelope returned by all 4xx and 5xx responses.","properties":{"error":{"$ref":"#/components/schemas/ErrorBody"}},"required":["error"],"type":"object"},"EventsMeta":{"description":"Metadata for raw events response.","properties":{"dateFrom":{"type":"string"},"dateTo":{"type":"string"}},"required":["dateFrom","dateTo"],"type":"object"},"EventsQueryParams":{"description":"Query parameters for the raw events endpoint.","properties":{"dateFrom":{"description":"Start date (YYYY-MM-DD, required)","example":"2026-03-01","format":"date","type":"string"},"dateTo":{"description":"End date (YYYY-MM-DD, required, max 31 days from dateFrom)","example":"2026-03-31","format":"date","type":"string"},"deviceId":{"description":"Filter by device ID","type":["string","null"]},"eventType":{"description":"Filter by event type","type":["string","null"]},"installationId":{"description":"Filter by installation ID","type":["string","null"]},"page":{"description":"Page number (default 1)","format":"int32","minimum":1,"type":["integer","null"]},"pageSize":{"description":"Rows per page (default 1000, max 10000)","format":"int32","maximum":10000,"minimum":1,"type":["integer","null"]},"spaceId":{"description":"Filter by space ID","type":["string","null"]}},"required":["dateFrom","dateTo"],"type":"object"},"EventsResponse":{"description":"Response from the raw events endpoint.","properties":{"data":{"description":"Raw event rows enriched with session data","items":{"$ref":"#/components/schemas/RawEventRow"},"type":"array"},"meta":{"$ref":"#/components/schemas/EventsMeta","description":"Query metadata"},"pagination":{"$ref":"#/components/schemas/PaginationMeta","description":"Pagination info"}},"required":["data","pagination","meta"],"type":"object"},"ExpectedAvailability":{"description":"Expected device availability schedule (per day of week).","properties":{"friday":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/DayAvailability"}]},"monday":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/DayAvailability"}]},"saturday":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/DayAvailability"}]},"sunday":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/DayAvailability"}]},"thursday":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/DayAvailability"}]},"tuesday":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/DayAvailability"}]},"wednesday":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/DayAvailability"}]}},"type":"object"},"FilterValue":{"description":"Filter value for [`CardPaginationParams::filter`]. Accepts a single string\n(equality) or an array of strings (IN-list match). The wire shape is\n`\"foo\"` or `[\"a\",\"b\"]`.","oneOf":[{"description":"Single value — equality match.","type":"string"},{"description":"Multiple values — IN-list match. Empty array is treated as no filter.","items":{"type":"string"},"type":"array"}]},"InstallationActivationRequest":{"additionalProperties":false,"description":"Request body for activating / deactivating an installation in a Space.\n\nRPC-style: `spaceId` rides the body, not the URL. Activate creates a\ncloud-app twin for `(installationId, spaceId)`; deactivate soft-deletes\nit. Both are idempotent.","example":{"spaceId":"650abc1234ef567890123456"},"properties":{"spaceId":{"description":"Space `_id` to activate / deactivate this Installation in.","type":"string"}},"required":["spaceId"],"type":"object"},"InstallationCreateRequest":{"additionalProperties":false,"description":"Request body for creating a new installation.\n\nThe caller identifies the App template by `appId`. Language configuration\nand `externalId` are set after create with `PUT /installations/{id}`.","example":{"appBuildId":"65a14f3d8e7c2a0008d4e1b9","appId":"5fd8dfe9a1d0eb0a63536096","displayName":"Anomaly Detector","tenantId":"507f1f77bcf86cd799439011"},"properties":{"appBuildId":{"description":"Pin to a specific build of `appId`. Defaults to the App's latest build.\nWhen provided, the build must belong to `appId` (`422` otherwise).","type":["string","null"]},"appId":{"description":"ID of the App template to install. Required and immutable after create.","type":"string"},"displayName":{"type":"string"},"installationGroupId":{"type":["string","null"]},"tenantId":{"type":"string"}},"required":["tenantId","appId","displayName"],"type":"object"},"InstallationResponse":{"description":"An installation in the Phystack platform (an App deployed to a tenant).","example":{"appId":"5fd8dfe9a1d0eb0a63536096","appType":"EDGE","createdAt":"2025-01-10T09:00:00.000Z","defaultLanguage":"en","displayName":"Perimeter Vision AI","iconUrl":"https://media.omborigrid.com/.../icon.png","id":"507f1f77bcf86cd799439044","installationSlug":"perimeter-vision-ai-3f8d","multiLanguageSupport":false,"sku":"GAC001","supportedLanguages":["en"],"tenantId":"507f1f77bcf86cd799439011","updatedAt":"2025-01-10T09:00:00.000Z","version":3},"properties":{"analyticsSchema":{"description":"Analytics dashboard schema (only present when `include=analyticsSchema`)."},"appId":{"description":"ID of the App this installation is an instance of.","type":["string","null"]},"appType":{"$ref":"#/components/schemas/AppType","description":"App platform / runtime category."},"createdAt":{"type":"string"},"defaultLanguage":{"type":"string"},"displayName":{"type":"string"},"externalId":{"type":["string","null"]},"iconUrl":{"description":"Resolved icon URL for the installation. Falls back to a generic icon\nwhen the App does not define one.","type":"string"},"id":{"type":"string"},"installationGroupId":{"type":["string","null"]},"installationSlug":{"description":"Auto-generated unique slug.","type":"string"},"multiLanguageSupport":{"type":"boolean"},"sku":{"description":"Product SKU code identifying the App variant.","type":["string","null"]},"supportedLanguages":{"items":{"type":"string"},"type":"array"},"tenantId":{"type":"string"},"updatedAt":{"type":"string"},"version":{"description":"OCC version; `null` for pre-2026-03 docs (echo back on `PUT`).","format":"int32","minimum":0,"type":["integer","null"]}},"required":["id","tenantId","installationSlug","displayName","appType","iconUrl","multiLanguageSupport","defaultLanguage","createdAt","updatedAt"],"type":"object"},"InstallationSettingsResponse":{"description":"Schema-driven settings for an installation — the pinned `appBuildId` and\nform `values` validated against the build's settings schema.","example":{"appBuildId":"65a14f3d8e7c2a0008d4e1b9","updatedAt":"2026-05-04T14:22:00.000Z","values":{"showQrCode":true,"theme":"dark","welcomeMessage":"Hi there!"},"version":5},"properties":{"appBuildId":{"description":"ID of the App build the installation is pinned to.","type":"string"},"updatedAt":{"type":"string"},"values":{"description":"Schema-driven form data, validated against the build's settings schema."},"version":{"description":"Current resource version. Pass this back on `PUT` calls for optimistic\nconcurrency — a mismatch returns `409 Conflict`.","format":"int32","minimum":0,"type":"integer"}},"required":["appBuildId","values","version","updatedAt"],"type":"object"},"InstallationSettingsUpdateRequest":{"additionalProperties":false,"description":"Request body for `PUT /installations/{id}/settings`.","example":{"appBuildId":"65a14f3d8e7c2a0008d4e1b9","values":{"showQrCode":true,"theme":"dark","welcomeMessage":"Hi there!"},"version":5},"properties":{"appBuildId":{"type":"string"},"values":{},"version":{"description":"Current resource version — required for optimistic concurrency. A\nmismatch returns `409 Conflict`.","format":"int32","minimum":0,"type":"integer"}},"required":["appBuildId","values","version"],"type":"object"},"InstallationUpdateRequest":{"additionalProperties":false,"description":"Request body for updating an existing installation.\n\nMetadata-only — the pinned `appBuildId` and schema-driven `values` are\nmanaged on the `/installations/{id}/settings` sub-resource.","example":{"defaultLanguage":"en","displayName":"Anomaly Detector v2","multiLanguageSupport":false,"supportedLanguages":["en"],"version":3},"properties":{"defaultLanguage":{"type":["string","null"]},"displayName":{"type":["string","null"]},"externalId":{"type":["string","null"]},"installationGroupId":{"type":["string","null"]},"multiLanguageSupport":{"type":["boolean","null"]},"supportedLanguages":{"items":{"type":"string"},"type":["array","null"]},"version":{"description":"OCC version. `null` opts out of the check and bootstraps the field.","format":"int32","minimum":0,"type":["integer","null"]}},"type":"object"},"InvitationCreateRequest":{"additionalProperties":false,"description":"Request body for creating an invitation.\n\nThe invitee is identified by `email`; access is described by one or more\n`tenants` grants. An email is sent on success.","example":{"email":"new.operator@example.com","tenants":[{"hasAccessToAllSpaces":true,"roleId":"operator","tenantId":"507f1f77bcf86cd799439011"}],"theme":"phystack"},"properties":{"email":{"description":"Email address of the person being invited.","type":"string"},"firstName":{"type":["string","null"]},"lastName":{"type":["string","null"]},"tenants":{"description":"One or more tenant + role grants the invitee receives on acceptance.","items":{"$ref":"#/components/schemas/InvitationMembershipRequest"},"type":"array"},"theme":{"description":"Branding theme for the invite email and console — `phystack | itab`.","type":"string"}},"required":["email","theme","tenants"],"type":"object"},"InvitationMembershipRequest":{"description":"A requested tenant + role grant in a create-invitation request.","properties":{"hasAccessToAllSpaces":{"description":"When `true` (default), the invitee gets access to every Space in the tenant.","type":["boolean","null"]},"roleId":{"description":"Role to assign within that tenant.","type":"string"},"spacesAccess":{"description":"Explicit Space ids the invitee may access when not all-Spaces.","items":{"type":"string"},"type":["array","null"]},"tenantId":{"description":"Tenant the invitee should be granted access to.","type":"string"}},"required":["tenantId","roleId"],"type":"object"},"InvitationResponse":{"description":"A pending invitation to join one or more tenants.","properties":{"createdAt":{"type":["string","null"]},"email":{"type":"string"},"expiresAt":{"type":["string","null"]},"id":{"type":"string"},"status":{"description":"`PENDING | ACCEPTED | EXPIRED`. A pending invite past its expiry is\nreported as `EXPIRED`.","type":"string"},"tenantId":{"description":"Convenience: the first membership's tenant. The full set lives in `tenants`.","type":["string","null"]},"tenants":{"items":{"$ref":"#/components/schemas/InvitationTenantMembership"},"type":"array"},"updatedAt":{"type":["string","null"]}},"required":["id","email","status","tenants"],"type":"object"},"InvitationTenantMembership":{"description":"An invitation's membership grant for a single tenant.","properties":{"hasAccessToAllSpaces":{"type":"boolean"},"roleId":{"type":["string","null"]},"spaceIds":{"items":{"type":"string"},"type":"array"},"tenantId":{"type":["string","null"]}},"required":["hasAccessToAllSpaces","spaceIds"],"type":"object"},"InvoiceResponse":{"description":"A system-generated invoice for a tenant.","properties":{"amount":{"format":"double","type":["number","null"]},"billingReason":{"type":["string","null"]},"currency":{"type":["string","null"]},"date":{"type":["string","null"]},"description":{"type":["string","null"]},"documentUrl":{"description":"Document download path, or null when the invoice has no document.","type":["string","null"]},"id":{"type":"string"},"status":{"type":["string","null"]},"tenantId":{"type":["string","null"]}},"required":["id"],"type":"object"},"LocationFanoutParams":{"description":"Parameters for the Store Breakdown server-side fanout. `groupId` is the\n16-char hex group id derived from the schema; `spaceIds` is the explicit\nlist of LOCATION space IDs to fan out for. The server returns one response\nentry per ID in the order requested. Maximum 100 IDs per request.","properties":{"groupId":{"description":"Stable 16-char hex group id the fanout expands.","type":"string"},"spaceIds":{"description":"Explicit list of LOCATION space ids to compute per-space results for.","items":{"type":"string"},"maxItems":100,"minItems":1,"type":"array"}},"required":["groupId","spaceIds"],"type":"object"},"ManagedTenantCreateRequest":{"description":"Request body for creating a managed (child) tenant under a parent.","example":{"country":"SE","supportPlanId":"63b1a0c23e402e000bbf7c5d","tenantDisplayName":"Horizon Robotics Nordics","tenantPlanId":"63b1a0d05b0726000889aed4"},"properties":{"country":{"description":"ISO 3166-1 alpha-2 country code.","type":"string"},"supportPlanId":{"description":"Support plan for the child. Must be one of the plans returned by\n`GET /tenants/{parentTenantId}/available-child-plans?type=support`.","type":"string"},"tenantDisplayName":{"description":"Human-readable name for the new tenant.","type":"string"},"tenantPlanId":{"description":"Tenant plan for the child. Must be one of the plans returned by\n`GET /tenants/{parentTenantId}/available-child-plans?type=tenant`.","type":"string"}},"required":["tenantDisplayName","country","tenantPlanId","supportPlanId"],"type":"object"},"MediaBreadcrumb":{"description":"A breadcrumb entry (root → current folder).","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"type":"object"},"MediaCompleteRequest":{"additionalProperties":false,"description":"Request body for `POST /media/{assetId}/complete` (presigned-upload step 2).","example":{"contentType":"image/png","name":"logo.png","parentId":"507f1f77bcf86cd799439001","size":20480,"tenantId":"507f1f77bcf86cd799439000"},"properties":{"contentType":{"type":"string"},"name":{"type":"string"},"parentId":{"type":["string","null"]},"size":{"format":"int64","minimum":0,"type":["integer","null"]},"tenantId":{"type":"string"}},"required":["tenantId","name","contentType"],"type":"object"},"MediaFolderCreateRequest":{"additionalProperties":false,"description":"Request body for creating a folder.","example":{"name":"Brand assets","parentId":"507f1f77bcf86cd799439001","tenantId":"507f1f77bcf86cd799439000"},"properties":{"name":{"type":"string"},"parentId":{"description":"Parent folder; absent creates the folder at the tenant root.","type":["string","null"]},"tenantId":{"type":"string"}},"required":["tenantId","name"],"type":"object"},"MediaListResponse":{"description":"Folder browse / search envelope.","example":{"breadcrumb":[{"id":"507f1f77bcf86cd799439001","name":"Brand assets"}],"items":[]},"properties":{"breadcrumb":{"description":"Root → current-folder trail. Empty at the tenant root or for a search.","items":{"$ref":"#/components/schemas/MediaBreadcrumb"},"type":"array"},"items":{"items":{"$ref":"#/components/schemas/MediaResponse"},"type":"array"}},"required":["items"],"type":"object"},"MediaResponse":{"description":"A media asset or folder.","example":{"ancestors":["507f1f77bcf86cd799439001"],"createdAt":"2026-01-10T09:00:00.000Z","id":"507f1f77bcf86cd799439011","kind":"asset","name":"logo.png","parentId":"507f1f77bcf86cd799439001","size":20480,"tenantId":"507f1f77bcf86cd799439000","type":"image/png","updatedAt":"2026-01-10T09:00:00.000Z","url":"https://api.phygrid.com/regions/eu/media/v1/assets/media/507f1f77bcf86cd799439000/abc"},"properties":{"ancestors":{"description":"Root → parent breadcrumb ids.","items":{"type":"string"},"type":"array"},"createdAt":{"type":["string","null"]},"id":{"description":"Media id.","type":"string"},"kind":{"description":"`\"folder\"` or `\"asset\"`.","type":"string"},"name":{"description":"Display name (file name or folder name).","type":"string"},"parentId":{"description":"Parent folder id; absent at the tenant root.","type":["string","null"]},"size":{"description":"Byte size (assets only).","format":"int64","minimum":0,"type":["integer","null"]},"tenantId":{"description":"Owning tenant.","type":["string","null"]},"thumbnailUrl":{"description":"CDN thumbnail URL (video assets only).","type":["string","null"]},"type":{"description":"`\"folder\"` for folders, otherwise the asset's MIME type (e.g. `image/png`).","type":"string"},"updatedAt":{"type":["string","null"]},"url":{"description":"CDN download URL (assets only).","type":["string","null"]}},"required":["id","name","type","kind"],"type":"object"},"MediaUpdateRequest":{"additionalProperties":false,"description":"Request body for renaming and/or moving a media item.\n\nAt least one field must be present. `parentId: null` moves to the tenant root.","example":{"name":"renamed.png","parentId":"507f1f77bcf86cd799439002"},"properties":{"name":{"type":["string","null"]},"parentId":{"description":"New parent folder id; `null` moves the item to the tenant root."}},"type":"object"},"MediaUploadRequest":{"additionalProperties":false,"description":"Request body for `POST /media/uploads` (presigned-upload step 1).","example":{"contentType":"image/png","name":"logo.png","parentId":"507f1f77bcf86cd799439001","size":20480,"tenantId":"507f1f77bcf86cd799439000"},"properties":{"contentType":{"description":"Asset MIME type (e.g. `image/png`). Must not be `folder`.","type":"string"},"name":{"type":"string"},"parentId":{"type":["string","null"]},"size":{"format":"int64","minimum":0,"type":["integer","null"]},"tenantId":{"type":"string"}},"required":["tenantId","name","contentType"],"type":"object"},"MediaUploadTarget":{"description":"Presigned-upload target returned by `POST /media/uploads`.","example":{"assetId":"5f8d0a3e-1234-4abc-9def-0123456789ab","blobType":"BlockBlob","uploadUrl":"https://account.blob.core.windows.net/container/tenant/uuid?sv=...&sig=..."},"properties":{"assetId":{"description":"Pre-allocated asset id (the blob uuid). Pass to the complete call.","type":"string"},"blobType":{"description":"Required header value for the Azure block-blob PUT (`x-ms-blob-type`).","type":"string"},"uploadUrl":{"description":"Fully-qualified PUT URL including the SAS query string.","type":"string"}},"required":["assetId","uploadUrl","blobType"],"type":"object"},"OverrideOp":{"additionalProperties":false,"description":"A single JSON-Pointer override operation.\n\n`set` carries a `value`; `remove` omits it. `path` is a non-root RFC 6901\nJSON Pointer (e.g. `/theme/color`, `/items/0`).","example":{"op":"set","path":"/theme/color","value":"red"},"properties":{"op":{"description":"`set` (insert or replace) or `remove`.","type":"string"},"path":{"description":"RFC 6901 JSON Pointer to the target property.","type":"string"},"value":{"description":"Required for `set`, omitted for `remove`."}},"required":["op","path"],"type":"object"},"OverrideResponse":{"description":"The stored override for a Space or Device — a JSON-Pointer delta plus its\noptimistic-concurrency version.","example":{"ops":[{"op":"set","path":"/theme/color","value":"red"},{"op":"remove","path":"/legacy/flag"}],"version":3},"properties":{"ops":{"description":"The override operations layered over the installation base.","items":{"$ref":"#/components/schemas/OverrideOp"},"type":"array"},"version":{"description":"Snapshot version; `0` when no override exists yet. Pass it back on\n`PUT` for optimistic concurrency (409 on mismatch).","format":"int32","minimum":0,"type":"integer"}},"required":["ops","version"],"type":"object"},"OverrideUpdateRequest":{"additionalProperties":false,"description":"Request body for `PUT .../settings`.","example":{"ops":[{"op":"set","path":"/theme/color","value":"red"},{"op":"remove","path":"/legacy/flag"}],"version":3},"properties":{"ops":{"description":"JSON-Pointer override operations to apply.","items":{"$ref":"#/components/schemas/OverrideOp"},"type":"array"},"version":{"description":"Current snapshot version — required for optimistic concurrency, a\nmismatch returns `409 Conflict`. GET returns `0` before the first\nwrite; pass that back verbatim, so creating an override is\nconcurrency-checked like every later update.","format":"int32","minimum":0,"type":"integer"}},"required":["ops","version"],"type":"object"},"PaginatedResponse_AppBuildListItem":{"description":"Paginated response envelope for list endpoints.","properties":{"data":{"items":{"description":"One row in the version-selector dropdown.\n\nLightweight on purpose: schemas live on [`AppBuildDetailResponse`] and are\nonly fetched when the user actually selects a version.","example":{"createdAt":"2026-01-10T09:00:00.000Z","id":"5fd8dfe9a1d0eb0a63536101","status":"published","version":"1.4.2"},"properties":{"createdAt":{"type":"string"},"id":{"type":"string"},"status":{"$ref":"#/components/schemas/AppBuildStatus","description":"Lifecycle status."},"version":{"description":"Free-form version label set by the build pipeline (commonly semver).\n`null` when the build's `result` sub-doc hasn't been populated yet\n(status `processing` / `error`).","type":["string","null"]}},"required":["id","status","createdAt"],"type":"object"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","pagination"],"type":"object"},"PaginatedResponse_AppResponse":{"description":"Paginated response envelope for list endpoints.","properties":{"data":{"items":{"description":"An app template in the Phystack platform.","example":{"createdAt":"2025-01-10T09:00:00.000Z","description":"Edge computer vision with real-time object detection, tracking, and analytics","displayName":"Vision AI","id":"507f1f77bcf86cd799439055","isPublished":true,"packageName":"@phystack/vision-ai","type":"CONTAINER","updatedAt":"2025-01-10T09:00:00.000Z"},"properties":{"access":{"$ref":"#/components/schemas/AppAccess","description":"App access level."},"createdAt":{"type":"string"},"description":{"type":["string","null"]},"displayName":{"type":"string"},"iconUrl":{"type":["string","null"]},"id":{"type":"string"},"isPublished":{"description":"Whether the app is published in the catalog.","type":"boolean"},"packageName":{"description":"Package identifier for the app (e.g. `@phystack/virtual-queues`).","type":"string"},"type":{"$ref":"#/components/schemas/AppType","description":"App category."},"updatedAt":{"type":"string"}},"required":["id","packageName","displayName","type","isPublished","createdAt","updatedAt"],"type":"object"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","pagination"],"type":"object"},"PaginatedResponse_CloudInstanceResponse":{"description":"Paginated response envelope for list endpoints.","properties":{"data":{"items":{"description":"A cloud-app instance — the slim, public-API projection of a phyhub cloud\ntwin. One row per `(installationId, spaceId)` pair. The deeper twin doc\nstays internal to phyhub; consumers only see the fields below.","example":{"appId":"5fd8dfe9a1d0eb0a63536096","appName":"acme/cloud-app","appVersion":"1.0.0","buildId":"65a14f3d8e7c2a0008d4e1b9","createdAt":"2026-03-01T09:00:00.000Z","id":"650abc1234ef567890123456","installationId":"507f1f77bcf86cd799439044","spaceId":"507f1f77bcf86cd799439022","status":"Online","tenantId":"507f1f77bcf86cd799439011","updatedAt":"2026-03-01T09:00:00.000Z"},"properties":{"appId":{"type":"string"},"appName":{"type":"string"},"appVersion":{"type":"string"},"buildId":{"type":["string","null"]},"createdAt":{"type":"string"},"id":{"type":"string"},"installationId":{"type":"string"},"lastStatusUpdatedAt":{"type":["string","null"]},"spaceId":{"type":"string"},"status":{"type":"string"},"tenantId":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","tenantId","installationId","appId","appName","appVersion","spaceId","status","createdAt","updatedAt"],"type":"object"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","pagination"],"type":"object"},"PaginatedResponse_ContentTagResponse":{"description":"Paginated response envelope for list endpoints.","properties":{"data":{"items":{"description":"A content tag — a tenant-scoped label (optionally multi-select) used to\nclassify content.","example":{"createdAt":"2025-01-10T09:00:00.000Z","description":"Seasonal campaign tag","id":"650abc1234ef567890123456","name":"Season","slug":"season","tenantId":"507f1f77bcf86cd799439011","type":"multi-select","updatedAt":"2025-01-10T09:00:00.000Z","values":["spring","summer","autumn","winter"]},"properties":{"createdAt":{"type":["string","null"]},"description":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"slug":{"description":"URL-safe identifier derived from `name`.","type":"string"},"tenantId":{"type":"string"},"type":{"description":"Input type discriminator — present only for tags carrying `values`.","type":["string","null"]},"updatedAt":{"type":["string","null"]},"values":{"description":"Allowed values for a multi-select tag. Absent for free-form tags.","items":{"type":"string"},"type":["array","null"]}},"required":["id","tenantId","name","slug","description"],"type":"object"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","pagination"],"type":"object"},"PaginatedResponse_DescriptorResponse":{"description":"Paginated response envelope for list endpoints.","properties":{"data":{"items":{"description":"A descriptor — the typed, published, versioned contract a peripheral derives\nits shape from. Owned by a tenant, visibility-scoped, author-stamped.","example":{"author":"Acme","createdAt":"2026-01-01T00:00:00.000Z","description":"A synthetic scanner descriptor","displayName":"Test Scanner","id":"507f1f77bcf86cd799439012","latestVersion":"1.0.0","name":"test-scanner","tenantId":"507f1f77bcf86cd799439011","updatedAt":"2026-01-02T00:00:00.000Z","validation":"advisory","visibility":"own"},"properties":{"author":{"description":"Author display name (stamped from the owning tenant on create).","type":"string"},"createdAt":{"type":"string"},"description":{"type":"string"},"displayName":{"type":"string"},"id":{"type":"string"},"latestVersion":{"description":"Highest published version (semver); empty until the first version exists.","type":"string"},"name":{"description":"Tenant-local descriptor name (kebab-case).","type":"string"},"tenantId":{"description":"Owning tenant.","type":"string"},"updatedAt":{"type":"string"},"validation":{"description":"Property-validation policy: `off` | `advisory` | `strict` (defaults to\n`advisory`). Drives the downstream descriptor-property validation gate.","type":"string"},"visibility":{"description":"Visibility: `public` | `child-tenants` | `own`.","type":"string"}},"required":["id","name","displayName","description","tenantId","visibility","validation","author","latestVersion","createdAt","updatedAt"],"type":"object"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","pagination"],"type":"object"},"PaginatedResponse_DeviceResponse":{"description":"Paginated response envelope for list endpoints.","properties":{"data":{"items":{"description":"A device in the Phystack platform.","example":{"createdAt":"2025-01-10T09:00:00.000Z","deviceSerial":"PHY-2025-A7X3","displayName":"edge-node-alpha-7","id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","installationIds":["507f1f77bcf86cd799439044"],"spaceId":"507f1f77bcf86cd799439022","status":"ONLINE","tags":[],"tenantId":"507f1f77bcf86cd799439011","timezone":"America/Los_Angeles","updatedAt":"2025-01-10T09:00:00.000Z"},"properties":{"createdAt":{"type":"string"},"dailyRebootTime":{"type":["string","null"]},"descriptors":{"items":{},"type":["array","null"]},"deviceSerial":{"description":"Hardware serial number.","type":["string","null"]},"displayName":{"description":"Human-readable device name.","type":"string"},"externalId":{"description":"Human-friendly external identifier / slug (e.g. `hobart-cbd7012`).\nSurfaced to clients that render the slug alongside the display name\n(monitoring history rows, device matrix cells).","type":["string","null"]},"id":{"type":"string"},"installationIds":{"items":{"type":"string"},"type":"array"},"installationsInfo":{},"isTimezoneOverridden":{"type":["boolean","null"]},"monitoring":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/DeviceMonitoring"}]},"notes":{"type":["string","null"]},"os":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/DeviceOs"}]},"reportedTelemetry":{},"screen":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/DeviceScreen","description":"Display properties (only present for SCREEN devices)."}]},"screenshotUrl":{"type":["string","null"]},"spaceId":{"description":"Space the device is assigned to.","type":["string","null"]},"status":{"$ref":"#/components/schemas/DeviceStatus","description":"Device connection status."},"statusHistory":{"type":["string","null"]},"statusUpdatedAt":{"type":["string","null"]},"tags":{"items":{"$ref":"#/components/schemas/Tag"},"type":"array"},"tenantId":{"description":"Tenant that owns this device.","type":"string"},"timezone":{"type":["string","null"]},"updatedAt":{"type":"string"}},"required":["id","tenantId","displayName","status","createdAt","updatedAt"],"type":"object"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","pagination"],"type":"object"},"PaginatedResponse_InstallationResponse":{"description":"Paginated response envelope for list endpoints.","properties":{"data":{"items":{"description":"An installation in the Phystack platform (an App deployed to a tenant).","example":{"appId":"5fd8dfe9a1d0eb0a63536096","appType":"EDGE","createdAt":"2025-01-10T09:00:00.000Z","defaultLanguage":"en","displayName":"Perimeter Vision AI","iconUrl":"https://media.omborigrid.com/.../icon.png","id":"507f1f77bcf86cd799439044","installationSlug":"perimeter-vision-ai-3f8d","multiLanguageSupport":false,"sku":"GAC001","supportedLanguages":["en"],"tenantId":"507f1f77bcf86cd799439011","updatedAt":"2025-01-10T09:00:00.000Z","version":3},"properties":{"analyticsSchema":{"description":"Analytics dashboard schema (only present when `include=analyticsSchema`)."},"appId":{"description":"ID of the App this installation is an instance of.","type":["string","null"]},"appType":{"$ref":"#/components/schemas/AppType","description":"App platform / runtime category."},"createdAt":{"type":"string"},"defaultLanguage":{"type":"string"},"displayName":{"type":"string"},"externalId":{"type":["string","null"]},"iconUrl":{"description":"Resolved icon URL for the installation. Falls back to a generic icon\nwhen the App does not define one.","type":"string"},"id":{"type":"string"},"installationGroupId":{"type":["string","null"]},"installationSlug":{"description":"Auto-generated unique slug.","type":"string"},"multiLanguageSupport":{"type":"boolean"},"sku":{"description":"Product SKU code identifying the App variant.","type":["string","null"]},"supportedLanguages":{"items":{"type":"string"},"type":"array"},"tenantId":{"type":"string"},"updatedAt":{"type":"string"},"version":{"description":"OCC version; `null` for pre-2026-03 docs (echo back on `PUT`).","format":"int32","minimum":0,"type":["integer","null"]}},"required":["id","tenantId","installationSlug","displayName","appType","iconUrl","multiLanguageSupport","defaultLanguage","createdAt","updatedAt"],"type":"object"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","pagination"],"type":"object"},"PaginatedResponse_InvitationResponse":{"description":"Paginated response envelope for list endpoints.","properties":{"data":{"items":{"description":"A pending invitation to join one or more tenants.","properties":{"createdAt":{"type":["string","null"]},"email":{"type":"string"},"expiresAt":{"type":["string","null"]},"id":{"type":"string"},"status":{"description":"`PENDING | ACCEPTED | EXPIRED`. A pending invite past its expiry is\nreported as `EXPIRED`.","type":"string"},"tenantId":{"description":"Convenience: the first membership's tenant. The full set lives in `tenants`.","type":["string","null"]},"tenants":{"items":{"$ref":"#/components/schemas/InvitationTenantMembership"},"type":"array"},"updatedAt":{"type":["string","null"]}},"required":["id","email","status","tenants"],"type":"object"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","pagination"],"type":"object"},"PaginatedResponse_InvoiceResponse":{"description":"Paginated response envelope for list endpoints.","properties":{"data":{"items":{"description":"A system-generated invoice for a tenant.","properties":{"amount":{"format":"double","type":["number","null"]},"billingReason":{"type":["string","null"]},"currency":{"type":["string","null"]},"date":{"type":["string","null"]},"description":{"type":["string","null"]},"documentUrl":{"description":"Document download path, or null when the invoice has no document.","type":["string","null"]},"id":{"type":"string"},"status":{"type":["string","null"]},"tenantId":{"type":["string","null"]}},"required":["id"],"type":"object"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","pagination"],"type":"object"},"PaginatedResponse_PeripheralListItem":{"description":"Paginated response envelope for list endpoints.","properties":{"data":{"items":{"description":"A peripheral list row (slim). The list intentionally omits `settings` /\n`telemetry` — fetch the single peripheral for the full payload.","example":{"descriptors":{"acme.test-scanner":"1.0.0"},"deviceDisplayName":"Till 3","deviceId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","hardwareStatus":"ready","id":"650abc1234ef567890123456","name":"Front Scanner","status":"OK"},"properties":{"descriptors":{"additionalProperties":{"type":"string"},"description":"Map of descriptor uniqueName → pinned version.","propertyNames":{"type":"string"},"type":"object"},"deviceDisplayName":{"description":"Display name of the owning device (empty string when the device twin is missing).","type":"string"},"deviceId":{"type":"string"},"hardwareStatus":{"description":"Hardware status reported by the peripheral (`ready` / `unavailable` / …);\n`null` until the device first reports.","type":["string","null"]},"id":{"type":"string"},"name":{"description":"Logical peripheral name.","type":"string"},"status":{"type":"string"}},"required":["id","deviceId","name","descriptors","status","deviceDisplayName"],"type":"object"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","pagination"],"type":"object"},"PaginatedResponse_RoleResponse":{"description":"Paginated response envelope for list endpoints.","properties":{"data":{"items":{"description":"A tenant-scoped role (permission set).","properties":{"allowedRoleIdsToManage":{"items":{"type":"string"},"type":"array"},"createdAt":{"type":["string","null"]},"id":{"type":"string"},"manageAllRoles":{"type":"boolean"},"name":{"type":"string"},"permissions":{"items":{"type":"string"},"type":"array"},"tenantId":{"type":"string"},"updatedAt":{"type":["string","null"]}},"required":["id","tenantId","name","permissions","manageAllRoles","allowedRoleIdsToManage"],"type":"object"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","pagination"],"type":"object"},"PaginatedResponse_ShortUrlResponse":{"description":"Paginated response envelope for list endpoints.","properties":{"data":{"items":{"description":"A tenant-managed custom short URL.","example":{"analyticsEnabled":true,"createdAt":"2026-01-10T09:00:00.000Z","displayName":"Summer Sale Landing","friendly":false,"id":"summer-sale","redirectUrl":"https://example.com/promotions/summer","shortUrl":"https://qr.run/summer-sale","tenantId":"507f1f77bcf86cd799439011","updatedAt":"2026-01-10T09:00:00.000Z"},"properties":{"analyticsEnabled":{"description":"Whether analytics tracking is attached to this short URL.","type":"boolean"},"createdAt":{"type":["string","null"]},"displayName":{"type":["string","null"]},"friendly":{"description":"Whether the shortener generated a friendly (word-based) slug.","type":"boolean"},"id":{"description":"Slug identifier (the shortener's `customId`). Caller-chosen, stable.","type":"string"},"redirectUrl":{"description":"Destination URL the short link redirects to.","type":"string"},"shortUrl":{"description":"Fully-qualified short URL the shortener minted.","type":["string","null"]},"tenantId":{"description":"Owning tenant (`organizationId`).","type":["string","null"]},"updatedAt":{"type":["string","null"]}},"required":["id","redirectUrl","friendly","analyticsEnabled"],"type":"object"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","pagination"],"type":"object"},"PaginatedResponse_SpaceResponse":{"description":"Paginated response envelope for list endpoints.","properties":{"data":{"items":{"description":"A space in the Phystack platform (store, venue, area, etc.).","example":{"address":"800 Mission Street","city":"San Francisco","country":"US","createdAt":"2025-01-10T09:00:00.000Z","displayName":"Building A - Research Lab","id":"507f1f77bcf86cd799439022","latitude":37.7855,"longitude":-122.3965,"notes":"Edge compute research facility with 12 vision nodes","postcode":"94105","state":"CA","tags":[],"tenantId":"507f1f77bcf86cd799439011","timezone":"America/Los_Angeles","type":"LOCATION","updatedAt":"2025-01-10T09:00:00.000Z"},"properties":{"address":{"type":["string","null"]},"billingAddress":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/BillingAddress","description":"Billing address — present only when billing details were set on the\nspace (settable at create). Same shape as tenant billing."}]},"billingContact":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/SpaceBillingContact","description":"Billing contact — present only when billing details were set on the\nspace (settable at create)."}]},"children":{"description":"Transitive descendant spaces (all levels, flattened — descendants do\nnot nest their own `children`). Present only on list rows when\n`include=children` is requested; narrowed by `filter[childrenType]`.\n`no_recursion` stops utoipa's schema generation from walking the\nself-reference forever (stack overflow at doc build otherwise).","items":{"$ref":"#/components/schemas/SpaceResponse"},"type":["array","null"]},"city":{"type":["string","null"]},"country":{"type":["string","null"]},"createdAt":{"description":"ISO 8601 creation timestamp.","type":"string"},"displayName":{"description":"Human-readable space name.","type":"string"},"externalId":{"type":["string","null"]},"id":{"description":"Unique space identifier.","type":"string"},"latitude":{"format":"double","type":["number","null"]},"longitude":{"format":"double","type":["number","null"]},"notes":{"type":["string","null"]},"parentSpaceId":{"description":"Parent space ID for hierarchical nesting.","type":["string","null"]},"postcode":{"type":["string","null"]},"state":{"type":["string","null"]},"tags":{"items":{"$ref":"#/components/schemas/Tag"},"type":"array"},"tenantId":{"description":"Tenant that owns this space.","type":"string"},"timezone":{"description":"IANA timezone (e.g. Europe/Stockholm).","type":["string","null"]},"type":{"$ref":"#/components/schemas/SpaceType","description":"Space type in the hierarchy."},"updatedAt":{"description":"ISO 8601 last update timestamp.","type":"string"},"weeklySchedule":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/WeeklySchedule"}]}},"required":["id","tenantId","displayName","type","createdAt","updatedAt"],"type":"object"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","pagination"],"type":"object"},"PaginatedResponse_TenantResponse":{"description":"Paginated response envelope for list endpoints.","properties":{"data":{"items":{"description":"A tenant in the Phystack platform.","example":{"allowedUserEmailDomains":[],"billingAddress":{"addressLine1":"123 Main St","city":"Chicago","country":"US","name":"Nexus Technologies","postal":"60601"},"billingContactEmail":"billing@nexus-tech.com","billingEntity":"phystack-ie","billingType":"credit-card","country":"US","createdAt":"2025-01-10T09:00:00.000Z","currency":"USD","dataResidency":"US","enabled":true,"hasActiveSubscription":true,"id":"507f1f77bcf86cd799439011","restrictUserEmailDomains":false,"tenantDisplayName":"Nexus Technologies","tenantName":"Nexus Technologies","tenantSlug":"nexus-tech-4821","timezone":"America/Chicago","updatedAt":"2025-01-10T09:00:00.000Z","vatId":"SE556677889901"},"properties":{"allowedUserEmailDomains":{"items":{"type":"string"},"type":"array"},"billingAddress":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/BillingAddressResponse","description":"Billing address for invoicing."}]},"billingContactEmail":{"description":"Billing contact email address.","type":["string","null"]},"billingEntity":{"description":"Billing entity for invoicing.","type":["string","null"]},"billingType":{"description":"Billing type (e.g. credit-card, azure-saas, direct-invoice).","type":["string","null"]},"country":{"type":"string"},"createdAt":{"type":"string"},"currency":{"type":"string"},"dataResidency":{"$ref":"#/components/schemas/DataResidency","description":"Data storage region."},"enabled":{"description":"Whether the tenant account is active.","type":"boolean"},"hasActiveSubscription":{"description":"Whether the tenant has a current active subscription. Read-only, derived from subscription data.","type":"boolean"},"id":{"type":"string"},"parentTenantId":{"description":"Parent tenant ID for sub-tenant hierarchy.","type":["string","null"]},"restrictUserEmailDomains":{"type":"boolean"},"tenantDisplayName":{"description":"Human-readable tenant name.","type":"string"},"tenantName":{"description":"Human-readable tenant name (alias of `tenantDisplayName`).","type":"string"},"tenantSlug":{"description":"Auto-generated unique slug (e.g. acme-7294).","type":"string"},"timezone":{"type":"string"},"updatedAt":{"type":"string"},"vatId":{"description":"VAT identification number.","type":["string","null"]}},"required":["id","tenantSlug","tenantDisplayName","tenantName","country","currency","timezone","dataResidency","restrictUserEmailDomains","createdAt","updatedAt","enabled","hasActiveSubscription"],"type":"object"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","pagination"],"type":"object"},"PaginatedResponse_TokenResponse":{"description":"Paginated response envelope for list endpoints.","properties":{"data":{"items":{"description":"A personal access token's metadata (never the secret).","properties":{"createdAt":{"type":"string"},"expiry":{"description":"ISO 8601 expiry.","type":"string"},"id":{"type":"string"},"isExpired":{"type":"boolean"},"lastUsedAt":{"description":"ISO 8601 of last use, or null if never used.","type":["string","null"]},"name":{"type":"string"}},"required":["id","name","expiry","isExpired","createdAt"],"type":"object"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","pagination"],"type":"object"},"PaginatedResponse_UserResponse":{"description":"Paginated response envelope for list endpoints.","properties":{"data":{"items":{"description":"A platform user.","properties":{"createdAt":{"type":["string","null"]},"email":{"type":"string"},"firstName":{"type":["string","null"]},"id":{"type":"string"},"isSysadmin":{"type":"boolean"},"lastName":{"type":["string","null"]},"lastSeenAt":{"type":["string","null"]},"phoneNumber":{"type":["string","null"]},"preferredLanguage":{"type":["string","null"]},"tenants":{"items":{"$ref":"#/components/schemas/UserTenantMembership"},"type":"array"},"updatedAt":{"type":["string","null"]}},"required":["id","email","isSysadmin","tenants"],"type":"object"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","pagination"],"type":"object"},"PaginatedResponse_WebEndpointResponse":{"description":"Paginated response envelope for list endpoints.","properties":{"data":{"items":{"description":"A web endpoint — the slim, public-API projection of a phyhub Web twin.\nOne row per endpoint; multiple endpoints per installation are allowed\n(uniqueness key is `urlId` — the composite `{tenantSlug}/{name}` public\npath, unique per region including deleted endpoints: urlIds are\npermanent, TECH-1469).","example":{"appName":"acme/queue-status-web","appVersion":"1.4.0","codeMode":"single-use","codeTtlSeconds":120,"createdAt":"2026-08-01T09:00:00.000Z","displayName":"Queue status — Oxford Street","enabled":true,"id":"3f9a2b1c-7d4e-4c2a-9f6b-1e8d5a3c7b90","installationId":"6512aa34bcf86cd799439aa1","maxSessionDurationSeconds":14400,"maxSessions":200,"scopes":["twin:read","twin:message"],"sessionTtlSeconds":900,"spaceId":"6512bb34bcf86cd799439bb2","status":"Online","updatedAt":"2026-08-01T09:00:00.000Z","url":"https://web.phystack.com/acme/queue-status-oxford-street/","urlId":"acme/queue-status-oxford-street"},"properties":{"appName":{"type":"string"},"appVersion":{"type":"string"},"codeMode":{"$ref":"#/components/schemas/WebEndpointCodeMode"},"codeTtlSeconds":{"description":"Claim-code lifetime in seconds.","format":"int32","minimum":0,"type":"integer"},"createdAt":{"type":"string"},"displayName":{"type":"string"},"enabled":{"description":"Disabled endpoints reject session minting; live sessions expire\nwithin one session TTL.","type":"boolean"},"id":{"description":"The endpoint's deviceId — a uuid minted at create, NOT the Web twin's\nMongo `_id`. Keys the per-endpoint settings override; opaque string.","type":"string"},"installationId":{"type":"string"},"maxSessionDurationSeconds":{"description":"Refresh-grant ceiling in seconds — total session lifetime across renewals.","format":"int32","minimum":0,"type":"integer"},"maxSessions":{"description":"Maximum concurrent web sockets for this endpoint.","format":"int32","minimum":0,"type":"integer"},"scopes":{"description":"Operation classes granted to minted sessions\n(e.g. `twin:read`, `twin:message`, `media:view`).","items":{"type":"string"},"type":"array"},"sessionTtlSeconds":{"description":"Session JWT lifetime in seconds.","format":"int32","minimum":0,"type":"integer"},"spaceId":{"description":"The Space this endpoint serves — one Space per endpoint.","type":"string"},"status":{"description":"Twin status (e.g. `\"Online\"`, `\"Offline\"`).","type":"string"},"updatedAt":{"type":"string"},"url":{"description":"Public URL the endpoint serves at (`{web hosting root}/{urlId}/`) —\nderived from the region's web hosting domain, not stored.","type":"string"},"urlId":{"description":"The composite public path `{tenantSlug}/{name}` (kebab-case segments,\nimmutable after create, never reused — deleted endpoints keep their\nurlId forever). Not a credential — sessions always require a live\nclaim code or refresh token.","type":"string"}},"required":["id","installationId","spaceId","appName","appVersion","status","urlId","url","displayName","enabled","scopes","codeMode","codeTtlSeconds","maxSessions","sessionTtlSeconds","maxSessionDurationSeconds","createdAt","updatedAt"],"type":"object"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","pagination"],"type":"object"},"PaginationMeta":{"description":"Pagination metadata for list responses.","properties":{"page":{"format":"int32","minimum":0,"type":"integer"},"pageSize":{"format":"int32","minimum":0,"type":"integer"},"totalCount":{"format":"int64","minimum":0,"type":"integer"},"totalPages":{"format":"int32","minimum":0,"type":"integer"}},"required":["page","pageSize","totalCount","totalPages"],"type":"object"},"PeripheralAction":{"description":"A single action available on a peripheral (from the descriptor schema).","properties":{"descriptor":{"type":"string"},"name":{"type":"string"},"params":{},"returns":{}},"required":["descriptor","name"],"type":"object"},"PeripheralActionResult":{"description":"The result of invoking an action on a peripheral — the owner's `respond()`\npayload, echoed back over the twin-message bus.","example":{"data":{"jobId":"job-42"},"message":"Printed","status":"success"},"properties":{"data":{"description":"Optional structured return value, shaped by the action's descriptor schema."},"message":{"description":"Human-readable message from the owner.","type":["string","null"]},"status":{"description":"Outcome reported by the peripheral owner (e.g. `success` / `error`).","type":"string"}},"required":["status"],"type":"object"},"PeripheralEvent":{"description":"A single event a peripheral can emit (from the descriptor schema). List-only\ndiscovery — NOT subscribable via this surface.","properties":{"descriptor":{"type":"string"},"name":{"type":"string"},"payload":{}},"required":["descriptor","name"],"type":"object"},"PeripheralListItem":{"description":"A peripheral list row (slim). The list intentionally omits `settings` /\n`telemetry` — fetch the single peripheral for the full payload.","example":{"descriptors":{"acme.test-scanner":"1.0.0"},"deviceDisplayName":"Till 3","deviceId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","hardwareStatus":"ready","id":"650abc1234ef567890123456","name":"Front Scanner","status":"OK"},"properties":{"descriptors":{"additionalProperties":{"type":"string"},"description":"Map of descriptor uniqueName → pinned version.","propertyNames":{"type":"string"},"type":"object"},"deviceDisplayName":{"description":"Display name of the owning device (empty string when the device twin is missing).","type":"string"},"deviceId":{"type":"string"},"hardwareStatus":{"description":"Hardware status reported by the peripheral (`ready` / `unavailable` / …);\n`null` until the device first reports.","type":["string","null"]},"id":{"type":"string"},"name":{"description":"Logical peripheral name.","type":"string"},"status":{"type":"string"}},"required":["id","deviceId","name","descriptors","status","deviceDisplayName"],"type":"object"},"PeripheralResponse":{"description":"A peripheral — a logical device-attached component (scanner, printer, …)\nwhose shape is derived from its descriptor(s). Settings are operator-writable\n(schema-validated); telemetry is read-only.","example":{"createdAt":"2026-01-02T03:04:05.000Z","descriptors":{"acme.test-scanner":"1.0.0"},"deviceId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","hardwareId":"hw-777","id":"650abc1234ef567890123456","name":"Front Scanner","settings":{"acme.test-scanner":{"beepVolume":7}},"status":"OK","telemetry":{"status":"ready"},"tenantId":"507f1f77bcf86cd799439011","updatedAt":"2026-01-03T03:04:05.000Z"},"properties":{"createdAt":{"type":"string"},"descriptors":{"additionalProperties":{"type":"string"},"description":"Map of descriptor uniqueName → pinned version.","propertyNames":{"type":"string"},"type":"object"},"deviceId":{"type":"string"},"hardwareId":{"type":"string"},"id":{"type":"string"},"name":{"description":"Logical peripheral name.","type":"string"},"settings":{"description":"Operator-writable settings, validated against the descriptor schema on write."},"status":{"type":"string"},"telemetry":{"description":"Read-only telemetry (the peripheral's reported state)."},"tenantId":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","deviceId","tenantId","name","hardwareId","descriptors","status","settings","telemetry","createdAt","updatedAt"],"type":"object"},"PeripheralSchemaResponse":{"description":"The peripheral's full contract, merged from its pinned descriptor versions.\nFacets keep the descriptor vocabulary: `desiredProperties` is the settings\ncontract (identity-excluded and strict — the SAME schema the settings PUT\nvalidates against), `reportedProperties` is the telemetry contract, and\n`actions`/`events` describe the invokable surface. `metaSchema` is the\nmatching four-facet UI sidecar (`ui:*` hints).","example":{"metaSchema":{"actions":{},"desiredProperties":{"acme.test-scanner":{"beepVolume":{"ui:widget":"range"}}},"events":{},"reportedProperties":{}},"schema":{"properties":{"actions":{"properties":{"acme.test-scanner":{"properties":{"beep":{"properties":{"params":{"type":"object"}},"type":"object"}},"type":"object"}}},"desiredProperties":{"additionalProperties":false,"properties":{"acme.test-scanner":{"additionalProperties":false,"properties":{"beepVolume":{"type":"integer"}},"type":"object"}},"type":"object"},"events":{"properties":{}},"reportedProperties":{"properties":{"acme.test-scanner":{"properties":{"paperLevel":{"type":"integer"}},"type":"object"}}}},"type":"object"}},"properties":{"metaSchema":{"description":"Four-facet UI sidecar (`ui:*` hints); facets are `{}` when hint-less."},"schema":{"description":"Merged JSON-schema contract with the four facets under `properties`."}},"required":["schema","metaSchema"],"type":"object"},"PeripheralSettingsResponse":{"description":"A peripheral's operator-writable settings view — values only. The contract\nneeded to render an edit form lives on `GET /peripherals/{id}/schema`.","example":{"settings":{"acme.test-scanner":{"beepVolume":7}}},"properties":{"settings":{}},"required":["settings"],"type":"object"},"PeripheralSettingsUpdateRequest":{"description":"Request body for updating a peripheral's settings. The settings object is\nvalidated (strict) against the peripheral's descriptor schema in phyhub; an\ninvalid or contract-less peripheral returns 422. The update is a partial\nmerge: provided keys are set/updated, omitted keys are left untouched.","example":{"settings":{"acme.test-scanner":{"beepVolume":7}}},"properties":{"settings":{"description":"The settings to merge (keyed by descriptor uniqueName). Provided keys are\nset/updated; omitted keys are NOT removed."}},"required":["settings"],"type":"object"},"PeripheralTelemetryResponse":{"description":"A peripheral's read-only telemetry view.","properties":{"telemetry":{}},"required":["telemetry"],"type":"object"},"PlanInfo":{"description":"Plan info within a subscription.","properties":{"billingDay":{"description":"Day of month for billing cycle (1-31).","format":"int32","minimum":0,"type":["integer","null"]},"planId":{"type":"string"},"planName":{"description":"Localized plan name.","type":["string","null"]},"subscribedAt":{"description":"ISO 8601 timestamp when the plan was subscribed.","type":["string","null"]}},"required":["planId"],"type":"object"},"PlanResponse":{"description":"Generic 2026-03 plan model — reused by every endpoint that returns\nplans. Mirrors core's `PlanResponse` (types/2026-03/plans.ts).","example":{"activePriceValue":{"currency":"EUR","value":1200000,"valueDecimal":12.0},"description":{"en":"For pilot deployments"},"id":"63b1a0d05b0726000889aed4","name":{"en":"Tenant Pilot"},"sku":"TEN002_23","type":"tenant"},"properties":{"activePriceValue":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/CurrencyAwareValue","description":"Monthly price in the requesting tenant's currency. Absent when the\nplan has no active price in that currency (always present on the\navailable-child-plans listing, which filters to priced plans)."}]},"description":{"description":"Localized plan description keyed by locale."},"id":{"type":"string"},"name":{"description":"Localized plan name keyed by locale."},"sku":{"type":"string"},"type":{"description":"Plan type: `tenant` or `support`.","type":"string"}},"required":["id","type","name","sku"],"type":"object"},"PlanSubscriptionState":{"description":"Plan subscription state (current + optional scheduled change).","properties":{"current":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PlanInfo"}]},"scheduled":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/ScheduledPlan"}]}},"type":"object"},"PlanTypeParam":{"description":"Plan type filter for the available-child-plans endpoint.","enum":["tenant","support"],"type":"string"},"RawEventRow":{"description":"A single raw event row from ClickHouse, enriched with session fields.\n\nOptional fields are nullable in the underlying store; `str1..str5` and\n`int1..int5` are generic app-defined columns whose meaning depends on the\nemitting app's event schema.","properties":{"appId":{"description":"App resolved from session","type":["string","null"]},"captureId":{"description":"Capture identifier","type":["string","null"]},"categoryId":{"description":"Category reference","type":["string","null"]},"clientId":{"description":"Client identifier (e.g. browser or SDK client id)","type":["string","null"]},"deviceId":{"description":"Device resolved from session","type":["string","null"]},"eventTime":{"description":"Event timestamp, ISO 8601","example":"2026-03-15T10:00:00Z","type":"string"},"eventType":{"description":"Event type (app-defined, e.g. `VIEW`, `CLICK`)","type":"string"},"id":{"description":"Event identifier (UUID)","type":"string"},"installationId":{"description":"Installation resolved from session","type":["string","null"]},"int1":{"description":"Generic numeric column 1","format":"int64","type":["integer","null"]},"int2":{"description":"Generic numeric column 2","format":"int64","type":["integer","null"]},"int3":{"description":"Generic numeric column 3","format":"int64","type":["integer","null"]},"int4":{"description":"Generic numeric column 4","format":"int64","type":["integer","null"]},"int5":{"description":"Generic numeric column 5","format":"int64","type":["integer","null"]},"interaction":{"description":"Whether the event was produced by a user interaction","type":["boolean","null"]},"ip":{"description":"Client IP address","type":["string","null"]},"productId":{"description":"Product reference","type":["string","null"]},"sessionId":{"description":"Session identifier","type":"string"},"spaceId":{"description":"Space where the event occurred","type":["string","null"]},"str1":{"description":"Generic string column 1","type":["string","null"]},"str2":{"description":"Generic string column 2","type":["string","null"]},"str3":{"description":"Generic string column 3","type":["string","null"]},"str4":{"description":"Generic string column 4","type":["string","null"]},"str5":{"description":"Generic string column 5","type":["string","null"]},"tenantId":{"description":"Tenant that owns the event","type":"string"}},"required":["id","tenantId","eventTime","sessionId","eventType"],"type":"object"},"RealtimeEventsCountMeta":{"properties":{"durationSeconds":{"format":"int32","minimum":0,"type":"integer"},"since":{"type":"string"},"until":{"type":"string"}},"required":["since","until","durationSeconds"],"type":"object"},"RealtimeEventsCountRequest":{"description":"Request body for `POST /tenants/{tenantId}/realtime-events/count`.","properties":{"deviceIds":{"description":"Devices to evaluate (1..1000). One entry per device in the response.","items":{"type":"string"},"maxItems":1000,"minItems":1,"type":"array"},"durationSeconds":{"description":"Required: window length in seconds. The window is\n`[until - durationSeconds, until]`.","format":"int32","maximum":3600,"minimum":1,"type":"integer"},"eventType":{"type":"string"},"groupBy":{"description":"Optional: groupBy column allowlist (`int1..5` / `str1..5`).\nEmpty / absent means \"single bucket per device\".","items":{"type":"string"},"maxItems":4,"type":["array","null"]},"predicates":{"description":"AND-combined predicates over `int1..5` / `str1..5`."},"until":{"description":"Optional ISO 8601 upper bound. Defaults to server clock.","type":["string","null"]}},"required":["deviceIds","eventType","durationSeconds"],"type":"object"},"RealtimeEventsCountResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/CountDeviceResult"},"type":"array"},"meta":{"$ref":"#/components/schemas/RealtimeEventsCountMeta"}},"required":["data","meta"],"type":"object"},"ResolvedOverrideResponse":{"description":"The fully-merged effective settings (`GET ?resolved=true`). Precedence is\n**device > space > installation**.","example":{"resolved":{"lang":"en","theme":{"color":"red","size":"m"}}},"properties":{"resolved":{"description":"The merged settings document."}},"required":["resolved"],"type":"object"},"RetypedProperty":{"description":"A top-level property whose declared `type` changed between two build schemas.","properties":{"from":{"description":"Declared type in the current (source) schema.","type":"string"},"key":{"description":"Top-level property key.","type":"string"},"to":{"description":"Declared type in the target schema.","type":"string"}},"required":["key","from","to"],"type":"object"},"RoleResponse":{"description":"A tenant-scoped role (permission set).","properties":{"allowedRoleIdsToManage":{"items":{"type":"string"},"type":"array"},"createdAt":{"type":["string","null"]},"id":{"type":"string"},"manageAllRoles":{"type":"boolean"},"name":{"type":"string"},"permissions":{"items":{"type":"string"},"type":"array"},"tenantId":{"type":"string"},"updatedAt":{"type":["string","null"]}},"required":["id","tenantId","name","permissions","manageAllRoles","allowedRoleIdsToManage"],"type":"object"},"ScheduledPlan":{"description":"Scheduled plan change.","properties":{"planId":{"type":"string"}},"required":["planId"],"type":"object"},"SessionKind":{"description":"Which kind of credential a session row represents.","enum":["cli","api","console"],"type":"string"},"SessionListResponse":{"description":"Envelope for the sessions list: the rows plus the total count.","properties":{"items":{"items":{"$ref":"#/components/schemas/SessionResponse"},"type":"array"},"total":{"format":"int64","minimum":0,"type":"integer"}},"required":["items","total"],"type":"object"},"SessionResponse":{"description":"One revocable session/credential in the unified registry. Mirrors core's\n`SessionResponse` (`apps/core/server/types/2026-03/auth-sessions.ts`); never\nexposes the token hash or raw session internals.","properties":{"createdAt":{"description":"ISO 8601 of creation, or null if unknown.","type":["string","null"]},"current":{"description":"True if this is the credential the current request authenticated with.","type":"boolean"},"id":{"description":"Token ObjectId hex, or console session id.","type":"string"},"kind":{"$ref":"#/components/schemas/SessionKind"},"label":{"description":"Human label (token name, or device/user-agent for a console session).","type":["string","null"]},"lastUsedAt":{"description":"ISO 8601 of last use, or null if never recorded.","type":["string","null"]},"lastUsedIp":{"type":["string","null"]},"lastUsedUserAgent":{"type":["string","null"]}},"required":["id","kind","current"],"type":"object"},"SettingsMigrationResponse":{"description":"Schema delta between the installation's currently pinned build and a target\nbuild. Powers the safe-version-migration flow: a caller inspects the delta,\nthen submits a migration patch (via `PUT /settings`) that makes the result\nvalid against the target build's schema.","example":{"added":["newTheme"],"currentValuesValidAgainstTarget":false,"fromAppBuildId":"65a14f3d8e7c2a0008d4e1b9","issues":["/timeoutSeconds must be string"],"removed":["legacyFlag"],"retyped":[{"from":"integer","key":"timeoutSeconds","to":"string"}],"toAppBuildId":"66b2510e9f8d3b1109e5f2ca"},"properties":{"added":{"description":"Top-level property keys present in target but not in current.","items":{"type":"string"},"type":"array"},"currentValuesValidAgainstTarget":{"description":"Whether the installation's current `values` already validate against the\ntarget schema. `true` → a bare version bump is schema-safe.","type":"boolean"},"fromAppBuildId":{"description":"The installation's currently pinned build (source of the diff).","type":"string"},"issues":{"description":"Human-readable validation issues when the current values don't fit.","items":{"type":"string"},"type":"array"},"removed":{"description":"Top-level property keys present in current but not in target.","items":{"type":"string"},"type":"array"},"retyped":{"description":"Properties whose declared type changed (current → target).","items":{"$ref":"#/components/schemas/RetypedProperty"},"type":"array"},"toAppBuildId":{"description":"The target build the delta is computed against.","type":"string"}},"required":["fromAppBuildId","toAppBuildId","added","removed","retyped","currentValuesValidAgainstTarget","issues"],"type":"object"},"ShortUrlCreateRequest":{"additionalProperties":false,"description":"Request body for creating a custom short URL.","example":{"displayName":"Summer Sale Landing","redirectUrl":"https://example.com/promotions/summer","shortId":"summer-sale","tenantId":"507f1f77bcf86cd799439011"},"properties":{"displayName":{"type":"string"},"redirectUrl":{"type":"string"},"shortId":{"description":"Caller-chosen slug (the shortener's `customId`). Immutable after create.","type":"string"},"tenantId":{"type":"string"}},"required":["tenantId","shortId","displayName","redirectUrl"],"type":"object"},"ShortUrlResponse":{"description":"A tenant-managed custom short URL.","example":{"analyticsEnabled":true,"createdAt":"2026-01-10T09:00:00.000Z","displayName":"Summer Sale Landing","friendly":false,"id":"summer-sale","redirectUrl":"https://example.com/promotions/summer","shortUrl":"https://qr.run/summer-sale","tenantId":"507f1f77bcf86cd799439011","updatedAt":"2026-01-10T09:00:00.000Z"},"properties":{"analyticsEnabled":{"description":"Whether analytics tracking is attached to this short URL.","type":"boolean"},"createdAt":{"type":["string","null"]},"displayName":{"type":["string","null"]},"friendly":{"description":"Whether the shortener generated a friendly (word-based) slug.","type":"boolean"},"id":{"description":"Slug identifier (the shortener's `customId`). Caller-chosen, stable.","type":"string"},"redirectUrl":{"description":"Destination URL the short link redirects to.","type":"string"},"shortUrl":{"description":"Fully-qualified short URL the shortener minted.","type":["string","null"]},"tenantId":{"description":"Owning tenant (`organizationId`).","type":["string","null"]},"updatedAt":{"type":["string","null"]}},"required":["id","redirectUrl","friendly","analyticsEnabled"],"type":"object"},"ShortUrlUpdateRequest":{"additionalProperties":false,"description":"Request body for updating a custom short URL.\n\nThe slug (`id`) is immutable; only `displayName` and `redirectUrl` change.","example":{"displayName":"Summer Sale 2026","redirectUrl":"https://example.com/promotions/summer-2026"},"properties":{"displayName":{"type":["string","null"]},"redirectUrl":{"type":["string","null"]}},"type":"object"},"SpaceBatchCreateRequest":{"description":"Request body for bulk-creating sequentially-named spaces.","example":{"amount":32,"parentSpaceId":"507f1f77bcf86cd799439022","prefix":"SCO","tenantId":"507f1f77bcf86cd799439011","type":"SECTION"},"properties":{"amount":{"description":"How many NEW spaces to create. Numbering auto-continues from the highest\nexisting `{prefix}{n}` child of the parent.","format":"int32","minimum":0,"type":"integer"},"parentSpaceId":{"description":"Parent space the new spaces nest under (required — these types are never\ntenant roots).","type":"string"},"prefix":{"description":"Shared name prefix, e.g. \"SCO\".","type":"string"},"tenantId":{"description":"Tenant to create the spaces in.","type":"string"},"type":{"$ref":"#/components/schemas/SpaceType","description":"Type to create. Bulk creation is restricted to the quota-free,\nparent-required types: FLOOR, SECTION, or CUSTOM."}},"required":["tenantId","parentSpaceId","type","prefix","amount"],"type":"object"},"SpaceBatchCreateResponse":{"description":"Result of a bulk space creation.","properties":{"created":{"description":"Spaces actually created, in generation order. Auto-continue numbering\nguarantees every name is new, so there is no \"skipped\" list.","items":{"$ref":"#/components/schemas/SpaceResponse"},"type":"array"},"startNumber":{"description":"The number the run started from (e.g. 17 for `SCO17 … SCO48`).","format":"int32","minimum":0,"type":"integer"}},"required":["created","startNumber"],"type":"object"},"SpaceBillingContact":{"description":"Billing contact for a space (LOCATION billing detail). All fields optional;\nadmin-api defaults absent values to empty strings on persist.","properties":{"email":{"type":["string","null"]},"firstName":{"type":["string","null"]},"lastName":{"type":["string","null"]},"phone":{"type":["string","null"]}},"type":"object"},"SpaceCreateRequest":{"description":"Request body for creating a new space.","example":{"displayName":"Warehouse Zone B","parentSpaceId":"507f1f77bcf86cd799439022","tenantId":"507f1f77bcf86cd799439011","type":"SECTION"},"properties":{"address":{"type":["string","null"]},"billingAddress":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/BillingAddress","description":"Billing address for this space. Same shape as tenant billing (shared\nupstream schema). Settable at create, like `billingContact`."}]},"billingContact":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/SpaceBillingContact","description":"Billing contact for this space. Settable at create, echoed in\n`SpaceResponse` when present; not updatable via `PUT` — mirrors\nadmin-api."}]},"city":{"type":["string","null"]},"country":{"type":["string","null"]},"displayName":{"description":"Human-readable space name.","type":"string"},"externalId":{"type":["string","null"]},"latitude":{"format":"double","type":["number","null"]},"longitude":{"format":"double","type":["number","null"]},"notes":{"type":["string","null"]},"parentSpaceId":{"description":"Parent space ID. Required for floor/section/custom types.","type":["string","null"]},"postcode":{"type":["string","null"]},"state":{"type":["string","null"]},"tags":{"items":{"$ref":"#/components/schemas/Tag"},"type":"array"},"tenantId":{"description":"Tenant to create the space in.","type":"string"},"timezone":{"type":["string","null"]},"type":{"$ref":"#/components/schemas/SpaceType","description":"Space type in the hierarchy."},"weeklySchedule":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/WeeklySchedule"}]}},"required":["displayName","type","tenantId"],"type":"object"},"SpaceMapCluster":{"description":"An aggregated geohash cell holding several spaces, shown when zoomed out.","properties":{"count":{"description":"Number of spaces in the cell.","format":"int64","minimum":0,"type":"integer"},"geohash":{"description":"Geohash prefix identifying the cell (cluster key).","type":"string"},"latitude":{"description":"Centroid latitude (mean of member coordinates).","format":"double","type":"number"},"longitude":{"description":"Centroid longitude.","format":"double","type":"number"}},"required":["geohash","count","latitude","longitude"],"type":"object"},"SpaceMapMarker":{"description":"An individual space rendered as a map marker.","properties":{"displayName":{"type":"string"},"id":{"type":"string"},"latitude":{"format":"double","type":"number"},"longitude":{"format":"double","type":"number"},"type":{"$ref":"#/components/schemas/SpaceType"}},"required":["id","displayName","type","latitude","longitude"],"type":"object"},"SpaceMapResponse":{"description":"Map data: individual markers plus aggregated clusters.","properties":{"clusters":{"items":{"$ref":"#/components/schemas/SpaceMapCluster"},"type":"array"},"markers":{"items":{"$ref":"#/components/schemas/SpaceMapMarker"},"type":"array"},"precision":{"description":"Geohash precision used for clustering at this zoom (1-8).","format":"int32","type":"integer"},"zoom":{"description":"Echo of the requested zoom level (matches the request `zoom` type).","format":"int32","type":"integer"}},"required":["zoom","precision","markers","clusters"],"type":"object"},"SpaceResponse":{"description":"A space in the Phystack platform (store, venue, area, etc.).","example":{"address":"800 Mission Street","city":"San Francisco","country":"US","createdAt":"2025-01-10T09:00:00.000Z","displayName":"Building A - Research Lab","id":"507f1f77bcf86cd799439022","latitude":37.7855,"longitude":-122.3965,"notes":"Edge compute research facility with 12 vision nodes","postcode":"94105","state":"CA","tags":[],"tenantId":"507f1f77bcf86cd799439011","timezone":"America/Los_Angeles","type":"LOCATION","updatedAt":"2025-01-10T09:00:00.000Z"},"properties":{"address":{"type":["string","null"]},"billingAddress":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/BillingAddress","description":"Billing address — present only when billing details were set on the\nspace (settable at create). Same shape as tenant billing."}]},"billingContact":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/SpaceBillingContact","description":"Billing contact — present only when billing details were set on the\nspace (settable at create)."}]},"children":{"description":"Transitive descendant spaces (all levels, flattened — descendants do\nnot nest their own `children`). Present only on list rows when\n`include=children` is requested; narrowed by `filter[childrenType]`.\n`no_recursion` stops utoipa's schema generation from walking the\nself-reference forever (stack overflow at doc build otherwise).","items":{"$ref":"#/components/schemas/SpaceResponse"},"type":["array","null"]},"city":{"type":["string","null"]},"country":{"type":["string","null"]},"createdAt":{"description":"ISO 8601 creation timestamp.","type":"string"},"displayName":{"description":"Human-readable space name.","type":"string"},"externalId":{"type":["string","null"]},"id":{"description":"Unique space identifier.","type":"string"},"latitude":{"format":"double","type":["number","null"]},"longitude":{"format":"double","type":["number","null"]},"notes":{"type":["string","null"]},"parentSpaceId":{"description":"Parent space ID for hierarchical nesting.","type":["string","null"]},"postcode":{"type":["string","null"]},"state":{"type":["string","null"]},"tags":{"items":{"$ref":"#/components/schemas/Tag"},"type":"array"},"tenantId":{"description":"Tenant that owns this space.","type":"string"},"timezone":{"description":"IANA timezone (e.g. Europe/Stockholm).","type":["string","null"]},"type":{"$ref":"#/components/schemas/SpaceType","description":"Space type in the hierarchy."},"updatedAt":{"description":"ISO 8601 last update timestamp.","type":"string"},"weeklySchedule":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/WeeklySchedule"}]}},"required":["id","tenantId","displayName","type","createdAt","updatedAt"],"type":"object"},"SpaceType":{"description":"Valid space types in the hierarchy.","enum":["LOCATION","COUNTRY","REGION","CITY","FLOOR","SECTION","CUSTOM"],"type":"string"},"SpaceUpdateRequest":{"description":"Request body for updating an existing space.","example":{"displayName":"Warehouse Zone B - Automated","notes":"Sensor deployment complete: 8 LiDAR units, 4 thermal cameras"},"properties":{"address":{"type":["string","null"]},"city":{"type":["string","null"]},"country":{"type":["string","null"]},"displayName":{"type":["string","null"]},"externalId":{"type":["string","null"]},"latitude":{"format":"double","type":["number","null"]},"longitude":{"format":"double","type":["number","null"]},"notes":{"type":["string","null"]},"parentSpaceId":{"description":"Tri-state: key absent = leave the parent unchanged, `null` = detach\n(space becomes a root), string = re-parent. The nested `Option`\n(with the `double_option` deserializer — plain serde folds an explicit\n`null` into the outer `None`) preserves the null-vs-absent distinction\nthrough re-serialization — a flat `Option<String>` collapses `null` to\n\"absent\" and silently turns a detach into a no-op upstream.","type":["string","null"]},"postcode":{"type":["string","null"]},"state":{"type":["string","null"]},"tags":{"items":{"$ref":"#/components/schemas/Tag"},"type":["array","null"]},"timezone":{"type":["string","null"]},"type":{"type":["string","null"]},"weeklySchedule":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/WeeklySchedule"}]}},"type":"object"},"SpaceUsersParams":{"description":"Query parameters for `GET /2026-03/spaces/{id}/users`.","properties":{"fields":{"description":"Optional comma-separated allowlist of response fields to include in\neach item of the paginated response. When omitted, the full user\nshape is returned. Unknown field names return HTTP 400.","type":["string","null"]},"page":{"format":"int32","minimum":0,"type":"integer"},"pageSize":{"format":"int32","minimum":0,"type":"integer"},"tenantId":{"description":"Tenant the space belongs to — scopes the per-tenant access check.","type":"string"}},"required":["tenantId"],"type":"object"},"StatePersistenceDeviceResult":{"properties":{"deviceId":{"type":"string"},"firstMatchAt":{"description":"ISO 8601 of the first matching event, or null when none matched.","type":["string","null"]},"matchCount":{"format":"int64","minimum":0,"type":"integer"},"persisted":{"type":"boolean"},"totalCount":{"format":"int64","minimum":0,"type":"integer"}},"required":["deviceId","persisted","matchCount","totalCount"],"type":"object"},"StatePersistenceMeta":{"properties":{"minDurationSeconds":{"format":"int32","minimum":0,"type":"integer"},"since":{"type":"string"},"until":{"type":"string"}},"required":["since","until","minDurationSeconds"],"type":"object"},"StatePersistenceRequest":{"description":"Request body for `POST /tenants/{tenantId}/realtime-events/state-persistence`.","properties":{"bufferSeconds":{"description":"Optional: lookback buffer in seconds. Defaults to 300.","format":"int32","maximum":3600,"minimum":0,"type":["integer","null"]},"deviceIds":{"description":"Devices to evaluate (1..1000). Each gets one row in the response.","items":{"type":"string"},"maxItems":1000,"minItems":1,"type":"array"},"eventType":{"description":"Event type (e.g. `\"heartbeat\"`).","type":"string"},"minDurationSeconds":{"description":"Required: minimum continuous-match duration in seconds.","format":"int32","maximum":3600,"minimum":1,"type":"integer"},"predicates":{"description":"AND-combined predicates. Each entry filters by one allowlisted\ncolumn (`int1..5` / `str1..5`) with one of the supported operators."},"until":{"description":"Optional ISO 8601 upper bound. Defaults to server clock.","type":["string","null"]}},"required":["deviceIds","eventType","minDurationSeconds"],"type":"object"},"StatePersistenceResponse":{"description":"Response for `POST /tenants/{tenantId}/realtime-events/state-persistence`.","properties":{"data":{"items":{"$ref":"#/components/schemas/StatePersistenceDeviceResult"},"type":"array"},"meta":{"$ref":"#/components/schemas/StatePersistenceMeta"}},"required":["data","meta"],"type":"object"},"SubscriptionCard":{"description":"Card summary for stripe subscriptions with a captured payment method.\nOnly brand and last4 — never the Stripe customer or payment-method ids.","properties":{"brand":{"description":"Card brand (e.g. `visa`, `mastercard`).","type":"string"},"last4":{"description":"Last four digits of the card number.","type":"string"}},"required":["brand","last4"],"type":"object"},"SubscriptionDetails":{"description":"Active subscription details from external provider.","properties":{"card":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/SubscriptionCard","description":"Present for stripe subscriptions with a payment method on file."}]},"externalId":{"description":"External subscription ID from the billing provider.","type":"string"},"source":{"description":"Billing provider (credit-card, azure-saas, direct-invoice, partner-invoice).","type":"string"},"status":{"type":"string"}},"required":["status","source","externalId"],"type":"object"},"Tag":{"description":"Content tag assigned to a space or device.","properties":{"tagId":{"description":"Tag ID.","type":"string"},"tagType":{"description":"Tag type identifier.","type":"string"},"values":{"description":"Tag values.","items":{"type":"string"},"type":"array"}},"required":["tagType","tagId","values"],"type":"object"},"TenantCreateRequest":{"description":"Request body for creating a new root tenant.\n\nRoot tenants are created for the caller with the same defaults as\nself-signup. Managed (child) tenants are created via\n`POST /tenants/{parentTenantId}/managed-tenants`.","example":{"country":"DE","tenantDisplayName":"Horizon Robotics"},"properties":{"country":{"description":"ISO 3166-1 alpha-2 country code.","type":"string"},"tenantDisplayName":{"description":"Human-readable name for the new tenant.","type":"string"}},"required":["tenantDisplayName","country"],"type":"object"},"TenantPlans":{"description":"Tenant and support plan subscriptions.","properties":{"support":{"$ref":"#/components/schemas/PlanSubscriptionState"},"tenant":{"$ref":"#/components/schemas/PlanSubscriptionState"}},"required":["tenant","support"],"type":"object"},"TenantResponse":{"description":"A tenant in the Phystack platform.","example":{"allowedUserEmailDomains":[],"billingAddress":{"addressLine1":"123 Main St","city":"Chicago","country":"US","name":"Nexus Technologies","postal":"60601"},"billingContactEmail":"billing@nexus-tech.com","billingEntity":"phystack-ie","billingType":"credit-card","country":"US","createdAt":"2025-01-10T09:00:00.000Z","currency":"USD","dataResidency":"US","enabled":true,"hasActiveSubscription":true,"id":"507f1f77bcf86cd799439011","restrictUserEmailDomains":false,"tenantDisplayName":"Nexus Technologies","tenantName":"Nexus Technologies","tenantSlug":"nexus-tech-4821","timezone":"America/Chicago","updatedAt":"2025-01-10T09:00:00.000Z","vatId":"SE556677889901"},"properties":{"allowedUserEmailDomains":{"items":{"type":"string"},"type":"array"},"billingAddress":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/BillingAddressResponse","description":"Billing address for invoicing."}]},"billingContactEmail":{"description":"Billing contact email address.","type":["string","null"]},"billingEntity":{"description":"Billing entity for invoicing.","type":["string","null"]},"billingType":{"description":"Billing type (e.g. credit-card, azure-saas, direct-invoice).","type":["string","null"]},"country":{"type":"string"},"createdAt":{"type":"string"},"currency":{"type":"string"},"dataResidency":{"$ref":"#/components/schemas/DataResidency","description":"Data storage region."},"enabled":{"description":"Whether the tenant account is active.","type":"boolean"},"hasActiveSubscription":{"description":"Whether the tenant has a current active subscription. Read-only, derived from subscription data.","type":"boolean"},"id":{"type":"string"},"parentTenantId":{"description":"Parent tenant ID for sub-tenant hierarchy.","type":["string","null"]},"restrictUserEmailDomains":{"type":"boolean"},"tenantDisplayName":{"description":"Human-readable tenant name.","type":"string"},"tenantName":{"description":"Human-readable tenant name (alias of `tenantDisplayName`).","type":"string"},"tenantSlug":{"description":"Auto-generated unique slug (e.g. acme-7294).","type":"string"},"timezone":{"type":"string"},"updatedAt":{"type":"string"},"vatId":{"description":"VAT identification number.","type":["string","null"]}},"required":["id","tenantSlug","tenantDisplayName","tenantName","country","currency","timezone","dataResidency","restrictUserEmailDomains","createdAt","updatedAt","enabled","hasActiveSubscription"],"type":"object"},"TenantSubscriptionResponse":{"description":"Tenant subscription and plan information.","example":{"billingType":"credit-card","hasActiveSubscription":true,"plans":{"support":{"current":{"billingDay":15,"planId":"60a1b2c3d4e5f6a7b8c9d0e2","planName":"Standard","subscribedAt":"2025-06-15T10:00:00.000Z"}},"tenant":{"current":{"billingDay":15,"planId":"60a1b2c3d4e5f6a7b8c9d0e1","planName":"Professional","subscribedAt":"2025-06-15T10:00:00.000Z"}}},"subscription":{"card":{"brand":"visa","last4":"4242"},"externalId":"sub_1234567890","source":"stripe","status":"subscribed"},"tenantId":"507f1f77bcf86cd799439011"},"properties":{"billingType":{"type":["string","null"]},"hasActiveSubscription":{"type":"boolean"},"plans":{"$ref":"#/components/schemas/TenantPlans"},"subscription":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/SubscriptionDetails"}]},"tenantId":{"type":"string"}},"required":["tenantId","hasActiveSubscription","plans"],"type":"object"},"TenantUpdateRequest":{"description":"Request body for updating an existing tenant.","example":{"tenantDisplayName":"Nexus Technologies Inc.","timezone":"America/New_York"},"properties":{"allowedUserEmailDomains":{"items":{"type":"string"},"type":["array","null"]},"billingAddress":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/BillingAddress","description":"Billing address for invoicing."}]},"billingContactEmail":{"description":"Billing contact email address.","type":["string","null"]},"restrictUserEmailDomains":{"type":["boolean","null"]},"tenantDisplayName":{"type":["string","null"]},"timezone":{"type":["string","null"]},"vatId":{"description":"VAT identification number.","type":["string","null"]}},"type":"object"},"TokenCreateRequest":{"description":"Request body to create a PAT.","properties":{"name":{"description":"Human-readable label (unique per user).","type":"string"},"ttlDays":{"description":"Lifetime in days (default 90, max 365). The expiry is computed server-side.","format":"int32","minimum":0,"type":["integer","null"]}},"required":["name"],"type":"object"},"TokenCreateResponse":{"allOf":[{"$ref":"#/components/schemas/TokenResponse"},{"properties":{"token":{"description":"The plaintext token — shown ONCE; cannot be retrieved again.","type":"string"}},"required":["token"],"type":"object"}],"description":"Create response: the metadata plus the one-time plaintext `token`."},"TokenResponse":{"description":"A personal access token's metadata (never the secret).","properties":{"createdAt":{"type":"string"},"expiry":{"description":"ISO 8601 expiry.","type":"string"},"id":{"type":"string"},"isExpired":{"type":"boolean"},"lastUsedAt":{"description":"ISO 8601 of last use, or null if never used.","type":["string","null"]},"name":{"type":"string"}},"required":["id","name","expiry","isExpired","createdAt"],"type":"object"},"UserResponse":{"description":"A platform user.","properties":{"createdAt":{"type":["string","null"]},"email":{"type":"string"},"firstName":{"type":["string","null"]},"id":{"type":"string"},"isSysadmin":{"type":"boolean"},"lastName":{"type":["string","null"]},"lastSeenAt":{"type":["string","null"]},"phoneNumber":{"type":["string","null"]},"preferredLanguage":{"type":["string","null"]},"tenants":{"items":{"$ref":"#/components/schemas/UserTenantMembership"},"type":"array"},"updatedAt":{"type":["string","null"]}},"required":["id","email","isSysadmin","tenants"],"type":"object"},"UserTenantMembership":{"description":"A user's membership in a tenant.","properties":{"hasAccessToAllSpaces":{"type":"boolean"},"roleId":{"type":["string","null"]},"spaceIds":{"items":{"type":"string"},"type":"array"},"tenantId":{"type":["string","null"]}},"required":["hasAccessToAllSpaces","spaceIds"],"type":"object"},"WebEndpointCodeMode":{"description":"How a claim code may be redeemed within its TTL.","enum":["single-use","multi-use"],"type":"string"},"WebEndpointCodeResponse":{"description":"A claim code plus its ready-to-open session link, minted by an authorized\noperator without a device (TECH-1471) — the exact credential a QR scan\ncarries. The link is LIVE for `expiresIn` seconds: treat it like the QR\nitself — show it once, never log it.","example":{"code":"K7KQ3ZDM2P","expiresIn":300,"url":"https://web.phystack.com/amco-5119/office-remote/#code=K7KQ3ZDM2P"},"properties":{"code":{"description":"The claim code (also embedded in `url`), redeemable per the endpoint's\n`codeMode` until it expires.","type":"string"},"expiresIn":{"description":"Seconds until the code expires (the endpoint's `codeTtlSeconds`).","format":"int64","minimum":0,"type":"integer"},"url":{"description":"Ready-to-open session link: `{webAppsRoot}/{urlId}/#code={code}`.","type":"string"}},"required":["code","url","expiresIn"],"type":"object"},"WebEndpointCreateRequest":{"additionalProperties":false,"description":"Request body for creating a web endpoint. Device-shaped (devices create\nprecedent): `tenantId` routes the request and is stripped before\nforwarding — phyhub carries the tenant in the URL. `urlId` is generated\nby phyhub when omitted; the session/abuse knobs are all required —\nphyhub validates the installation, space, and ranges.","example":{"codeMode":"single-use","codeTtlSeconds":120,"displayName":"Queue status — Oxford Street","installationId":"6512aa34bcf86cd799439aa1","maxSessionDurationSeconds":14400,"maxSessions":200,"scopes":["twin:read","twin:message"],"sessionTtlSeconds":900,"spaceId":"6512bb34bcf86cd799439bb2","tenantId":"507f1f77bcf86cd799439011"},"properties":{"codeMode":{"$ref":"#/components/schemas/WebEndpointCodeMode"},"codeTtlSeconds":{"description":"Claim-code lifetime in seconds (60–3600).","format":"int32","maximum":3600,"minimum":60,"type":"integer"},"displayName":{"type":"string"},"installationId":{"description":"The web Installation this endpoint serves — a Mongo ObjectId (24 hex chars).","type":"string"},"maxSessionDurationSeconds":{"description":"Refresh-grant ceiling in seconds — total session lifetime across\nrenewals (300–86400).","format":"int32","maximum":86400,"minimum":300,"type":"integer"},"maxSessions":{"description":"Maximum concurrent web sockets for this endpoint (1–10000).","format":"int32","maximum":10000,"minimum":1,"type":"integer"},"scopes":{"description":"Operation classes granted to minted sessions\n(e.g. `twin:read`, `twin:message`, `media:view`).","items":{"type":"string"},"type":"array"},"sessionTtlSeconds":{"description":"Session JWT lifetime in seconds (60–3600). Must not exceed\n`maxSessionDurationSeconds` — violating that returns 400.","format":"int32","maximum":3600,"minimum":60,"type":"integer"},"spaceId":{"description":"The Space this endpoint serves — a Mongo ObjectId (24 hex chars).","type":"string"},"tenantId":{"type":"string"},"urlId":{"description":"Public URL path segment (unique per region, kebab-case). Generated\nfrom the display name when omitted; collision returns 409.","type":["string","null"]}},"required":["tenantId","installationId","spaceId","displayName","scopes","codeMode","codeTtlSeconds","maxSessions","sessionTtlSeconds","maxSessionDurationSeconds"],"type":"object"},"WebEndpointResponse":{"description":"A web endpoint — the slim, public-API projection of a phyhub Web twin.\nOne row per endpoint; multiple endpoints per installation are allowed\n(uniqueness key is `urlId` — the composite `{tenantSlug}/{name}` public\npath, unique per region including deleted endpoints: urlIds are\npermanent, TECH-1469).","example":{"appName":"acme/queue-status-web","appVersion":"1.4.0","codeMode":"single-use","codeTtlSeconds":120,"createdAt":"2026-08-01T09:00:00.000Z","displayName":"Queue status — Oxford Street","enabled":true,"id":"3f9a2b1c-7d4e-4c2a-9f6b-1e8d5a3c7b90","installationId":"6512aa34bcf86cd799439aa1","maxSessionDurationSeconds":14400,"maxSessions":200,"scopes":["twin:read","twin:message"],"sessionTtlSeconds":900,"spaceId":"6512bb34bcf86cd799439bb2","status":"Online","updatedAt":"2026-08-01T09:00:00.000Z","url":"https://web.phystack.com/acme/queue-status-oxford-street/","urlId":"acme/queue-status-oxford-street"},"properties":{"appName":{"type":"string"},"appVersion":{"type":"string"},"codeMode":{"$ref":"#/components/schemas/WebEndpointCodeMode"},"codeTtlSeconds":{"description":"Claim-code lifetime in seconds.","format":"int32","minimum":0,"type":"integer"},"createdAt":{"type":"string"},"displayName":{"type":"string"},"enabled":{"description":"Disabled endpoints reject session minting; live sessions expire\nwithin one session TTL.","type":"boolean"},"id":{"description":"The endpoint's deviceId — a uuid minted at create, NOT the Web twin's\nMongo `_id`. Keys the per-endpoint settings override; opaque string.","type":"string"},"installationId":{"type":"string"},"maxSessionDurationSeconds":{"description":"Refresh-grant ceiling in seconds — total session lifetime across renewals.","format":"int32","minimum":0,"type":"integer"},"maxSessions":{"description":"Maximum concurrent web sockets for this endpoint.","format":"int32","minimum":0,"type":"integer"},"scopes":{"description":"Operation classes granted to minted sessions\n(e.g. `twin:read`, `twin:message`, `media:view`).","items":{"type":"string"},"type":"array"},"sessionTtlSeconds":{"description":"Session JWT lifetime in seconds.","format":"int32","minimum":0,"type":"integer"},"spaceId":{"description":"The Space this endpoint serves — one Space per endpoint.","type":"string"},"status":{"description":"Twin status (e.g. `\"Online\"`, `\"Offline\"`).","type":"string"},"updatedAt":{"type":"string"},"url":{"description":"Public URL the endpoint serves at (`{web hosting root}/{urlId}/`) —\nderived from the region's web hosting domain, not stored.","type":"string"},"urlId":{"description":"The composite public path `{tenantSlug}/{name}` (kebab-case segments,\nimmutable after create, never reused — deleted endpoints keep their\nurlId forever). Not a credential — sessions always require a live\nclaim code or refresh token.","type":"string"}},"required":["id","installationId","spaceId","appName","appVersion","status","urlId","url","displayName","enabled","scopes","codeMode","codeTtlSeconds","maxSessions","sessionTtlSeconds","maxSessionDurationSeconds","createdAt","updatedAt"],"type":"object"},"WebEndpointUpdateRequest":{"additionalProperties":false,"description":"Request body for updating a web endpoint. Installation, space, and\n`urlId` are immutable — derived from the existing endpoint.","example":{"displayName":"Queue status — Oxford Street","enabled":false,"scopes":["twin:read"]},"properties":{"codeMode":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/WebEndpointCodeMode"}]},"codeTtlSeconds":{"description":"Claim-code lifetime in seconds (60–3600).","format":"int32","maximum":3600,"minimum":60,"type":["integer","null"]},"displayName":{"type":["string","null"]},"enabled":{"description":"Disabling an endpoint revokes its refresh grants immediately; live\nsockets expire within one session TTL.","type":["boolean","null"]},"maxSessionDurationSeconds":{"description":"Refresh-grant ceiling in seconds — total session lifetime across\nrenewals (300–86400).","format":"int32","maximum":86400,"minimum":300,"type":["integer","null"]},"maxSessions":{"description":"Maximum concurrent web sockets for this endpoint (1–10000).","format":"int32","maximum":10000,"minimum":1,"type":["integer","null"]},"scopes":{"items":{"type":"string"},"type":["array","null"]},"sessionTtlSeconds":{"description":"Session JWT lifetime in seconds (60–3600). Must not exceed the\neffective `maxSessionDurationSeconds` — violating that returns 400.","format":"int32","maximum":3600,"minimum":60,"type":["integer","null"]}},"type":"object"},"WebSessionCodeRequest":{"description":"Session mint via claim code — the code read from the QR fragment\n(`#code=`) is the credential; consumed per the endpoint's `codeMode`.\nServed by phyhub via the regional passthrough (see module docs).","example":{"code":"K7KQ3ZDM","urlId":"amco-5119/office-remote"},"properties":{"code":{"description":"A live claim code issued over an authenticated phyhub socket.","type":"string"},"urlId":{"description":"The endpoint's composite public path, `{tenantSlug}/{name}` (TECH-1469).","type":"string"}},"required":["urlId","code"],"type":"object"},"WebSessionMintRequest":{"description":"Request body for phyhub's public session route\n(`POST /regions/{region}/phyhub/api/v1/web-sessions`): the composite\n`urlId` plus exactly one of a claim `code` (first mint) or a\n`refreshToken` (renewal).\nErrors: 404 unknown `urlId`; 401 for EVERYTHING else — invalid/expired/\nconsumed code or token, disabled endpoint — deliberately one generic\nstatus so public callers get no probing oracle; 429 rate limit exceeded.","oneOf":[{"$ref":"#/components/schemas/WebSessionCodeRequest"},{"$ref":"#/components/schemas/WebSessionRefreshRequest"}]},"WebSessionRefreshRequest":{"description":"Session renewal via refresh token — one-time-use, rotated on every\nrefresh, bounded by the endpoint's `maxSessionDurationSeconds`.\nServed by phyhub via the regional passthrough (see module docs).","example":{"refreshToken":"wrt_9f2c4c1a7d8e4b5f","urlId":"amco-5119/office-remote"},"properties":{"refreshToken":{"description":"The refresh token returned by the previous session mint.","type":"string"},"urlId":{"description":"The endpoint's composite public path, `{tenantSlug}/{name}` (TECH-1469).","type":"string"}},"required":["urlId","refreshToken"],"type":"object"},"WebSessionResponse":{"description":"A minted web session: a short-lived scoped JWT plus the rotated refresh\ntoken for renewals. Returned by phyhub's public session route (see\nmodule docs for the regional passthrough path).","example":{"deadlineInSeconds":14400,"expiresAt":"2026-08-06T12:15:00.000Z","expiresIn":900,"refreshToken":"wrt_9f2c4c1a7d8e4b5f","token":"eyJhbGciOiJIUzI1NiIs..."},"properties":{"deadlineInSeconds":{"description":"Remaining seconds to the absolute session deadline (the refresh\nchain's hard end, fixed at code redemption). Clients key stored-grant\neviction off it — relative seconds, same clock-skew reasoning as\n`expiresIn` (TECH-1468).","format":"int64","minimum":0,"type":"integer"},"expiresAt":{"description":"ISO 8601 expiry of `token`.","type":"string"},"expiresIn":{"description":"Seconds until `token` expires. Clients compute the renewal deadline\nfrom this (relative), not from `expiresAt` — phone clocks skew.","format":"int64","minimum":0,"type":"integer"},"refreshToken":{"description":"One-time-use refresh token — rotated on every renewal; replay\nrevokes the session.","type":"string"},"token":{"description":"Scoped HS256 JWT for the phyhub socket handshake (`auth.webAppJwt`).","type":"string"}},"required":["token","expiresIn","expiresAt","refreshToken","deadlineInSeconds"],"type":"object"},"WeeklySchedule":{"description":"Weekly operating schedule. Keys are day numbers (0=Sunday through 6=Saturday).","properties":{"weeklyTime":{"additionalProperties":{"$ref":"#/components/schemas/DailyBusinessHours"},"propertyNames":{"type":"string"},"type":["object","null"]}},"type":"object"}},"securitySchemes":{"bearer":{"scheme":"bearer","type":"http"}}},"info":{"description":"REST API for [PhyStack](https://phystack.com) — the operating system for the physical world.\n\nPhyStack is an edge-cloud platform that lets you deploy applications and AI at the edge, manage fleets of connected devices, and build interactive experiences across physical spaces. This API provides unified access to tenant management, device orchestration, space hierarchies, app catalogs, and installation lifecycle — across all regions, behind a single endpoint.\n\n**Resources**\n- [Developer Documentation](https://build.phystack.com) — SDK guides, CLI reference, and architecture\n- [User Guides](https://learn.phystack.com) — Console walkthroughs and tutorials\n- [PhyStack Website](https://phystack.com) — Platform overview","license":{"name":""},"title":"PhyStack API","version":"2026-03"},"openapi":"3.1.0","paths":{"/app-builds/{id}":{"get":{"description":"Returns the full payload for one app build, including the JSON Schema and meta-schema that drive the installation settings form.","operationId":"get_app_build","parameters":[{"description":"App Build ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_AppBuildDetailResponse"}}},"description":"App build found"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"App build not found"}},"security":[{"bearer":[]}],"summary":"Get an app build","tags":["App Builds"]}},"/apps":{"get":{"description":"Returns a paginated list of app templates available to the given tenant.","operationId":"list_apps","parameters":[{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}},{"description":"Page number (default: 1)","in":"query","name":"page","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Items per page (default: 25, max: 100)","in":"query","name":"pageSize","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Sort field, prefix with - for descending","in":"query","name":"sort","required":false,"schema":{"type":"string"}},{"description":"Server-side text search","in":"query","name":"search","required":false,"schema":{"type":"string"}},{"description":"Filter by app type","in":"query","name":"filterType","required":false,"schema":{"type":"string"}},{"description":"Filter by app type (standard bracket form, alias of `filterType`)","in":"query","name":"filter[type]","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_AppResponse"}}},"description":"Paginated list of apps"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"500":{"description":"Internal server error"}},"security":[{"bearer":[]}],"summary":"List apps","tags":["Apps"]},"post":{"operationId":"create_app","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppCreateRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_AppCreateResponse"}}},"description":"App created"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"security":[{"bearer":[]}],"summary":"Create an app","tags":["Apps"]}},"/apps/{appId}/builds":{"get":{"description":"Paginated list of app builds (versions) for a single app. Designed for the console version-selector dropdown — schemas live on the detail endpoint and are not returned here.","operationId":"list_app_builds","parameters":[{"description":"App ID","in":"path","name":"appId","required":true,"schema":{"type":"string"}},{"description":"Page number (default: 1)","in":"query","name":"page","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Items per page (default: 25, max: 100)","in":"query","name":"pageSize","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Sort field; prefix with `-` for descending (default: `-createdAt`)","in":"query","name":"sort","required":false,"schema":{"type":"string"}},{"description":"CSV of statuses: processing, draft, error, published","in":"query","name":"status","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_AppBuildListItem"}}},"description":"Paginated list of app builds"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"App not found"}},"security":[{"bearer":[]}],"summary":"List app builds","tags":["App Builds"]},"post":{"operationId":"create_app_build","parameters":[{"description":"App ID","in":"path","name":"appId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppBuildCreateRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_AppBuildDetailResponse"}}},"description":"Build created (processing)"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"App not found"}},"security":[{"bearer":[]}],"summary":"Submit an app build","tags":["App Builds"]}},"/apps/{appId}/builds/upload-token":{"get":{"description":"Returns one-time SAS credentials to upload a build archive to blob storage. App-access-gated.","operationId":"upload_app_build_token","parameters":[{"description":"App ID","in":"path","name":"appId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_AppBuildUploadToken"}}},"description":"Upload credentials"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"App not found"}},"security":[{"bearer":[]}],"summary":"Get an app-build upload token","tags":["App Builds"]}},"/apps/{appId}/builds/{buildId}/publish":{"post":{"description":"Marks a processed build as published. 400 if the build is still processing or failed. CDN release-assembly is not performed here.","operationId":"publish_app_build","parameters":[{"description":"App ID","in":"path","name":"appId","required":true,"schema":{"type":"string"}},{"description":"App Build ID","in":"path","name":"buildId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_AppBuildDetailResponse"}}},"description":"Build published"},"400":{"description":"Build not in a publishable state"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"App or build not found"}},"security":[{"bearer":[]}],"summary":"Publish an app build","tags":["App Builds"]}},"/apps/{id}":{"delete":{"operationId":"delete_app","parameters":[{"description":"App ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"App deleted"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"App not found"}},"security":[{"bearer":[]}],"summary":"Delete an app","tags":["Apps"]},"get":{"description":"Returns a single app template by its ID.","operationId":"get_app","parameters":[{"description":"App ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_AppResponse"}}},"description":"App found"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"App not found"}},"security":[{"bearer":[]}],"summary":"Get an app","tags":["Apps"]},"put":{"operationId":"update_app","parameters":[{"description":"App ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppUpdateRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_AppResponse"}}},"description":"App updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"App not found"}},"security":[{"bearer":[]}],"summary":"Update an app","tags":["Apps"]}},"/auth/device/approve":{"post":{"operationId":"device_approve","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceApproveRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceApproveResponse"}}},"description":"Flow approved"},"400":{"description":"Bad request (e.g. unknown or expired user code)"},"401":{"description":"Unauthorized"}},"security":[{"bearer":[]}],"summary":"Approve a pending device flow","tags":["Auth"]}},"/auth/device/authorize":{"post":{"operationId":"device_authorize","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceAuthorizeRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceAuthorizeResponse"}}},"description":"Device flow started"}},"summary":"Start a device-authorization flow","tags":["Auth"]}},"/auth/device/token":{"post":{"description":"The gateway passes the upstream status and body through VERBATIM: core owns\nthe 200-vs-400 choice (200 with the PAT once approved, 400 carrying an RFC\n8628 error string while pending/denied/expired). The gateway must not remap\nthat semantics, so we use `raw_response` rather than parsing the body.","operationId":"device_token","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceTokenRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceTokenResponse"}}},"description":"Approved — Personal Access Token minted (shown once)"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceTokenError"}}},"description":"Flow not complete. `error` is an RFC 8628 string: `authorization_pending`, `slow_down`, `access_denied`, or `expired_token`."}},"summary":"Poll for the device-flow token","tags":["Auth"]}},"/auth/sessions":{"get":{"operationId":"list_sessions","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionListResponse"}}},"description":"Sessions"},"401":{"description":"Unauthorized"}},"security":[{"bearer":[]}],"summary":"List sessions","tags":["Auth"]}},"/auth/sessions/{id}":{"delete":{"operationId":"delete_session","parameters":[{"description":"Session id (token id or console session id)","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Session revoked"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden (not your session)"},"404":{"description":"Session not found"}},"security":[{"bearer":[]}],"summary":"Revoke a session","tags":["Auth"]}},"/cloud-instances":{"get":{"description":"Returns a paginated list of active cloud-app instances for the given tenant. Each instance corresponds to a phyhub cloud twin pinned to an `(installationId, spaceId)` pair. Mirrors the `/2026-03/devices` query / filter contract.","operationId":"list_cloud_instances","parameters":[{"description":"Page number (default: 1)","in":"query","name":"page","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Items per page (default: 25, max: 100)","in":"query","name":"pageSize","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Sort field (appName | createdAt | updatedAt)","in":"query","name":"sort","required":false,"schema":{"type":"string"}},{"description":"Server-side text search on appName","in":"query","name":"search","required":false,"schema":{"type":"string"}},{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}},{"description":"Filter by Installation ID","in":"query","name":"installationId","required":false,"schema":{"type":"string"}},{"description":"Filter by Gridapp ID","in":"query","name":"appId","required":false,"schema":{"type":"string"}},{"description":"Filter by Space ID","in":"query","name":"spaceId","required":false,"schema":{"type":"string"}},{"description":"Filter by twin status (e.g. 'Online', 'Offline')","in":"query","name":"status","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_CloudInstanceResponse"}}},"description":"Paginated list of cloud-app instances"},"400":{"description":"Invalid query params"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"500":{"description":"Internal server error"}},"security":[{"bearer":[]}],"summary":"List cloud-app instances","tags":["CloudInstances"]}},"/content-tags":{"get":{"description":"Returns a paginated list of content tags for the given tenant, optionally filtered by a case-insensitive name search.","operationId":"list_content_tags","parameters":[{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}},{"description":"1-based page (default: 1)","in":"query","name":"page","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Page size (default: 25, max: 1000)","in":"query","name":"pageSize","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Case-insensitive name search","in":"query","name":"search","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_ContentTagResponse"}}},"description":"Content tags"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"security":[{"bearer":[]}],"summary":"List content tags","tags":["ContentTags"]},"post":{"description":"Creates a content tag for a tenant. The tag name must be unique within the tenant (slug collision returns 409).","operationId":"create_content_tag","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentTagCreateRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_ContentTagResponse"}}},"description":"Content tag created"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"No access to tenant"},"409":{"description":"A tag with this name already exists for the tenant"}},"security":[{"bearer":[]}],"summary":"Create a content tag","tags":["ContentTags"]}},"/content-tags/{id}":{"delete":{"description":"Deletes a content tag by its ID.","operationId":"delete_content_tag","parameters":[{"description":"Content tag ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Content tag deleted"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Content tag not found"}},"security":[{"bearer":[]}],"summary":"Delete a content tag","tags":["ContentTags"]},"get":{"description":"Returns a single content tag by its ID. No tenantId needed — derived from the tag.","operationId":"get_content_tag","parameters":[{"description":"Content tag ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_ContentTagResponse"}}},"description":"Content tag"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Content tag not found"}},"security":[{"bearer":[]}],"summary":"Get a content tag","tags":["ContentTags"]},"put":{"description":"Updates a content tag and returns the updated version. Tenant is derived from the existing tag.","operationId":"update_content_tag","parameters":[{"description":"Content tag ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentTagUpdateRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_ContentTagResponse"}}},"description":"Content tag updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Content tag not found"},"409":{"description":"A tag with this name already exists for the tenant"}},"security":[{"bearer":[]}],"summary":"Update a content tag","tags":["ContentTags"]}},"/descriptors":{"get":{"description":"Returns the descriptors visible to the tenant — its own, public descriptors, descriptors specifically shared with it, and a parent tenant's child-visible descriptors (visibility is resolved across the parent↔child tenant link). Optionally filter by a case-insensitive name/displayName search and by visibility.","operationId":"list_descriptors","parameters":[{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}},{"description":"1-based page (default: 1)","in":"query","name":"page","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Page size (default: 25, max: 100)","in":"query","name":"pageSize","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Case-insensitive name/displayName search","in":"query","name":"search","required":false,"schema":{"type":"string"}},{"description":"Filter by visibility: public | child-tenants | own","in":"query","name":"visibility","required":false,"schema":{"type":"string"}},{"description":"Sort field (e.g. name, -createdAt)","in":"query","name":"sort","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_DescriptorResponse"}}},"description":"Descriptors"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"security":[{"bearer":[]}],"summary":"List descriptors","tags":["Descriptors"]},"post":{"operationId":"create_descriptor","parameters":[{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DescriptorCreateRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_DescriptorResponse"}}},"description":"Descriptor created"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"409":{"description":"A descriptor with this name already exists"}},"security":[{"bearer":[]}],"summary":"Create a descriptor","tags":["Descriptors"]}},"/descriptors/{name}":{"get":{"operationId":"get_descriptor","parameters":[{"description":"Descriptor name (kebab-case)","in":"path","name":"name","required":true,"schema":{"type":"string"}},{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_DescriptorResponse"}}},"description":"Descriptor"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Descriptor not found"}},"security":[{"bearer":[]}],"summary":"Get a descriptor","tags":["Descriptors"]}},"/descriptors/{name}/versions":{"post":{"operationId":"create_descriptor_version","parameters":[{"description":"Descriptor name (kebab-case)","in":"path","name":"name","required":true,"schema":{"type":"string"}},{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DescriptorVersionCreateRequest"}}},"required":true},"responses":{"201":{"description":"Version published"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Descriptor not found"},"409":{"description":"Version already exists"}},"security":[{"bearer":[]}],"summary":"Publish a descriptor version","tags":["Descriptors"]}},"/descriptors/{name}/versions/{version}":{"get":{"description":"Returns one immutable descriptor version's schema and metaSchema. Doubles as the S2S source phyhub caches versions from (a published version never changes, so it is safe to cache by tenant+name+version).","operationId":"get_descriptor_version","parameters":[{"description":"Descriptor name (kebab-case)","in":"path","name":"name","required":true,"schema":{"type":"string"}},{"description":"Semantic version (e.g. 1.0.0)","in":"path","name":"version","required":true,"schema":{"type":"string"}},{"description":"Owning tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_DescriptorVersionResponse"}}},"description":"Descriptor version"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Descriptor or version not found"}},"security":[{"bearer":[]}],"summary":"Get a descriptor version (schema)","tags":["Descriptors"]}},"/devices":{"get":{"description":"Returns a paginated list of devices for the given tenant. Routes to PhyHub or legacy backend based on tenant configuration. Supports opt-in response field selection via `fields=` (see parameter below) to shrink payloads for enrichment workloads.","operationId":"list_devices","parameters":[{"description":"Page number (default: 1)","in":"query","name":"page","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Items per page (default: 25, max: 100)","in":"query","name":"pageSize","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Sort field, prefix with - for descending","in":"query","name":"sort","required":false,"schema":{"type":"string"}},{"description":"Server-side text search","in":"query","name":"search","required":false,"schema":{"type":"string"}},{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}},{"description":"Filter by space ID","in":"query","name":"spaceId","required":false,"schema":{"type":"string"}},{"description":"Filter by device status","in":"query","name":"status","required":false,"schema":{"type":"string"}},{"description":"Comma-separated allowlist of response fields to include in each item. Unknown field names return HTTP 400. Omit for the full shape. The `id` field is always included. Example: `fields=id,tenantId,displayName,spaceId,deviceSerial,timezone,installationIds,status`","in":"query","name":"fields","required":false,"schema":{"type":"string"}},{"description":"Comma-separated list of device IDs to fetch in a single request. Capped at 100 IDs per request. Invalid IDs return HTTP 400 with the first bad value named.","in":"query","name":"filter[id]","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_DeviceResponse"}}},"description":"Paginated list of devices"},"400":{"description":"Invalid query params (e.g. unknown field in `fields`)"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"500":{"description":"Internal server error"}},"security":[{"bearer":[]}],"summary":"List devices","tags":["Devices"]},"post":{"description":"Creates a new device. Only supported for PhyHub tenants; legacy tenants receive 405.","operationId":"create_device","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceCreateRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_DeviceResponse"}}},"description":"Device created"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"405":{"description":"Method not allowed (legacy tenant)"}},"security":[{"bearer":[]}],"summary":"Create a device","tags":["Devices"]}},"/devices/{deviceId}/analytics/query":{"post":{"description":"Unified analytics query for a device. Tenant resolved automatically. Specify metrics explicitly or set schema=true for dashboard-driven queries.","operationId":"post_device_analytics_query","parameters":[{"description":"Device ID","in":"path","name":"deviceId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"dateFrom":"2026-03-01","dateTo":"2026-03-31","granularity":"day","metrics":["events","sessions"]},"schema":{"$ref":"#/components/schemas/AnalyticsQueryRequest"}}},"description":"Analytics query configuration","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsQueryResponse"}}},"description":"Analytics data for requested metrics"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Invalid query (e.g., both metrics and schema specified)"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Device not found"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Admin API unavailable"}},"security":[{"bearer":[]}],"summary":"Query device analytics","tags":["Analytics"]}},"/devices/{deviceId}/analytics/rank":{"post":{"description":"Returns the ordered row-key list for a DataMatrix card scoped to a device. Tenant is resolved from the device. Current schemas have no device-scope DataMatrix cards, so this endpoint typically responds with an empty `orderedKeys` array.","operationId":"post_device_analytics_rank","parameters":[{"description":"Device ID","in":"path","name":"deviceId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsRankRequest"}}},"description":"Rank request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsRankResponse"}}},"description":"Ordered key list"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Invalid rank request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Device, schema, or card not found"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Admin API unavailable"}},"security":[{"bearer":[]}],"summary":"Rank device DataMatrix card","tags":["Analytics"]}},"/devices/{deviceId}/analytics/schema":{"get":{"description":"Return the device's analytics schema tree. Tenant resolved automatically.","operationId":"get_device_analytics_schema","parameters":[{"description":"Device ID","in":"path","name":"deviceId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Schema tree with computed ids"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Device not found or missing schema"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Admin API unavailable"}},"security":[{"bearer":[]}],"summary":"Fetch device analytics schema","tags":["Analytics"]}},"/devices/{id}":{"delete":{"description":"Deletes a device by its ID. The tenant is resolved automatically. Only supported for PhyHub tenants; legacy tenants receive 405.","operationId":"delete_device","parameters":[{"description":"Device ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Device deleted"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Device not found"},"405":{"description":"Method not allowed (legacy tenant)"}},"security":[{"bearer":[]}],"summary":"Delete a device","tags":["Devices"]},"get":{"description":"Returns a single device by its ID. The tenant is resolved automatically. Routes to PhyHub or legacy backend based on tenant configuration.","operationId":"get_device","parameters":[{"description":"Device ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_DeviceResponse"}}},"description":"Device found"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Device not found"}},"security":[{"bearer":[]}],"summary":"Get a device","tags":["Devices"]},"put":{"description":"Updates an existing device. The tenant is resolved automatically. Routes to PhyHub or legacy backend based on tenant configuration.","operationId":"update_device","parameters":[{"description":"Device ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceUpdateRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_DeviceResponse"}}},"description":"Device updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Device not found"}},"security":[{"bearer":[]}],"summary":"Update a device","tags":["Devices"]}},"/devices/{id}/reboot":{"post":{"operationId":"reboot_device","parameters":[{"description":"Device ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_DeviceCommandResponse"}}},"description":"Reboot queued"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Device not found"},"405":{"description":"Method not allowed (legacy tenant)"}},"security":[{"bearer":[]}],"summary":"Reboot a device","tags":["Devices"]}},"/devices/{id}/shell":{"post":{"description":"Opens a live SSH-over-WebSocket tunnel to a PhyHub device and returns the device-streams endpoint the client bridges a local SSH process over. Only supported for PhyHub tenants; legacy tenants receive 405.","operationId":"shell_device","parameters":[{"description":"Device ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceShellRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_DeviceShellResponse"}}},"description":"Tunnel ready"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Device not found"},"405":{"description":"Method not allowed (legacy tenant)"}},"security":[{"bearer":[]}],"summary":"Open a shell tunnel to a device","tags":["Devices"]}},"/installations":{"get":{"description":"Returns a paginated list of installations for the given tenant. Supports optional response field selection via `fields=` to reduce payload size.","operationId":"list_installations","parameters":[{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}},{"description":"Page number (default: 1)","in":"query","name":"page","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Items per page (default: 25, max: 100)","in":"query","name":"pageSize","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Sort field, prefix with - for descending","in":"query","name":"sort","required":false,"schema":{"type":"string"}},{"description":"Server-side text search","in":"query","name":"search","required":false,"schema":{"type":"string"}},{"description":"Filter by App platform (SCREEN | EDGE | WEB | CLOUD | QUEUE_LEGACY)","in":"query","name":"filter[appType]","required":false,"schema":{"type":"string"}},{"description":"Filter by installation group ID","in":"query","name":"filter[installationGroupId]","required":false,"schema":{"type":"string"}},{"description":"Comma-separated optional fields: analyticsSchema","in":"query","name":"include","required":false,"schema":{"type":"string"}},{"description":"Comma-separated list of installation IDs to fetch in a single request. Set `pageSize` to at least the number of IDs to avoid clipping. Invalid IDs return HTTP 400 with the first bad value named.","in":"query","name":"filter[id]","required":false,"schema":{"type":"string"}},{"description":"Comma-separated allowlist of response fields to include in each item. Unknown field names return HTTP 400. Omit for the full shape. The `id` field is always included. Fields requested via `include=` (e.g. `analyticsSchema`) are preserved. Example: `fields=id,displayName,appId,iconUrl`","in":"query","name":"fields","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_InstallationResponse"}}},"description":"Paginated list of installations"},"400":{"description":"Bad request (e.g. unknown `fields` name)"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"500":{"description":"Internal server error"}},"security":[{"bearer":[]}],"summary":"List installations","tags":["Installations"]},"post":{"description":"Installs an App for a tenant. The body identifies the App template by `appId`; optionally pin to a specific `appBuildId`.","operationId":"create_installation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstallationCreateRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_InstallationResponse"}}},"description":"Installation created"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"No access to tenant"},"404":{"description":"App (`appId`) not found"},"422":{"description":"`appBuildId` does not belong to `appId`"}},"security":[{"bearer":[]}],"summary":"Create an installation","tags":["Installations"]}},"/installations/{id}":{"delete":{"description":"Deletes an installation by its ID.","operationId":"delete_installation","parameters":[{"description":"Installation ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Installation deleted"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Installation not found"}},"security":[{"bearer":[]}],"summary":"Delete an installation","tags":["Installations"]},"get":{"description":"Returns a single installation by its ID.","operationId":"get_installation","parameters":[{"description":"Installation ID","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Comma-separated optional fields: analyticsSchema. No tenantId needed — derived from the installation.","in":"query","name":"include","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_InstallationResponse"}}},"description":"Installation found"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Installation not found"}},"security":[{"bearer":[]}],"summary":"Get an installation","tags":["Installations"]},"put":{"description":"Updates an existing installation and returns the updated version. Requires the current version number for optimistic concurrency — a mismatch returns 409.","operationId":"update_installation","parameters":[{"description":"Installation ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstallationUpdateRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_InstallationResponse"}}},"description":"Installation updated"},"400":{"description":"Bad request (e.g. invalid multi-language config)"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Installation not found"},"409":{"description":"Version mismatch — installation was updated by another caller"}},"security":[{"bearer":[]}],"summary":"Update an installation","tags":["Installations"]}},"/installations/{id}/activate":{"post":{"description":"Creates the cloud-app instance twin for `(installationId, spaceId)`. Returns 409 if the Space is already activated.","operationId":"activate_installation","parameters":[{"description":"Installation ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstallationActivationRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_Vec_CloudInstanceResponse"}}},"description":"Installation activated in the Space"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Installation not found"},"409":{"description":"Space is already activated for this installation"},"422":{"description":"Installation is not a cloud app"}},"security":[{"bearer":[]}],"summary":"Activate an installation in a Space","tags":["Installations"]}},"/installations/{id}/clone":{"post":{"description":"Deep-copies an Edge (IOT) or Screen (PWA) installation — settings and space/device overrides — under a new display name, kicking off an auto-build.","operationId":"clone_installation","parameters":[{"description":"Source installation ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CloneInstallationRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_InstallationResponse"}}},"description":"Installation cloned"},"400":{"description":"Installation type is not cloneable (Edge/Screen only)"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Installation not found"}},"security":[{"bearer":[]}],"summary":"Clone an installation","tags":["Installations"]}},"/installations/{id}/deactivate":{"post":{"description":"Soft-deletes the cloud-app instance twin for `(installationId, spaceId)`. Idempotent — returns 204 even if no twin exists for that Space.","operationId":"deactivate_installation","parameters":[{"description":"Installation ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstallationActivationRequest"}}},"required":true},"responses":{"204":{"description":"Installation deactivated in the Space"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Installation not found"}},"security":[{"bearer":[]}],"summary":"Deactivate an installation in a Space","tags":["Installations"]}},"/installations/{id}/devices":{"get":{"operationId":"list_installation_devices","parameters":[{"description":"Installation ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_DeviceResponse"}}},"description":"Devices"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Installation not found"}},"security":[{"bearer":[]}],"summary":"List devices assigned to an installation","tags":["Installations"]},"post":{"operationId":"assign_installation_device","parameters":[{"description":"Installation ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignDeviceRequest"}}},"required":true},"responses":{"200":{"description":"Assigned"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Installation not found"}},"security":[{"bearer":[]}],"summary":"Assign a device to an installation","tags":["Installations"]}},"/installations/{id}/devices/{deviceId}":{"delete":{"operationId":"unassign_installation_device","parameters":[{"description":"Installation ID","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Device ID","in":"path","name":"deviceId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Unassigned"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Installation or device not found"}},"security":[{"bearer":[]}],"summary":"Unassign a device from an installation","tags":["Installations"]}},"/installations/{id}/settings":{"get":{"description":"Returns the pinned `appBuildId` and schema-driven `values` for an installation. Metadata (`displayName`, languages, …) lives on the parent installation resource.","operationId":"get_installation_settings","parameters":[{"description":"Installation ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_InstallationSettingsResponse"}}},"description":"Settings"},"400":{"description":"Installation has no pinned build"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Installation not found"}},"security":[{"bearer":[]}],"summary":"Get installation settings","tags":["Installations"]},"put":{"description":"Atomically updates the pinned `appBuildId` and `values`. Optimistic concurrency via `version` (409 on mismatch). When `?autoDeployEnvironment=` is set, a release build is triggered after the save.","operationId":"update_installation_settings","parameters":[{"description":"Installation ID","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Optional release-pipeline trigger; when set, fires the build after save.","in":"query","name":"autoDeployEnvironment","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstallationSettingsUpdateRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_InstallationSettingsResponse"}}},"description":"Settings updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Installation or build not found"},"409":{"description":"Version mismatch — settings were updated by another caller"},"422":{"description":"`appBuildId` does not belong to the installation's `appId`"}},"security":[{"bearer":[]}],"summary":"Update installation settings","tags":["Installations"]}},"/installations/{id}/settings/migration":{"get":{"description":"Returns the schema delta (added / removed / retyped top-level properties) between the installation's currently pinned build and the target build (`?appBuildId=`), plus whether the installation's current `values` already validate against the target schema. Read-only. Used to plan a safe version migration before writing via `PUT /installations/{id}/settings`.","operationId":"get_settings_migration","parameters":[{"description":"Installation ID","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Target build to diff the pinned-build schema against.","in":"query","name":"appBuildId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_SettingsMigrationResponse"}}},"description":"Schema delta"},"400":{"description":"Installation has no pinned build"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Installation or target build not found"},"422":{"description":"Target build does not belong to the installation's app"}},"security":[{"bearer":[]}],"summary":"Diff settings schema for a version migration","tags":["Installations"]}},"/installations/{installationId}/analytics/query":{"post":{"description":"Unified analytics query for an installation. Tenant resolved automatically. Specify metrics explicitly or set schema=true for dashboard-driven queries.","operationId":"post_installation_analytics_query","parameters":[{"description":"Installation ID","in":"path","name":"installationId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"dateFrom":"2026-03-01","dateTo":"2026-03-31","granularity":"day","metrics":["events","sessions","nps"]},"schema":{"$ref":"#/components/schemas/AnalyticsQueryRequest"}}},"description":"Analytics query configuration","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsQueryResponse"}}},"description":"Analytics data for requested metrics"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Invalid query (e.g., both metrics and schema specified)"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Installation not found"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Admin API unavailable"}},"security":[{"bearer":[]}],"summary":"Query installation analytics","tags":["Analytics"]}},"/installations/{installationId}/analytics/rank":{"post":{"description":"Returns the ordered row-key list for a DataMatrix card scoped to an installation. Tenant is resolved from the installation.","operationId":"post_installation_analytics_rank","parameters":[{"description":"Installation ID","in":"path","name":"installationId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsRankRequest"}}},"description":"Rank request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsRankResponse"}}},"description":"Ordered key list"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Invalid rank request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Installation, schema, or card not found"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Admin API unavailable"}},"security":[{"bearer":[]}],"summary":"Rank installation DataMatrix card","tags":["Analytics"]}},"/installations/{installationId}/analytics/schema":{"get":{"description":"Return the installation's analytics schema tree. Tenant resolved automatically.","operationId":"get_installation_analytics_schema","parameters":[{"description":"Installation ID","in":"path","name":"installationId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Schema tree with computed ids"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Installation not found or missing schema"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Admin API unavailable"}},"security":[{"bearer":[]}],"summary":"Fetch installation analytics schema","tags":["Analytics"]}},"/installations/{installationId}/devices/{deviceId}/settings":{"delete":{"description":"Removes the Device override (idempotent — 204 even if none existed). Triggers a scoped build when an override was cleared.","operationId":"delete_device_override","parameters":[{"description":"Installation ID","in":"path","name":"installationId","required":true,"schema":{"type":"string"}},{"description":"Device ID","in":"path","name":"deviceId","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Override cleared"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Installation not found"}},"security":[{"bearer":[]}],"summary":"Clear a Device settings override","tags":["Installations"]},"get":{"description":"Returns the JSON-Pointer delta override for a Device, or the fully-merged effective settings when `?resolved=true` (precedence device > space > installation).","operationId":"get_device_override","parameters":[{"description":"Installation ID","in":"path","name":"installationId","required":true,"schema":{"type":"string"}},{"description":"Device ID","in":"path","name":"deviceId","required":true,"schema":{"type":"string"}},{"description":"Return merged effective settings instead of the raw delta","in":"query","name":"resolved","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_OverrideResponse"}}},"description":"Override delta or resolved settings"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Installation not found"}},"security":[{"bearer":[]}],"summary":"Get a Device settings override","tags":["Installations"]},"put":{"description":"Applies a JSON-Pointer delta as the Device override. The resulting resolved settings are validated against the installation's pinned build schema (422 on failure). Optimistic concurrency via `version` (409 on mismatch). Triggers a scoped edge build; cloud installations don't build on device overrides.","operationId":"put_device_override","parameters":[{"description":"Installation ID","in":"path","name":"installationId","required":true,"schema":{"type":"string"}},{"description":"Device ID","in":"path","name":"deviceId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OverrideUpdateRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_OverrideResponse"}}},"description":"Override saved"},"400":{"description":"Malformed ops / JSON Pointer"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Installation not found"},"409":{"description":"Version mismatch"},"422":{"description":"Resolved settings failed schema validation"}},"security":[{"bearer":[]}],"summary":"Set a Device settings override","tags":["Installations"]}},"/installations/{installationId}/spaces/{spaceId}/settings":{"delete":{"description":"Removes the Space override (idempotent — 204 even if none existed). Triggers a scoped build when an override was cleared.","operationId":"delete_space_override","parameters":[{"description":"Installation ID","in":"path","name":"installationId","required":true,"schema":{"type":"string"}},{"description":"Space ID","in":"path","name":"spaceId","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Override cleared"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Installation not found"}},"security":[{"bearer":[]}],"summary":"Clear a Space settings override","tags":["Installations"]},"get":{"description":"Returns the JSON-Pointer delta override for a Space, or the fully-merged effective settings when `?resolved=true` (precedence device > space > installation).","operationId":"get_space_override","parameters":[{"description":"Installation ID","in":"path","name":"installationId","required":true,"schema":{"type":"string"}},{"description":"Space ID","in":"path","name":"spaceId","required":true,"schema":{"type":"string"}},{"description":"Return merged effective settings instead of the raw delta","in":"query","name":"resolved","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_OverrideResponse"}}},"description":"Override delta or resolved settings"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Installation not found"}},"security":[{"bearer":[]}],"summary":"Get a Space settings override","tags":["Installations"]},"put":{"description":"Applies a JSON-Pointer delta as the Space override. The resulting resolved settings are validated against the installation's pinned build schema (422 on failure). Optimistic concurrency via `version` (409 on mismatch). Triggers a scoped build.","operationId":"put_space_override","parameters":[{"description":"Installation ID","in":"path","name":"installationId","required":true,"schema":{"type":"string"}},{"description":"Space ID","in":"path","name":"spaceId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OverrideUpdateRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_OverrideResponse"}}},"description":"Override saved"},"400":{"description":"Malformed ops / JSON Pointer"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Installation not found"},"409":{"description":"Version mismatch"},"422":{"description":"Resolved settings failed schema validation"}},"security":[{"bearer":[]}],"summary":"Set a Space settings override","tags":["Installations"]}},"/invitations":{"get":{"description":"Paginated pending invitations scoped to a tenant, optionally filtered by email search.","operationId":"list_invitations","parameters":[{"description":"Tenant the invitations belong to","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}},{"description":"1-based page","in":"query","name":"page","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Page size (max 100)","in":"query","name":"pageSize","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Case-insensitive email search","in":"query","name":"search","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_InvitationResponse"}}},"description":"Invitations"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"security":[{"bearer":[]}],"summary":"List invitations","tags":["Invitations"]},"post":{"description":"Invites a person (by `email`) to one or more tenants with the given roles. Sends an invitation email on success.","operationId":"create_invitation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvitationCreateRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_InvitationResponse"}}},"description":"Invitation created"},"400":{"description":"Bad request (duplicate invite, disallowed email domain, invalid role)"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Tenant not found"}},"security":[{"bearer":[]}],"summary":"Create an invitation","tags":["Invitations"]}},"/invitations/{id}":{"delete":{"description":"Revokes a pending invitation by its id.","operationId":"delete_invitation","parameters":[{"description":"Invitation id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Invitation deleted"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Invitation not found"}},"security":[{"bearer":[]}],"summary":"Delete an invitation","tags":["Invitations"]},"get":{"operationId":"get_invitation","parameters":[{"description":"Invitation id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_InvitationResponse"}}},"description":"Invitation"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Invitation not found"}},"security":[{"bearer":[]}],"summary":"Get an invitation","tags":["Invitations"]}},"/invoices":{"get":{"description":"Paginated, tenant-scoped invoices over a date window. Invoices are system-generated by the billing service and read-only.","operationId":"list_invoices","parameters":[{"description":"Tenant whose invoices to list","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}},{"description":"ISO 8601 start of the invoice date window (defaults to 12 months ago)","in":"query","name":"startDate","required":false,"schema":{"type":"string"}},{"description":"ISO 8601 end of the invoice date window (defaults to now)","in":"query","name":"endDate","required":false,"schema":{"type":"string"}},{"description":"1-based page","in":"query","name":"page","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Page size (max 1000)","in":"query","name":"pageSize","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_InvoiceResponse"}}},"description":"Invoices"},"400":{"description":"Missing or invalid query parameters"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"security":[{"bearer":[]}],"summary":"List invoices","tags":["Invoices"]}},"/invoices/{id}":{"get":{"operationId":"get_invoice","parameters":[{"description":"Invoice id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_InvoiceResponse"}}},"description":"Invoice"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Invoice not found"}},"security":[{"bearer":[]}],"summary":"Get an invoice","tags":["Invoices"]}},"/media":{"get":{"description":"Browse a folder's children plus its breadcrumb, or — when `search` is set — filter the tenant's media by name across the whole tree (server-side). This is an upgrade over the legacy console's client-side-only search.","operationId":"list_media","parameters":[{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}},{"description":"Folder id to browse (omitted = tenant root)","in":"query","name":"folder","required":false,"schema":{"type":"string"}},{"description":"Case-insensitive name search across the tree","in":"query","name":"search","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaListResponse"}}},"description":"Folder contents + breadcrumb, or search results"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"security":[{"bearer":[]}],"summary":"Browse or search media","tags":["Media"]}},"/media/folders":{"post":{"operationId":"create_media_folder","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaFolderCreateRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaResponse"}}},"description":"Folder created"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"No access to tenant"}},"security":[{"bearer":[]}],"summary":"Create a folder","tags":["Media"]}},"/media/uploads":{"post":{"description":"Allocates an asset id and returns a presigned Azure SAS `uploadUrl`. The client PUTs the file bytes to `uploadUrl` (with header `x-ms-blob-type: BlockBlob`), then calls `POST /media/{assetId}/complete` to register the asset record.","operationId":"create_media_upload","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaUploadRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaUploadTarget"}}},"description":"Presigned upload target"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"No access to tenant"}},"security":[{"bearer":[]}],"summary":"Begin a media upload","tags":["Media"]}},"/media/{assetId}/complete":{"post":{"description":"Registers the asset record after the client has PUT the bytes to the presigned `uploadUrl`. `{assetId}` is the value returned by `POST /media/uploads`.","operationId":"complete_media_upload","parameters":[{"description":"Asset id (blob uuid) from the upload step","in":"path","name":"assetId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaCompleteRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaResponse"}}},"description":"Asset registered"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"No access to tenant"}},"security":[{"bearer":[]}],"summary":"Finalize a media upload","tags":["Media"]}},"/media/{mediaId}":{"delete":{"description":"Deletes a media item. For a folder, all descendants are cascade-deleted.","operationId":"delete_media","parameters":[{"description":"Media id","in":"path","name":"mediaId","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Media deleted"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Media not found"}},"security":[{"bearer":[]}],"summary":"Delete media","tags":["Media"]},"get":{"description":"Returns a media asset or folder. Assets include a CDN download `url`.","operationId":"get_media","parameters":[{"description":"Media id","in":"path","name":"mediaId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaResponse"}}},"description":"Media item"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Media not found"}},"security":[{"bearer":[]}],"summary":"Get media detail","tags":["Media"]},"patch":{"description":"Renames (`name`) and/or moves (`parentId`) a media item. Moving recomputes the item's ancestor lineage. `parentId: null` moves to the tenant root.","operationId":"update_media","parameters":[{"description":"Media id","in":"path","name":"mediaId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaUpdateRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaResponse"}}},"description":"Media updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Media not found"}},"security":[{"bearer":[]}],"summary":"Rename or move media","tags":["Media"]}},"/peripherals":{"get":{"description":"Returns a paginated list of peripherals for the given tenant. Optionally search by name/hardware id and filter by `deviceId` or `descriptor`.","operationId":"list_peripherals","parameters":[{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}},{"description":"1-based page (default: 1)","in":"query","name":"page","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Page size (default: 25, max: 100)","in":"query","name":"pageSize","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Sort field (`createdAt` | `updatedAt` | `name` | `status`), prefix with - for descending","in":"query","name":"sort","required":false,"schema":{"type":"string"}},{"description":"Text search on peripheral name, display name, and hardware id (min 2 characters)","in":"query","name":"search","required":false,"schema":{"type":"string"}},{"description":"Filter to a single device","in":"query","name":"deviceId","required":false,"schema":{"type":"string"}},{"description":"Filter to a descriptor uniqueName","in":"query","name":"descriptor","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_PeripheralListItem"}}},"description":"Peripherals"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"security":[{"bearer":[]}],"summary":"List peripherals","tags":["Peripherals"]}},"/peripherals/{id}":{"delete":{"description":"Soft-delete a peripheral twin. Operator-facing cleanup (e.g. an orphaned twin whose owning app no longer creates it); consumers are notified over phyhub's twin-message bus. If the owning app is still running and configured for this peripheral, it may re-create the twin on its next create.","operationId":"delete_peripheral","parameters":[{"description":"Peripheral (twin) id","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Tenant id that owns the peripheral","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Peripheral deleted"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Peripheral not found"}},"security":[{"bearer":[]}],"summary":"Delete a peripheral","tags":["Peripherals"]},"get":{"operationId":"get_peripheral","parameters":[{"description":"Peripheral ID","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_PeripheralResponse"}}},"description":"Peripheral"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Peripheral not found"}},"security":[{"bearer":[]}],"summary":"Get a peripheral","tags":["Peripherals"]}},"/peripherals/{id}/actions":{"get":{"description":"Lists the actions a peripheral declares via its descriptor(s). Use `POST /peripherals/{id}/actions/{name}` to invoke one.","operationId":"get_peripheral_actions","parameters":[{"description":"Peripheral ID","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Actions"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Peripheral not found"}},"security":[{"bearer":[]}],"summary":"List peripheral actions","tags":["Peripherals"]}},"/peripherals/{id}/actions/{name}":{"post":{"description":"Invokes a named action on a peripheral and returns the owner's result. The request body is the action's params object — validated (in phyhub) against the action's descriptor schema. Errors: the peripheral does not exist or belongs to another tenant → 404; the action is not declared by any of the peripheral's descriptors → 404; params fail schema validation → 422; the peripheral is offline and cannot receive the action → 409; the owner does not respond in time → 504.","operationId":"invoke_peripheral_action","parameters":[{"description":"Peripheral ID","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Action name (as declared in the descriptor)","in":"path","name":"name","required":true,"schema":{"type":"string"}},{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"copies":2},"schema":{}}},"description":"Action params object (shape defined by the action's descriptor schema). Send `{}` for a parameterless action.","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_PeripheralActionResult"}}},"description":"Action result"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Peripheral not found, or action not declared by its descriptors"},"409":{"description":"Peripheral is offline and cannot receive the action"},"422":{"description":"Params failed the action's descriptor schema validation"},"504":{"description":"The peripheral owner did not respond in time"}},"security":[{"bearer":[]}],"summary":"Invoke a peripheral action","tags":["Peripherals"]}},"/peripherals/{id}/events":{"get":{"description":"Lists the events a peripheral declares via its descriptor(s). Discovery only — events are not subscribable on the 2026-03 surface (event/stream subscribe is an on-device app-to-app concern).","operationId":"get_peripheral_events","parameters":[{"description":"Peripheral ID","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Events"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Peripheral not found"}},"security":[{"bearer":[]}],"summary":"List peripheral events","tags":["Peripherals"]}},"/peripherals/{id}/schema":{"get":{"description":"Returns the peripheral's full contract, merged from its pinned descriptor versions: the `desiredProperties` facet is the settings contract (identity-excluded, strict — the same schema `PUT /settings` validates against), `reportedProperties` is the telemetry contract, and `actions`/`events` describe the invokable surface. `metaSchema` is the matching UI sidecar. Contract discovery only — values live on `/settings` and `/telemetry`.","operationId":"get_peripheral_schema","parameters":[{"description":"Peripheral ID","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_PeripheralSchemaResponse"}}},"description":"Peripheral schema"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Peripheral not found"}},"security":[{"bearer":[]}],"summary":"Get the peripheral schema","tags":["Peripherals"]}},"/peripherals/{id}/settings":{"get":{"operationId":"get_peripheral_settings","parameters":[{"description":"Peripheral ID","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PeripheralSettingsResponse"}}},"description":"Settings"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Peripheral not found"}},"security":[{"bearer":[]}],"summary":"Get peripheral settings","tags":["Peripherals"]},"put":{"description":"Partially merges the provided settings into the peripheral's operator-writable settings: keys present in the request are set/updated; keys omitted from the request are NOT removed (this is a merge, not a full replace). There is no way to remove a settings key via this endpoint. The settings object is validated (strict) against the peripheral's descriptor schema; invalid settings or a peripheral with no descriptor return 422.","operationId":"update_peripheral_settings","parameters":[{"description":"Peripheral ID","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PeripheralSettingsUpdateRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_PeripheralResponse"}}},"description":"Updated peripheral. Settings reflect the merge of the request into the prior settings; keys omitted from the request remain set (they cannot be removed via this endpoint)."},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Peripheral not found"},"422":{"description":"Settings failed schema validation / no descriptor schema"}},"security":[{"bearer":[]}],"summary":"Update peripheral settings","tags":["Peripherals"]}},"/peripherals/{id}/telemetry":{"get":{"operationId":"get_peripheral_telemetry","parameters":[{"description":"Peripheral ID","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PeripheralTelemetryResponse"}}},"description":"Telemetry"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Peripheral not found"}},"security":[{"bearer":[]}],"summary":"Get peripheral telemetry","tags":["Peripherals"]}},"/roles":{"get":{"description":"Paginated roles, scoped to a tenant and optionally filtered by a case-insensitive name search.","operationId":"list_roles","parameters":[{"description":"Scope to roles belonging to this tenant","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}},{"description":"1-based page","in":"query","name":"page","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Page size (max 1000)","in":"query","name":"pageSize","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Case-insensitive name search","in":"query","name":"search","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_RoleResponse"}}},"description":"Roles"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"security":[{"bearer":[]}],"summary":"List roles","tags":["Roles"]}},"/roles/{id}":{"get":{"operationId":"get_role","parameters":[{"description":"Role id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_RoleResponse"}}},"description":"Role"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Role not found"}},"security":[{"bearer":[]}],"summary":"Get a role","tags":["Roles"]}},"/short-urls":{"get":{"description":"Returns a paginated list of custom short URLs for the given tenant, optionally filtered by a substring search over display name / redirect URL.","operationId":"list_short_urls","parameters":[{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}},{"description":"Page number (default: 1)","in":"query","name":"page","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Items per page (default: 25, max: 100)","in":"query","name":"pageSize","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Case-insensitive substring search","in":"query","name":"search","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_ShortUrlResponse"}}},"description":"Paginated list of short URLs"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"security":[{"bearer":[]}],"summary":"List short URLs","tags":["ShortUrls"]},"post":{"description":"Mints a custom short URL for a tenant. The caller chooses the slug via `shortId`.","operationId":"create_short_url","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShortUrlCreateRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_ShortUrlResponse"}}},"description":"Short URL created"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"No access to tenant"}},"security":[{"bearer":[]}],"summary":"Create a short URL","tags":["ShortUrls"]}},"/short-urls/{id}":{"delete":{"description":"`tenantId` rides the body so admin-api can verify tenant access — the slug\nalone doesn't carry tenant ownership.","operationId":"delete_short_url","parameters":[{"description":"Short URL slug","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Short URL deleted"},"401":{"description":"Unauthorized"},"403":{"description":"No access to tenant"},"404":{"description":"Short URL not found"}},"security":[{"bearer":[]}],"summary":"Delete a short URL","tags":["ShortUrls"]},"get":{"operationId":"get_short_url","parameters":[{"description":"Short URL slug","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_ShortUrlResponse"}}},"description":"Short URL"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Short URL not found"}},"security":[{"bearer":[]}],"summary":"Get a short URL","tags":["ShortUrls"]},"put":{"description":"Updates the display name and redirect URL of a custom short URL. The slug is immutable.","operationId":"update_short_url","parameters":[{"description":"Short URL slug","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShortUrlUpdateRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_ShortUrlResponse"}}},"description":"Short URL updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"No access to tenant"},"404":{"description":"Short URL not found"}},"security":[{"bearer":[]}],"summary":"Update a short URL","tags":["ShortUrls"]}},"/spaces":{"get":{"description":"Returns a paginated list of spaces for the given tenant. Supports optional response field selection via `fields=` to reduce payload size, and per-row descendant embedding via `include=children`.","operationId":"list_spaces","parameters":[{"description":"Page number (default: 1)","in":"query","name":"page","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Items per page (default: 25, max: 1000)","in":"query","name":"pageSize","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Sort field, prefix with - for descending","in":"query","name":"sort","required":false,"schema":{"type":"string"}},{"description":"Server-side text search","in":"query","name":"search","required":false,"schema":{"type":"string"}},{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}},{"description":"Comma-separated allowlist of response fields to include in each item. Unknown field names return HTTP 400. Omit for the full shape. The `id` field is always included. Example: `fields=id,tenantId,parentSpaceId,displayName,externalId,timezone,type`","in":"query","name":"fields","required":false,"schema":{"type":"string"}},{"description":"Narrow the list to a single space type (`LOCATION`, `COUNTRY`, `REGION`, `CITY`, `FLOOR`, `SECTION`, `CUSTOM`).","in":"query","name":"filter[type]","required":false,"schema":{"type":"string"}},{"description":"Narrow the list to direct children of the given parent space. Accepts a comma-separated list to union the children of several parents.","in":"query","name":"filter[parentSpaceId]","required":false,"schema":{"type":"string"}},{"description":"Comma-separated list of space IDs to fetch in a single request. Set `pageSize` to at least the number of IDs to avoid clipping. Invalid IDs return HTTP 400 with the first bad value named.","in":"query","name":"filter[id]","required":false,"schema":{"type":"string"}},{"description":"Set to `children` to embed each row's transitive descendants (all levels, flattened) as a `children` array. The `children` field is never stripped by a `fields=` projection.","in":"query","name":"include","required":false,"schema":{"type":"string"}},{"description":"Comma-separated list of space types to include in each row's `children` array (e.g. `FLOOR,SECTION,CUSTOM`). Only applied when `include=children` is set.","in":"query","name":"filter[childrenType]","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_SpaceResponse"}}},"description":"Paginated list of spaces"},"400":{"description":"Invalid query params (e.g. unknown field in `fields`)"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"500":{"description":"Internal server error"}},"security":[{"bearer":[]}],"summary":"List spaces","tags":["Spaces"]},"post":{"description":"Creates a new space and returns it.","operationId":"create_space","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpaceCreateRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_SpaceResponse"}}},"description":"Space created"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"security":[{"bearer":[]}],"summary":"Create a space","tags":["Spaces"]}},"/spaces/batch":{"post":{"description":"Creates `amount` spaces of `type` under `parentSpaceId`, named `{prefix}{n}`. Numbering auto-continues from the highest existing `{prefix}{n}` child (including soft-deleted ones), so every name is new and re-running tops up the run rather than colliding. Restricted to the quota-free, parent-required types — FLOOR, SECTION, CUSTOM — so a bulk request can never bypass the LOCATION plan quota.","operationId":"create_spaces_batch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpaceBatchCreateRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpaceBatchCreateResponse"}}},"description":"Spaces created"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"security":[{"bearer":[]}],"summary":"Bulk-create spaces from a prefix + amount","tags":["Spaces"]}},"/spaces/display-name-facets":{"get":{"description":"Returns distinct `displayName` values (with counts) for the tenant's spaces. Optionally scoped by `filter[type]` and `filter[hasParent]`. Efficient alternative to listing all spaces when the caller only needs the set of names (e.g. for a UI filter dropdown).","operationId":"list_display_name_facets","parameters":[{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}},{"description":"Restrict to space type","in":"query","name":"filter[type]","required":false,"schema":{"type":"string"}},{"description":"`true` → only spaces with a parent; `false` → only roots.","in":"query","name":"filter[hasParent]","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisplayNameFacetsResponse"}}},"description":"Distinct displayNames"},"400":{"description":"Bad request"},"403":{"description":"Forbidden"}},"security":[{"bearer":[]}],"summary":"Distinct displayName counts","tags":["Spaces"]}},"/spaces/map":{"get":{"description":"Viewport map data for a tenant's LOCATION spaces. Bounds + geohash clustering keep the payload independent of the tenant's total space count: zoomed out returns aggregated geohash cells (count + centroid); zoomed in (or a small result set) returns individual markers. Optional `search` matches displayName/externalId/city/address.","operationId":"list_spaces_map","parameters":[{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}},{"description":"Map zoom level (1-22), default 3","in":"query","name":"zoom","required":false,"schema":{"format":"int32","type":"integer"}},{"description":"Viewport south bound (with west/north/east)","in":"query","name":"south","required":false,"schema":{"format":"double","type":"number"}},{"description":"Viewport west bound","in":"query","name":"west","required":false,"schema":{"format":"double","type":"number"}},{"description":"Viewport north bound","in":"query","name":"north","required":false,"schema":{"format":"double","type":"number"}},{"description":"Viewport east bound","in":"query","name":"east","required":false,"schema":{"format":"double","type":"number"}},{"description":"Free-text search, min 3 chars","in":"query","name":"search","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpaceMapResponse"}}},"description":"Map markers and clusters"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"security":[{"bearer":[]}],"summary":"Map markers and clusters for spaces","tags":["Spaces"]}},"/spaces/{id}":{"delete":{"description":"Deletes a space by its ID.","operationId":"delete_space","parameters":[{"description":"Space ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Space deleted"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Space not found"}},"security":[{"bearer":[]}],"summary":"Delete a space","tags":["Spaces"]},"get":{"description":"Returns a single space by its ID.","operationId":"get_space","parameters":[{"description":"Space ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_SpaceResponse"}}},"description":"Space found"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Space not found"}},"security":[{"bearer":[]}],"summary":"Get a space","tags":["Spaces"]},"put":{"description":"Updates an existing space and returns the updated version.","operationId":"update_space","parameters":[{"description":"Space ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpaceUpdateRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_SpaceResponse"}}},"description":"Space updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Space not found"}},"security":[{"bearer":[]}],"summary":"Update a space","tags":["Spaces"]}},"/spaces/{id}/ancestors":{"get":{"description":"Returns the ordered ancestor chain for the space, top-down (nearest LOCATION ancestor first, immediate parent last), excluding the space itself. The walk stops at (and includes) the nearest LOCATION; geo-only chains (COUNTRY/REGION/CITY with no LOCATION) return the full chain to the root. Empty array when the space is a LOCATION or has no parent. Ancestors outside the caller's space access are omitted, so a space-restricted user's chain starts at their topmost granted space. Standard collection envelope; the chain is bounded by the space tree depth cap (10), so the full chain always fits the single fixed page. Supports optional response field selection via `fields=` to reduce payload size.","operationId":"get_space_ancestors","parameters":[{"description":"Space ID","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Comma-separated allowlist of response fields to include per ancestor. `id` is always included. Unknown field names return HTTP 400. Example: `fields=id,displayName,type,parentSpaceId,city,country`","in":"query","name":"fields","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_SpaceResponse"}}},"description":"Ancestor spaces, top-down"},"400":{"description":"Unknown field name in `fields`"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Space not found"}},"security":[{"bearer":[]}],"summary":"Get ancestor spaces","tags":["Spaces"]}},"/spaces/{id}/effective-timezone":{"get":{"description":"Walks the space parent chain to find the nearest TZ-capable ancestor (LOCATION/COUNTRY/REGION/CITY/FLOOR) whose `timezone` is not the default 'UTC'. Falls back to the tenant's timezone, then to literal UTC.","operationId":"get_effective_timezone","parameters":[{"description":"Space ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EffectiveTimezoneResponse"}}},"description":"Resolved timezone"},"403":{"description":"Forbidden"},"404":{"description":"Space not found"}},"security":[{"bearer":[]}],"summary":"Resolve effective timezone","tags":["Spaces"]}},"/spaces/{spaceId}/analytics/query":{"post":{"description":"Unified analytics query for a space. Tenant resolved automatically. Specify metrics explicitly or set schema=true for dashboard-driven queries.","operationId":"post_space_analytics_query","parameters":[{"description":"Space ID","in":"path","name":"spaceId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"dateFrom":"2026-03-01","dateTo":"2026-03-31","granularity":"day","metrics":["events","sessions"]},"schema":{"$ref":"#/components/schemas/AnalyticsQueryRequest"}}},"description":"Analytics query configuration","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsQueryResponse"}}},"description":"Analytics data for requested metrics"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Invalid query (e.g., both metrics and schema specified)"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Space not found"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Admin API unavailable"}},"security":[{"bearer":[]}],"summary":"Query space analytics","tags":["Analytics"]}},"/spaces/{spaceId}/analytics/rank":{"post":{"description":"Returns the ordered row-key list for a DataMatrix card scoped to a space. Tenant is resolved from the space.","operationId":"post_space_analytics_rank","parameters":[{"description":"Space ID","in":"path","name":"spaceId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsRankRequest"}}},"description":"Rank request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsRankResponse"}}},"description":"Ordered key list"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Invalid rank request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Space, schema, or card not found"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Admin API unavailable"}},"security":[{"bearer":[]}],"summary":"Rank space DataMatrix card","tags":["Analytics"]}},"/spaces/{spaceId}/analytics/schema":{"get":{"description":"Return the space's analytics schema tree. Tenant resolved automatically.","operationId":"get_space_analytics_schema","parameters":[{"description":"Space ID","in":"path","name":"spaceId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Schema tree with computed ids"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Space not found or missing schema"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Admin API unavailable"}},"security":[{"bearer":[]}],"summary":"Fetch space analytics schema","tags":["Analytics"]}},"/tenants":{"get":{"description":"Returns a paginated list of tenants visible to the authenticated user.","operationId":"list_tenants","parameters":[{"description":"Page number (default: 1)","in":"query","name":"page","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Items per page (default: 25, max: 100)","in":"query","name":"pageSize","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Sort field, prefix with - for descending","in":"query","name":"sort","required":false,"schema":{"type":"string"}},{"description":"Server-side text search","in":"query","name":"search","required":false,"schema":{"type":"string"}},{"description":"Filter to children of this parent tenant","in":"query","name":"parentTenantId","required":false,"schema":{"type":"string"}},{"description":"Filter to children of this parent tenant (standard bracket form, alias of `parentTenantId`)","in":"query","name":"filter[parentTenantId]","required":false,"schema":{"type":"string"}},{"description":"Filter by country code","in":"query","name":"country","required":false,"schema":{"type":"string"}},{"description":"Filter by country code (standard bracket form, alias of `country`)","in":"query","name":"filter[country]","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_TenantResponse"}}},"description":"Paginated list of tenants"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"500":{"description":"Internal server error"}},"security":[{"bearer":[]}],"summary":"List tenants","tags":["Tenants"]},"post":{"description":"Creates a new tenant and returns it.","operationId":"create_tenant","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantCreateRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_TenantResponse"}}},"description":"Tenant created"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"security":[{"bearer":[]}],"summary":"Create a tenant","tags":["Tenants"]}},"/tenants/{id}":{"delete":{"description":"Deletes a tenant by its ID.","operationId":"delete_tenant","parameters":[{"description":"Tenant ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Tenant deleted"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Tenant not found"}},"security":[{"bearer":[]}],"summary":"Delete a tenant","tags":["Tenants"]},"get":{"description":"Returns a single tenant by its ID.","operationId":"get_tenant","parameters":[{"description":"Tenant ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_TenantResponse"}}},"description":"Tenant found"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Tenant not found"}},"security":[{"bearer":[]}],"summary":"Get a tenant","tags":["Tenants"]},"put":{"description":"Updates an existing tenant and returns the updated version.","operationId":"update_tenant","parameters":[{"description":"Tenant ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantUpdateRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_TenantResponse"}}},"description":"Tenant updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Tenant not found"}},"security":[{"bearer":[]}],"summary":"Update a tenant","tags":["Tenants"]}},"/tenants/{id}/deletion-blockers":{"get":{"description":"Returns a list of resources that must be removed before this tenant can be deleted. Use this to show pre-flight validation in the UI before attempting deletion.","operationId":"get_deletion_blockers","parameters":[{"description":"Tenant ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeletionBlockersResponse"}}},"description":"Deletion blockers check"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Tenant not found"}},"security":[{"bearer":[]}],"summary":"Check tenant deletion blockers","tags":["Tenants"]}},"/tenants/{id}/subscription":{"get":{"description":"Returns subscription status and plan information for a tenant.","operationId":"get_tenant_subscription","parameters":[{"description":"Tenant ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantSubscriptionResponse"}}},"description":"Subscription info"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Tenant not found"}},"security":[{"bearer":[]}],"summary":"Get tenant subscription","tags":["Tenants"]}},"/tenants/{parentTenantId}/available-child-plans":{"get":{"description":"Returns publicly available, paid, non-trial plan tiers priced in the parent tenant's currency, sorted by ascending monthly price. Private (customer-specific) plans are excluded. Backs the plan selection when creating a managed tenant.","operationId":"get_available_child_plans","parameters":[{"description":"Parent tenant ID","in":"path","name":"parentTenantId","required":true,"schema":{"type":"string"}},{"description":"Plan type: tenant or support","in":"query","name":"type","required":true,"schema":{"$ref":"#/components/schemas/PlanTypeParam"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/PlanResponse"},"type":"array"}}},"description":"Sorted list of available plans"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Caller lacks create permission on the parent tenant"},"404":{"description":"Parent tenant not found"}},"security":[{"bearer":[]}],"summary":"List available child plans","tags":["Tenants"]}},"/tenants/{parentTenantId}/managed-tenants":{"post":{"description":"Creates a child tenant under the parent. The caller must have create permission on the parent tenant. The child inherits the parent's currency and billing entity, is billed via partner invoice, and must be assigned a paid tenant and support plan from the available-child-plans listing.","operationId":"create_managed_tenant","parameters":[{"description":"Parent tenant ID","in":"path","name":"parentTenantId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManagedTenantCreateRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_TenantResponse"}}},"description":"Managed tenant created"},"400":{"description":"Bad request (invalid or disallowed plan, invalid parent)"},"401":{"description":"Unauthorized"},"403":{"description":"Caller lacks create permission on the parent tenant"},"404":{"description":"Parent tenant not found"}},"security":[{"bearer":[]}],"summary":"Create a managed tenant","tags":["Tenants"]}},"/tenants/{tenantId}/analytics/query":{"post":{"description":"Unified analytics query for a tenant. Specify metrics explicitly or set schema=true for dashboard-driven queries. Multiple metrics execute in parallel.","operationId":"post_tenant_analytics_query","parameters":[{"description":"Tenant ID","in":"path","name":"tenantId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"dateFrom":"2026-03-01","dateTo":"2026-03-31","granularity":"day","metrics":["events","sessions","nps"]},"schema":{"$ref":"#/components/schemas/AnalyticsQueryRequest"}}},"description":"Analytics query configuration","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsQueryResponse"}}},"description":"Analytics data for requested metrics"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Invalid query (e.g., both metrics and schema specified)"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Forbidden"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Admin API unavailable"}},"security":[{"bearer":[]}],"summary":"Query tenant analytics","tags":["Analytics"]}},"/tenants/{tenantId}/analytics/rank":{"post":{"description":"Returns the full ordered row-key list for a single DataMatrix card under the requested sort. Subsequent paged `/analytics/query` calls can slice this list via `cardPagination[cardId].rankKeys`, so only the first call pays the cold compute cost. Results are cached for 12 hours per `(cardId, sortField, direction)` tuple.","operationId":"post_tenant_analytics_rank","parameters":[{"description":"Tenant ID","in":"path","name":"tenantId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"cardId":"ce964eefdf55858b","dateFrom":"2026-03-01","dateTo":"2026-03-31","sort":{"direction":"desc","field":"totalTransactions"}},"schema":{"$ref":"#/components/schemas/AnalyticsRankRequest"}}},"description":"Rank request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsRankResponse"}}},"description":"Ordered key list"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Invalid rank request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Schema or card not found"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Admin API unavailable"}},"security":[{"bearer":[]}],"summary":"Rank tenant DataMatrix card","tags":["Analytics"]}},"/tenants/{tenantId}/analytics/schema":{"get":{"description":"Returns the tenant's analytics schema tree (groups + cards) with server-computed `groupId` / `cardId` identifiers and no card data. Intended as the primer for a per-card parallel render: fetch this first, then issue per-card `/analytics/query` requests in parallel. Cached for 12 hours.","operationId":"get_tenant_analytics_schema","parameters":[{"description":"Tenant ID","in":"path","name":"tenantId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Schema tree with computed ids"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Tenant has no analytics schema"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Admin API unavailable"}},"security":[{"bearer":[]}],"summary":"Fetch tenant analytics schema","tags":["Analytics"]}},"/tenants/{tenantId}/raw-events":{"get":{"description":"Fetches raw events from ClickHouse with server-side session joins. Returns enriched event rows with installationId, deviceId, and appId resolved from the session. Date range is required and capped at 31 days.\n\nToday's events are still being written and would return partial data, so `dateTo` is silently clamped to yesterday (UTC) whenever the caller sends today or a future date. Past `dateTo` values pass through unchanged. The effective value is echoed back in `meta.dateTo`.","operationId":"get_tenant_raw_events","parameters":[{"description":"Tenant ID","in":"path","name":"tenantId","required":true,"schema":{"type":"string"}},{"description":"Start date (YYYY-MM-DD)","in":"query","name":"dateFrom","required":true,"schema":{"format":"date","type":"string"}},{"description":"End date (YYYY-MM-DD, max 31 days from dateFrom)","in":"query","name":"dateTo","required":true,"schema":{"format":"date","type":"string"}},{"description":"Filter by installation ID","in":"query","name":"installationId","required":false,"schema":{"type":"string"}},{"description":"Filter by space ID","in":"query","name":"spaceId","required":false,"schema":{"type":"string"}},{"description":"Filter by device ID","in":"query","name":"deviceId","required":false,"schema":{"type":"string"}},{"description":"Filter by event type","in":"query","name":"eventType","required":false,"schema":{"type":"string"}},{"description":"Page number (default 1)","in":"query","name":"page","required":false,"schema":{"format":"int32","minimum":1,"type":"integer"}},{"description":"Rows per page (default 1000, max 10000)","in":"query","name":"pageSize","required":false,"schema":{"format":"int32","maximum":10000,"minimum":1,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventsResponse"}}},"description":"Raw events with session metadata"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Invalid query (e.g., date range exceeds 31 days)"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Forbidden"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"ClickHouse unavailable"}},"security":[{"bearer":[]}],"summary":"Get raw events for a tenant","tags":["Analytics"]}},"/tenants/{tenantId}/realtime-events/count":{"post":{"description":"Returns per-device per-group counts plus a sample event for the chronologically first match. Predicate evaluation and group-by are performed server-side; the caller receives a pre-aggregated result. For status-stream event types (periodic current-status re-sends, currently `MONITOR_SESAME_DEVICE_STATUS`), consecutive events with an unchanged status for the same (device, str1, str2) count once -- only status transitions count; all other event types are raw-counted (TECH-1318).","operationId":"post_realtime_events_count","parameters":[{"description":"Tenant ID","in":"path","name":"tenantId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"deviceIds":["af9fabaf-..."],"durationSeconds":60,"eventType":"checkout","groupBy":["str1","int1"],"predicates":[{"field":"int2","operator":"gt","type":"numeric","value":5}]},"schema":{"$ref":"#/components/schemas/RealtimeEventsCountRequest"}}},"description":"Batch device list + predicate + duration + optional groupBy","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RealtimeEventsCountResponse"}}},"description":"Per-device per-group counts"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Invalid request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Forbidden"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"ClickHouse unavailable"}},"security":[{"bearer":[]}],"summary":"Count events per device with optional groupBy","tags":["Analytics"]}},"/tenants/{tenantId}/realtime-events/state-persistence":{"post":{"description":"Returns one row per requested `deviceId` indicating whether the matching state held continuously for at least `minDurationSeconds`. Predicate evaluation, session resolution, and the persistence rule are performed server-side, so the caller receives a per-device boolean and match summary instead of raw event arrays.","operationId":"post_realtime_events_state_persistence","parameters":[{"description":"Tenant ID","in":"path","name":"tenantId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"bufferSeconds":300,"deviceIds":["af9fabaf-...","1cd1080d-..."],"eventType":"heartbeat","minDurationSeconds":30,"predicates":[{"field":"int1","operator":"eq","type":"numeric","value":1000},{"field":"str1","operator":"eq","type":"string","value":"gate"}]},"schema":{"$ref":"#/components/schemas/StatePersistenceRequest"}}},"description":"Batch device list + predicate + duration","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatePersistenceResponse"}}},"description":"Per-device persistence results"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Invalid request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Forbidden"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"ClickHouse unavailable"}},"security":[{"bearer":[]}],"summary":"Evaluate state persistence for a batch of devices","tags":["Analytics"]}},"/tokens":{"get":{"operationId":"list_tokens","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_TokenResponse"}}},"description":"Tokens"},"401":{"description":"Unauthorized"}},"security":[{"bearer":[]}],"summary":"List personal access tokens","tags":["Tokens"]},"post":{"operationId":"create_token","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenCreateRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_TokenCreateResponse"}}},"description":"Token created (plaintext returned once)"},"400":{"description":"Bad request (e.g. duplicate name)"},"401":{"description":"Unauthorized"}},"security":[{"bearer":[]}],"summary":"Create a personal access token","tags":["Tokens"]}},"/tokens/{id}":{"delete":{"operationId":"delete_token","parameters":[{"description":"Token id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Token revoked"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden (not your token)"},"404":{"description":"Token not found"}},"security":[{"bearer":[]}],"summary":"Revoke a personal access token","tags":["Tokens"]}},"/users":{"get":{"description":"Paginated users, optionally scoped to a tenant (membership) and filtered by email search.","operationId":"list_users","parameters":[{"description":"Scope to users who are members of this tenant","in":"query","name":"tenantId","required":false,"schema":{"type":"string"}},{"description":"1-based page","in":"query","name":"page","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Page size (max 1000)","in":"query","name":"pageSize","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Case-insensitive email search","in":"query","name":"search","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_UserResponse"}}},"description":"Users"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"security":[{"bearer":[]}],"summary":"List users","tags":["Users"]}},"/users/{id}":{"get":{"operationId":"get_user","parameters":[{"description":"User id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_UserResponse"}}},"description":"User"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"User not found"}},"security":[{"bearer":[]}],"summary":"Get a user","tags":["Users"]}},"/web-endpoints":{"get":{"description":"Returns a paginated list of web endpoints for the given tenant. Each endpoint corresponds to a phyhub Web twin. Mirrors the `/2026-03/cloud-instances` query / filter contract.","operationId":"list_web_endpoints","parameters":[{"description":"Page number (default: 1)","in":"query","name":"page","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Items per page (default: 25, max: 100)","in":"query","name":"pageSize","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}},{"description":"Filter by Installation ID","in":"query","name":"installationId","required":false,"schema":{"type":"string"}},{"description":"Filter by Space ID","in":"query","name":"spaceId","required":false,"schema":{"type":"string"}},{"description":"Filter by twin status (e.g. 'Online', 'Offline')","in":"query","name":"status","required":false,"schema":{"type":"string"}},{"description":"Filter by enabled state","in":"query","name":"enabled","required":false,"schema":{"type":"boolean"}},{"description":"Sort field, prefix with - for descending (appName | createdAt | updatedAt)","in":"query","name":"sort","required":false,"schema":{"type":"string"}},{"description":"Server-side text search (min 2 characters)","in":"query","name":"search","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_WebEndpointResponse"}}},"description":"Paginated list of web endpoints"},"400":{"description":"Invalid query params"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"500":{"description":"Internal server error"}},"security":[{"bearer":[]}],"summary":"List web endpoints","tags":["WebEndpoints"]},"post":{"description":"Creates a web endpoint (phyhub Web twin) for a web Installation in the given Space. The response `id` is the endpoint's deviceId (uuid). `urlId` is generated when omitted; a `urlId` collision returns 409. Web-app installations only.","operationId":"create_web_endpoint","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebEndpointCreateRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_WebEndpointResponse"}}},"description":"Web endpoint created"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Installation or space not found"},"409":{"description":"The urlId or displayName is already taken by another web endpoint"}},"security":[{"bearer":[]}],"summary":"Create a web endpoint","tags":["WebEndpoints"]}},"/web-endpoints/{endpointId}":{"delete":{"description":"Soft-deletes the Web twin, removes the endpoint's legacy device row, and\nrevokes its refresh grants and pending claim codes; live sockets expire\nwithin one session TTL.","operationId":"delete_web_endpoint","parameters":[{"description":"Web endpoint ID — the endpoint's deviceId (uuid, opaque string), not a Mongo ObjectId","in":"path","name":"endpointId","required":true,"schema":{"type":"string"}},{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Web endpoint deleted"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Web endpoint not found"}},"security":[{"bearer":[]}],"summary":"Delete a web endpoint","tags":["WebEndpoints"]},"get":{"operationId":"get_web_endpoint","parameters":[{"description":"Web endpoint ID — the endpoint's deviceId (uuid, opaque string), not a Mongo ObjectId","in":"path","name":"endpointId","required":true,"schema":{"type":"string"}},{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_WebEndpointResponse"}}},"description":"Web endpoint"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Web endpoint not found"}},"security":[{"bearer":[]}],"summary":"Get a web endpoint","tags":["WebEndpoints"]},"put":{"description":"Updates a web endpoint's config and returns the updated version. Installation, space, and `urlId` are immutable. Disabling an endpoint revokes its refresh grants immediately; live sockets expire within one session TTL.","operationId":"update_web_endpoint","parameters":[{"description":"Web endpoint ID — the endpoint's deviceId (uuid, opaque string), not a Mongo ObjectId","in":"path","name":"endpointId","required":true,"schema":{"type":"string"}},{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebEndpointUpdateRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_WebEndpointResponse"}}},"description":"Web endpoint updated"},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Web endpoint not found"},"409":{"description":"The displayName is already taken by another web endpoint"}},"security":[{"bearer":[]}],"summary":"Update a web endpoint","tags":["WebEndpoints"]}},"/web-endpoints/{endpointId}/code":{"post":{"description":"Mints a claim code and returns the same working session link a QR scan carries (`{webAppsRoot}/{urlId}/#code={code}`). The endpoint's own `codeMode`, `codeTtlSeconds`, and session limits apply unchanged. The returned link is a live credential for `expiresIn` seconds — show it once, do not store or log it.","operationId":"create_web_endpoint_code","parameters":[{"description":"Web endpoint ID — the endpoint's deviceId (uuid, opaque string), not a Mongo ObjectId","in":"path","name":"endpointId","required":true,"schema":{"type":"string"}},{"description":"Tenant ID","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}}],"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse_WebEndpointCodeResponse"}}},"description":"Claim code minted"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Web endpoint not found (also covers foreign and disabled endpoints — deliberately opaque)"}},"security":[{"bearer":[]}],"summary":"Mint a session link for a web endpoint","tags":["WebEndpoints"]}},"/{id}/descendants":{"get":{"description":"Returns all descendant space IDs for the given space, computed server-side via hierarchy walk. The tenant is resolved automatically from the space.","operationId":"get_space_descendants","parameters":[{"description":"Space ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DescendantsResponse"}}},"description":"Descendant space IDs"},"403":{"description":"Forbidden"},"404":{"description":"Not found"}},"security":[{"bearer":[]}],"summary":"Get descendant space IDs","tags":["Spaces"]}},"/{id}/users":{"get":{"description":"Paginated list of users who can access this space — those whose role in the tenant grants all-spaces access or whose space access explicitly includes this space. Valid for any space type; space-user access is not type-restricted. Supports optional response field selection via `fields=` to reduce payload size.","operationId":"list_space_users","parameters":[{"description":"Space ID","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Tenant the space belongs to","in":"query","name":"tenantId","required":true,"schema":{"type":"string"}},{"description":"1-based page","in":"query","name":"page","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Page size (max 100)","in":"query","name":"pageSize","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Comma-separated allowlist of response fields to include in each item. When omitted, the full user shape is returned. `id` is always preserved. Example: `fields=id,email,firstName,lastName,lastSeenAt,tenants`","in":"query","name":"fields","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResponse_UserResponse"}}},"description":"Users with access to the space"},"400":{"description":"Bad request (e.g. unknown `fields` name)"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Space not found"}},"security":[{"bearer":[]}],"summary":"List a space's users","tags":["Spaces"]}}},"servers":[{"description":"PhyStack API (api.phystack.com)","url":"https://api.phystack.com/2026-03"}],"tags":[{"description":"Tenant management","name":"Tenants"},{"description":"Space management — hierarchical locations, floors, sections","name":"Spaces"},{"description":"Installation management","name":"Installations"},{"description":"App template catalog","name":"Apps"},{"description":"App builds (versions) — schemas and metadata","name":"App Builds"},{"description":"Device management — tenant-aware routing to PhyHub or legacy backend","name":"Devices"},{"description":"Peripheral management — device-attached components whose shape derives from a descriptor","name":"Peripherals"},{"description":"Descriptors — typed contracts (settings/telemetry/actions/events) a peripheral derives from","name":"Descriptors"},{"description":"Cloud-app instance listing — one row per active (installation, space) pair","name":"CloudInstances"},{"description":"Analytics reports — schema-aware dashboard data for tenants, spaces, devices, and installations","name":"Analytics"},{"description":"User management","name":"Users"},{"description":"Role management — tenant-scoped permission sets","name":"Roles"},{"description":"User invitations","name":"Invitations"},{"description":"Content tag management — tenant-scoped classification labels","name":"ContentTags"},{"description":"Tenant-managed custom short URLs","name":"ShortUrls"},{"description":"Invoices — read-only, tenant-scoped, system-generated","name":"Invoices"},{"description":"Personal access tokens — headless/CI auth credentials","name":"Tokens"},{"description":"Authentication — the unified, revocable sessions registry (CLI/API tokens + console sessions)","name":"Auth"},{"description":"Media library — folders + assets, browse/search, presigned upload","name":"Media"},{"description":"Web endpoints — public phone-facing web app entry points (phyhub Web twins) with claim-code session minting; the public session mint is served by phyhub via the regional passthrough (`/regions/{region}/phyhub/api/v1/web-sessions`)","name":"WebEndpoints"}]}