{
  "openapi": "3.1.0",
  "info": {
    "title": "Zelos Cloud API",
    "version": "1.0.0",
    "summary": "The REST API behind Zelos Cloud: organizations, traces, notebooks, layouts, and tags.",
    "description": "The Zelos Cloud API is the same OAuth 2.0 REST surface the Zelos CLI and the Zelos desktop app use. It manages organizations and their members, cloud traces, published notebooks, workspace layouts, and the tag vocabulary that ties them together.\n\n## Authenticating\n\nEvery operation except the health check, the OAuth endpoints, and the public share endpoints needs `Authorization: Bearer <token>`.\n\nA terminal or an autonomous agent should use the device authorization grant (RFC 8628), which needs no browser redirect back to a local port:\n\n1. `POST /auth/device/code` with your `client_id`.\n2. Show `user_code` and send the person to `verification_uri_complete`.\n3. Poll `POST /auth/oauth2/token` with `grant_type=urn:ietf:params:oauth:grant-type:device_code`, no faster than `interval` seconds, until it stops answering `authorization_pending`.\n\nClients can register themselves at the `registration_endpoint` in the discovery document. `zelos login` runs exactly this flow and stores the token in your system keyring.\n\n## Conventions\n\n- Resources are scoped by organization slug. Read the slugs the caller can use from `orgs[].slug` on `GET /auth/me`.\n- Successful responses wrap their payload in `data`. List responses add `page`, and add `filters` when the request carried `?tags=`.\n- Lists page by opaque keyset cursor: pass `page.nextCursor` back as `?cursor=`. A cursor that names no visible row ends the walk with an empty page rather than restarting.\n- Errors return `{ \"error\": \"...\" }` with a message safe to show a user.\n- A resource that does not exist, belongs to another organization, or is another member's personal resource all return the same 404, so ids cannot be probed. A 403 means the resource exists and the caller's role is not enough.\n- Trace and notebook bytes are not proxied through this API. It mints prefix-scoped, time-limited S3 credentials and the client reads or writes object storage directly.\n\n## Versioning and deprecation\n\n`v1` is stable. A breaking change ships under a new version path rather than altering `v1` in place.\n\nBefore an operation is removed it is marked `deprecated: true` in this document, and its responses carry the `Deprecation` and `Sunset` headers (RFC 9745, RFC 8594). Read `Sunset` as the date the operation stops answering.\n\n## Rate limits\n\nRate-limited operations advertise their quota on every response, including a 401, with `RateLimit-Policy` (draft-ietf-httpapi-ratelimit-headers), for example `RateLimit-Policy: \"traces-create\";q=5;w=60` for five calls per sixty seconds. Read it once and pace yourself against it.\n\nExceeding a limit returns `429` with `{ \"error\": \"rate_limited\" }`, a `Retry-After` in seconds, and `RateLimit` carrying the remaining quota (zero) and the seconds until reset. Successful responses deliberately carry no live `RateLimit` field: the limiter reports only whether a call was allowed, so a remaining count would be invented.\n\nCurrent quotas, all per user per minute: `traces-create` 5, `traces-delete` 10, `traces-open` 30, `traces-finalize` 60. Finalize is higher because it is resumable and one upload legitimately spends many calls.\n\n## What is not here\n\nThis document covers the stable, token-authenticated surface. Staff-only administration, Stripe billing checkout, the streaming AI chat endpoint, and browser-session-only routes are deliberately excluded and are not part of the compatibility commitment this document makes.\n\n## About this copy\n\nThis is the offline fallback served by zeloscloud.io when the live document cannot be fetched. It is a strict subset: it lists only operations a deployed console answers today. The authoritative document ships with the API at `https://console2.zeloscloud.io/openapi.json`, and zeloscloud.io/openapi.json normally proxies straight to it.",
    "termsOfService": "https://zeloscloud.io/privacy",
    "contact": {
      "name": "Zelos support",
      "email": "support@zeloscloud.io",
      "url": "https://zeloscloud.io/contact"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://zeloscloud.io/privacy"
    }
  },
  "externalDocs": {
    "description": "Zelos documentation",
    "url": "https://docs.zeloscloud.io"
  },
  "servers": [
    {
      "url": "https://console2.zeloscloud.io/api",
      "description": "Zelos Cloud production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    },
    {
      "oauth2": ["openid", "profile", "email"]
    }
  ],
  "tags": [
    {
      "name": "Service",
      "description": "Liveness and version."
    },
    {
      "name": "Authentication",
      "description": "OAuth 2.0 discovery, the device flow, and the caller's identity."
    },
    {
      "name": "Organizations",
      "description": "Creating organizations. Read the ones you belong to from `getCurrentUser`."
    },
    {
      "name": "Layouts",
      "description": "Workspace layouts and their version history."
    },
    {
      "name": "Members",
      "description": "Organization membership and invitations."
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "tags": ["Service"],
        "operationId": "getHealth",
        "summary": "Check API health",
        "description": "Unauthenticated liveness probe. Returns `healthy` with the running console version when the database is reachable, and 503 with `unhealthy` when it is not. Safe to poll; it performs one trivial query.",
        "responses": {
          "200": {
            "description": "The API is serving and the database is reachable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthStatus"
                }
              }
            }
          },
          "503": {
            "description": "The API is serving but the database is unreachable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthStatus"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/auth/.well-known/openid-configuration": {
      "get": {
        "tags": ["Authentication"],
        "operationId": "getOpenIdConfiguration",
        "summary": "Fetch OpenID Connect discovery metadata",
        "description": "The OAuth 2.0 / OpenID Connect issuer metadata document (RFC 8414, OpenID Connect Discovery 1.0). Read the authorization, token, registration, introspection, and revocation endpoints from here rather than hardcoding them. `grant_types_supported` includes `urn:ietf:params:oauth:grant-type:device_code`, which is the grant a terminal or an agent should use.",
        "responses": {
          "200": {
            "description": "Issuer metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpenIdConfiguration"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/auth/device/code": {
      "post": {
        "tags": ["Authentication"],
        "operationId": "createDeviceAuthorization",
        "summary": "Start an OAuth 2.0 device authorization flow",
        "description": "Begin the device authorization grant (RFC 8628 section 3.1). Show `user_code` to the person and send them to `verification_uri` (or open `verification_uri_complete` directly), then poll `POST /auth/oauth2/token` no faster than `interval` seconds. Accepts `application/x-www-form-urlencoded` per the RFC, and `application/json` for convenience. No client authentication is required.",
        "responses": {
          "200": {
            "description": "Device and user codes, with the polling interval.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceAuthorizationResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, for example a missing `client_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported `Content-Type`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/DeviceAuthorizationRequest"
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeviceAuthorizationRequest"
              }
            }
          }
        },
        "security": []
      }
    },
    "/auth/oauth2/token": {
      "post": {
        "tags": ["Authentication"],
        "operationId": "createAccessToken",
        "summary": "Exchange a grant for an access token",
        "description": "The OAuth 2.0 token endpoint. For the device flow, poll with `grant_type=urn:ietf:params:oauth:grant-type:device_code` and the `device_code` from `createDeviceAuthorization`; keep polling while the response is `authorization_pending`, and back off on `slow_down`. Other grants listed in `grant_types_supported` are handled by the same endpoint. Send the resulting `access_token` as `Authorization: Bearer <token>` on every other operation.",
        "responses": {
          "200": {
            "description": "An access token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                }
              }
            }
          },
          "400": {
            "description": "The grant is pending, denied, expired, or malformed. Inspect `error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/TokenRequest"
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TokenRequest"
              }
            }
          }
        },
        "security": []
      }
    },
    "/auth/me": {
      "get": {
        "tags": ["Authentication"],
        "operationId": "getCurrentUser",
        "summary": "Get the authenticated user and their organizations",
        "description": "The identity behind the bearer token, plus every organization the user belongs to. `orgs` is ordered with the user's active organization first, then by a deterministic rule, so `orgs[0]` is a stable default for a client that needs to pick one. `teams` is a byte-identical alias kept for older desktop builds; new clients should read `orgs`.",
        "responses": {
          "200": {
            "description": "The current user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CurrentUser"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/orgs": {
      "post": {
        "tags": ["Organizations"],
        "operationId": "createOrganization",
        "summary": "Create an organization",
        "description": "Create an organization owned by the caller. A slug is derived from `name` when `slug` is omitted; reserved slugs are rejected. This operation requires a browser session cookie, because the creator must be made owner and the organization made active on the session. Bearer-token clients should create the organization in the console first.",
        "responses": {
          "201": {
            "description": "The organization was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateOrganizationResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateOrganizationRequest"
              }
            }
          }
        }
      }
    },
    "/v1/orgs/{slug}/layouts": {
      "get": {
        "tags": ["Layouts"],
        "operationId": "listLayouts",
        "summary": "List layouts visible to the caller",
        "description": "The organization's shared layouts plus the caller's own personal ones, ordered by name. Unlike the other list endpoints, omitting `limit` returns the whole visible set rather than one page; pass `limit` to page explicitly.",
        "responses": {
          "200": {
            "description": "Layouts visible to the caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LayoutListResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Organization slug. Read it from `orgs[].slug` on `GET /auth/me`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$",
              "examples": ["acme-robotics"]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size. Defaults to 50 and is capped at 100. A non-numeric or non-positive value falls back to the default rather than erroring.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque keyset cursor from the previous page's `page.nextCursor`. A cursor that names no row this caller can see ends the walk with an empty page rather than restarting at page one.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Case-insensitive substring filter. LIKE metacharacters in the needle are matched literally.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tags",
            "in": "query",
            "required": false,
            "description": "Tag name to filter by. Repeat the parameter to require ALL the named tags.",
            "explode": true,
            "style": "form",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        ]
      },
      "post": {
        "tags": ["Layouts"],
        "operationId": "createLayout",
        "summary": "Create a layout",
        "description": "Create a workspace layout. `data` is the opaque layout document the desktop app writes; the API stores and versions it without interpreting it. Set `isPersonal` to keep the layout private to the caller instead of sharing it with the organization.",
        "responses": {
          "201": {
            "description": "The layout was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LayoutResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Organization slug. Read it from `orgs[].slug` on `GET /auth/me`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$",
              "examples": ["acme-robotics"]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateLayoutRequest"
              }
            }
          }
        }
      }
    },
    "/v1/orgs/{slug}/layouts/{layoutId}": {
      "get": {
        "tags": ["Layouts"],
        "operationId": "getLayout",
        "summary": "Get a layout",
        "description": "One layout with its full `data` document and its tags. A layout that does not exist, belongs to another organization, or is another member's personal layout all return the same 404, so ids cannot be probed.",
        "responses": {
          "200": {
            "description": "The layout.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LayoutResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Organization slug. Read it from `orgs[].slug` on `GET /auth/me`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$",
              "examples": ["acme-robotics"]
            }
          },
          {
            "name": "layoutId",
            "in": "path",
            "required": true,
            "description": "The layout's id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ]
      },
      "put": {
        "tags": ["Layouts"],
        "operationId": "updateLayout",
        "summary": "Update a layout",
        "description": "Update a layout's name, data, or personal flag. Omitted fields keep their current value. A successful change to `data` appends a new version to the layout's history.",
        "responses": {
          "200": {
            "description": "The updated layout.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LayoutResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Organization slug. Read it from `orgs[].slug` on `GET /auth/me`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$",
              "examples": ["acme-robotics"]
            }
          },
          {
            "name": "layoutId",
            "in": "path",
            "required": true,
            "description": "The layout's id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateLayoutRequest"
              }
            }
          }
        }
      },
      "delete": {
        "tags": ["Layouts"],
        "operationId": "deleteLayout",
        "summary": "Delete a layout",
        "description": "Delete a layout and its version history. Allowed for the layout's owner and for organization admins and owners.",
        "responses": {
          "200": {
            "description": "The layout was deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Organization slug. Read it from `orgs[].slug` on `GET /auth/me`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$",
              "examples": ["acme-robotics"]
            }
          },
          {
            "name": "layoutId",
            "in": "path",
            "required": true,
            "description": "The layout's id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ]
      }
    },
    "/v1/orgs/{slug}/layouts/{layoutId}/transfer": {
      "post": {
        "tags": ["Layouts"],
        "operationId": "transferLayout",
        "summary": "Transfer layout ownership",
        "description": "Hand ownership of a layout to another member of the same organization. The new owner gains edit rights; the previous owner keeps whatever access their role gives them.",
        "responses": {
          "200": {
            "description": "The transferred layout.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LayoutResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Organization slug. Read it from `orgs[].slug` on `GET /auth/me`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$",
              "examples": ["acme-robotics"]
            }
          },
          {
            "name": "layoutId",
            "in": "path",
            "required": true,
            "description": "The layout's id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TransferLayoutRequest"
              }
            }
          }
        }
      }
    },
    "/v1/orgs/{slug}/layouts/{layoutId}/versions": {
      "get": {
        "tags": ["Layouts"],
        "operationId": "listLayoutVersions",
        "summary": "List a layout's version history",
        "description": "Every stored version of a layout, newest first. Versions carry provenance and an optional label. The `data` snapshot is omitted here; fetch one version with `getLayoutVersion` to get it.",
        "responses": {
          "200": {
            "description": "The layout's versions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LayoutVersionListResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Organization slug. Read it from `orgs[].slug` on `GET /auth/me`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$",
              "examples": ["acme-robotics"]
            }
          },
          {
            "name": "layoutId",
            "in": "path",
            "required": true,
            "description": "The layout's id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ]
      }
    },
    "/v1/orgs/{slug}/layouts/{layoutId}/versions/{version}": {
      "get": {
        "tags": ["Layouts"],
        "operationId": "getLayoutVersion",
        "summary": "Get one layout version",
        "description": "One version of a layout, including the full `data` snapshot as it was at that point.",
        "responses": {
          "200": {
            "description": "The layout version.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LayoutVersionResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Organization slug. Read it from `orgs[].slug` on `GET /auth/me`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$",
              "examples": ["acme-robotics"]
            }
          },
          {
            "name": "layoutId",
            "in": "path",
            "required": true,
            "description": "The layout's id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "version",
            "in": "path",
            "required": true,
            "description": "The version number, as listed by `listLayoutVersions`.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ]
      },
      "patch": {
        "tags": ["Layouts"],
        "operationId": "updateLayoutVersionLabel",
        "summary": "Label or unlabel a layout version",
        "description": "Set or clear a version's label. Labeling is metadata and does not create a new version. Labeled versions are exempt from retention pruning, which is how a client pins a known-good layout.",
        "responses": {
          "200": {
            "description": "The updated version.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LayoutVersionResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Organization slug. Read it from `orgs[].slug` on `GET /auth/me`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$",
              "examples": ["acme-robotics"]
            }
          },
          {
            "name": "layoutId",
            "in": "path",
            "required": true,
            "description": "The layout's id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "version",
            "in": "path",
            "required": true,
            "description": "The version number.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LayoutVersionLabelRequest"
              }
            }
          }
        }
      }
    },
    "/v1/orgs/{slug}/layouts/{layoutId}/versions/{version}/restore": {
      "post": {
        "tags": ["Layouts"],
        "operationId": "restoreLayoutVersion",
        "summary": "Restore a layout to a previous version",
        "description": "Append the named version's snapshot as a NEW version carrying `restoredFromVersion` provenance, the way a document editor restores history without rewriting it. Restoring the current state is a no-op.",
        "responses": {
          "200": {
            "description": "The layout after restoring.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LayoutResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Organization slug. Read it from `orgs[].slug` on `GET /auth/me`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$",
              "examples": ["acme-robotics"]
            }
          },
          {
            "name": "layoutId",
            "in": "path",
            "required": true,
            "description": "The layout's id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "version",
            "in": "path",
            "required": true,
            "description": "The version number to restore.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ]
      }
    },
    "/v1/orgs/{slug}/members": {
      "get": {
        "tags": ["Members"],
        "operationId": "listOrganizationMembers",
        "summary": "List organization members",
        "description": "Every member of the organization with their role and join date. Membership is the single source of truth for access to the organization's traces, notebooks, and layouts.",
        "responses": {
          "200": {
            "description": "The organization's members.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemberListResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Organization slug. Read it from `orgs[].slug` on `GET /auth/me`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$",
              "examples": ["acme-robotics"]
            }
          }
        ]
      },
      "post": {
        "tags": ["Members"],
        "operationId": "addOrganizationMember",
        "summary": "Add an existing user as a member",
        "description": "Add a user who already has a Zelos account, by email. Owners and admins may call this, and only owners may grant the `admin` role. To bring in somebody without an account, create an invitation instead.",
        "responses": {
          "201": {
            "description": "The member was added.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemberResponse"
                }
              }
            }
          },
          "409": {
            "description": "That user is already a member.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Organization slug. Read it from `orgs[].slug` on `GET /auth/me`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$",
              "examples": ["acme-robotics"]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddMemberRequest"
              }
            }
          }
        }
      }
    },
    "/v1/orgs/{slug}/members/{memberId}": {
      "patch": {
        "tags": ["Members"],
        "operationId": "updateOrganizationMemberRole",
        "summary": "Change a member's role",
        "description": "Set a member's role to `member`, `admin`, or `owner`. Only owners may promote to or demote from `owner`.",
        "responses": {
          "200": {
            "description": "The updated member.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemberResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Organization slug. Read it from `orgs[].slug` on `GET /auth/me`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$",
              "examples": ["acme-robotics"]
            }
          },
          {
            "name": "memberId",
            "in": "path",
            "required": true,
            "description": "The membership row's id, from `listOrganizationMembers`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateMemberRoleRequest"
              }
            }
          }
        }
      },
      "delete": {
        "tags": ["Members"],
        "operationId": "removeOrganizationMember",
        "summary": "Remove a member",
        "description": "Remove a member from the organization. Their personal layouts and notebooks stay with them; traces they uploaded stay with the organization and list with a null author.",
        "responses": {
          "200": {
            "description": "The member was removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Organization slug. Read it from `orgs[].slug` on `GET /auth/me`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$",
              "examples": ["acme-robotics"]
            }
          },
          {
            "name": "memberId",
            "in": "path",
            "required": true,
            "description": "The membership row's id.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/orgs/{slug}/invitations": {
      "get": {
        "tags": ["Members"],
        "operationId": "listOrganizationInvitations",
        "summary": "List pending invitations",
        "description": "Invitations that have been created and not yet accepted, cancelled, or expired. Owners and admins only.",
        "responses": {
          "200": {
            "description": "Pending invitations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvitationListResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Organization slug. Read it from `orgs[].slug` on `GET /auth/me`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$",
              "examples": ["acme-robotics"]
            }
          }
        ]
      },
      "post": {
        "tags": ["Members"],
        "operationId": "createOrganizationInvitation",
        "summary": "Invite someone by email",
        "description": "Create a pending invitation for an email address. No email is sent: the invitation is surfaced in the product on the invitee's next sign-in, which is why the invitee must sign up first if they have no account.",
        "responses": {
          "201": {
            "description": "The invitation was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvitationResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Organization slug. Read it from `orgs[].slug` on `GET /auth/me`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$",
              "examples": ["acme-robotics"]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateInvitationRequest"
              }
            }
          }
        }
      }
    },
    "/v1/orgs/{slug}/invitations/{invitationId}": {
      "delete": {
        "tags": ["Members"],
        "operationId": "cancelOrganizationInvitation",
        "summary": "Cancel a pending invitation",
        "description": "Cancel an invitation before it is accepted. The invitation must belong to this organization; one from another organization returns 404 rather than a permission error.",
        "responses": {
          "200": {
            "description": "The invitation was cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Organization slug. Read it from `orgs[].slug` on `GET /auth/me`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$",
              "examples": ["acme-robotics"]
            }
          },
          {
            "name": "invitationId",
            "in": "path",
            "required": true,
            "description": "The invitation's id.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An opaque access token from the OAuth 2.0 token endpoint. Send it as `Authorization: Bearer <token>`."
      },
      "oauth2": {
        "type": "oauth2",
        "description": "OAuth 2.0. A CLI, a CI job, or an agent should use the device authorization grant (RFC 8628): `POST /auth/device/code`, then poll `POST /auth/oauth2/token` with `grant_type=urn:ietf:params:oauth:grant-type:device_code`. Both are documented as operations under the Authentication tag, and the endpoint URLs are repeated in `x-deviceAuthorizationUrl` below. OpenAPI 3.1 has no flow object for the device grant, so `authorizationCode` and `clientCredentials` are the two declared here.",
        "x-deviceAuthorizationUrl": "https://console2.zeloscloud.io/api/auth/device/code",
        "x-deviceTokenUrl": "https://console2.zeloscloud.io/api/auth/oauth2/token",
        "x-deviceGrantType": "urn:ietf:params:oauth:grant-type:device_code",
        "flows": {
          "clientCredentials": {
            "tokenUrl": "https://console2.zeloscloud.io/api/auth/oauth2/token",
            "refreshUrl": "https://console2.zeloscloud.io/api/auth/oauth2/token",
            "scopes": {
              "openid": "Authenticate the user.",
              "profile": "Read the user's name and picture.",
              "email": "Read the user's email address.",
              "offline_access": "Receive a refresh token."
            }
          },
          "authorizationCode": {
            "authorizationUrl": "https://console2.zeloscloud.io/api/auth/oauth2/authorize",
            "tokenUrl": "https://console2.zeloscloud.io/api/auth/oauth2/token",
            "refreshUrl": "https://console2.zeloscloud.io/api/auth/oauth2/token",
            "scopes": {
              "openid": "Authenticate the user.",
              "profile": "Read the user's name and picture.",
              "email": "Read the user's email address.",
              "offline_access": "Receive a refresh token."
            }
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request body or a parameter is invalid. `error` names what.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "The bearer token is missing, expired, or invalid.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The caller is authenticated but their role does not allow this.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "No such resource, or the caller may not see it. The two are deliberately indistinguishable.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "A human-readable message. Clients may display it verbatim."
          }
        },
        "required": ["error"],
        "description": "The uniform error envelope.",
        "additionalProperties": false
      },
      "OAuthError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "An OAuth 2.0 error code, for example `authorization_pending`, `slow_down`, `access_denied`, `expired_token`, or `invalid_request`."
          },
          "error_description": {
            "type": "string",
            "description": "A human-readable explanation."
          }
        },
        "required": ["error"],
        "description": "An OAuth 2.0 error response (RFC 6749 section 5.2).",
        "additionalProperties": false
      },
      "SuccessResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true."
          }
        },
        "required": ["success"],
        "description": "A bare success acknowledgement.",
        "additionalProperties": false
      },
      "HealthStatus": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": ["healthy", "unhealthy"],
            "description": "Overall health."
          },
          "version": {
            "type": "string",
            "description": "Version of the running console."
          },
          "gitSha": {
            "type": "string",
            "description": "Commit the running console was built from, when known."
          },
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "description": "When the check ran."
          }
        },
        "required": ["status", "version", "timestamp"],
        "description": "The health probe's answer.",
        "additionalProperties": false
      },
      "OpenIdConfiguration": {
        "type": "object",
        "properties": {
          "issuer": {
            "type": "string",
            "description": "The issuer identifier."
          },
          "authorization_endpoint": {
            "type": "string",
            "description": "Authorization endpoint."
          },
          "token_endpoint": {
            "type": "string",
            "description": "Token endpoint."
          },
          "registration_endpoint": {
            "type": "string",
            "description": "Dynamic client registration endpoint (RFC 7591)."
          },
          "introspection_endpoint": {
            "type": "string",
            "description": "Token introspection endpoint."
          },
          "revocation_endpoint": {
            "type": "string",
            "description": "Token revocation endpoint."
          },
          "jwks_uri": {
            "type": "string",
            "description": "JSON Web Key Set."
          },
          "scopes_supported": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Scopes the issuer accepts."
          },
          "grant_types_supported": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Grant types the issuer accepts, including the device code grant."
          },
          "response_types_supported": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Supported response types."
          },
          "code_challenge_methods_supported": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "PKCE methods the issuer accepts."
          }
        },
        "required": ["issuer", "authorization_endpoint", "token_endpoint"],
        "description": "OAuth 2.0 authorization server metadata (RFC 8414).",
        "additionalProperties": true
      },
      "DeviceAuthorizationRequest": {
        "type": "object",
        "properties": {
          "client_id": {
            "type": "string",
            "description": "Your client identifier. Register one at the issuer's `registration_endpoint`."
          },
          "scope": {
            "type": "string",
            "description": "Space-separated scopes to request."
          }
        },
        "required": ["client_id"],
        "description": "A device authorization request (RFC 8628 section 3.1).",
        "additionalProperties": false
      },
      "DeviceAuthorizationResponse": {
        "type": "object",
        "properties": {
          "device_code": {
            "type": "string",
            "description": "Secret code your client polls the token endpoint with."
          },
          "user_code": {
            "type": "string",
            "description": "Short code to show the person."
          },
          "verification_uri": {
            "type": "string",
            "description": "Where the person enters the user code."
          },
          "verification_uri_complete": {
            "type": "string",
            "description": "The verification URI with the user code already filled in."
          },
          "expires_in": {
            "type": "integer",
            "description": "Seconds until the device code expires."
          },
          "interval": {
            "type": "integer",
            "description": "Minimum seconds between token-endpoint polls."
          }
        },
        "required": ["device_code", "user_code", "verification_uri", "expires_in", "interval"],
        "description": "Device and user codes (RFC 8628 section 3.2).",
        "additionalProperties": false
      },
      "TokenRequest": {
        "type": "object",
        "properties": {
          "grant_type": {
            "type": "string",
            "description": "The grant. Use `urn:ietf:params:oauth:grant-type:device_code` for the device flow."
          },
          "device_code": {
            "type": "string",
            "description": "The `device_code` from `createDeviceAuthorization`. Required for the device grant."
          },
          "client_id": {
            "type": "string",
            "description": "Your client identifier."
          }
        },
        "required": ["grant_type"],
        "description": "A token request (RFC 6749 section 4, RFC 8628 section 3.4).",
        "additionalProperties": false
      },
      "TokenResponse": {
        "type": "object",
        "properties": {
          "access_token": {
            "type": "string",
            "description": "Send as `Authorization: Bearer <token>`."
          },
          "token_type": {
            "type": "string",
            "description": "Always `Bearer`."
          },
          "expires_in": {
            "type": "integer",
            "description": "Seconds until the access token expires."
          },
          "scope": {
            "type": "string",
            "description": "Scopes actually granted."
          },
          "refresh_token": {
            "type": "string",
            "description": "Present when `offline_access` was granted."
          }
        },
        "required": ["access_token", "token_type"],
        "description": "A successful token response.",
        "additionalProperties": false
      },
      "OrganizationMembership": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The organization's id."
          },
          "name": {
            "type": "string",
            "description": "The organization's display name."
          },
          "slug": {
            "type": "string",
            "description": "The organization's slug. This is the `slug` path parameter everywhere else."
          },
          "role": {
            "type": "string",
            "enum": ["owner", "admin", "member"],
            "description": "The caller's role here."
          },
          "subscriptionTier": {
            "type": "string",
            "description": "The organization's billing tier."
          },
          "memberCount": {
            "type": "integer",
            "description": "How many members the organization has."
          },
          "nextInvoiceAt": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "End of the current billing period, when there is a subscription."
          },
          "cancelAtPeriodEnd": {
            "type": "boolean",
            "description": "True when the subscription will not renew."
          }
        },
        "required": ["id", "name", "slug", "role", "subscriptionTier", "memberCount"],
        "description": "One organization the authenticated user belongs to.",
        "additionalProperties": false
      },
      "CurrentUser": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The user's id."
          },
          "email": {
            "type": "string",
            "description": "The user's email."
          },
          "name": {
            "type": ["string", "null"],
            "description": "Display name."
          },
          "image": {
            "type": ["string", "null"],
            "description": "Profile picture URL from the identity provider."
          },
          "subscriptionTier": {
            "type": "string",
            "description": "Tier of the user's default organization."
          },
          "orgs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrganizationMembership"
            },
            "description": "Organizations the user belongs to, active one first."
          },
          "teams": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrganizationMembership"
            },
            "description": "Alias of `orgs`, kept for older desktop builds. Prefer `orgs`."
          }
        },
        "required": ["id", "email", "orgs", "teams"],
        "description": "The authenticated user and their organizations.",
        "additionalProperties": false
      },
      "CreateOrganizationRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Display name for the organization."
          },
          "slug": {
            "type": "string",
            "description": "Preferred slug. Derived from `name` when omitted. Reserved slugs are rejected."
          }
        },
        "required": ["name"],
        "description": "Create an organization.",
        "additionalProperties": false
      },
      "CreateOrganizationResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "slug": {
                "type": "string",
                "description": "The slug the organization was created with."
              }
            },
            "required": ["slug"],
            "description": "The created organization.",
            "additionalProperties": false
          }
        },
        "required": ["data"],
        "description": "The new organization's slug.",
        "additionalProperties": false
      },
      "TagRef": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The tag's id."
          },
          "name": {
            "type": "string",
            "description": "Display name. Unique per organization, compared case-insensitively."
          },
          "color": {
            "type": "string",
            "description": "Hex color used to render the tag chip.",
            "examples": ["#7C3AED"]
          }
        },
        "required": ["id", "name", "color"],
        "description": "A tag as referenced from a tagged resource.",
        "additionalProperties": false
      },
      "ListPage": {
        "type": "object",
        "properties": {
          "nextCursor": {
            "type": ["string", "null"],
            "description": "Pass back as `?cursor=` for the next page. Null on the last page."
          },
          "matching": {
            "type": "integer",
            "description": "How many rows match the filters, ignoring paging. This is the M in \"showing N of M\"."
          }
        },
        "required": ["nextCursor", "matching"],
        "description": "The envelope describing a paginated walk.",
        "additionalProperties": false
      },
      "ListFilters": {
        "type": "object",
        "properties": {
          "tags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TagRef"
            },
            "description": "The tags the `?tags=` names actually resolved to."
          },
          "unmatched": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Names from `?tags=` that matched no tag, as typed."
          }
        },
        "required": ["tags", "unmatched"],
        "description": "Present exactly when the request carried `?tags=`.",
        "additionalProperties": false
      },
      "Layout": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The layout's id."
          },
          "name": {
            "type": "string",
            "description": "Display name. At most 100 characters."
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "The layout document. Opaque to the API: stored and versioned, never interpreted."
          },
          "isPersonal": {
            "type": "boolean",
            "description": "True when the layout is private to its owner."
          },
          "teamId": {
            "type": "string",
            "description": "Id of the organization the layout belongs to."
          },
          "teamSlug": {
            "type": "string",
            "description": "Slug of that organization."
          },
          "userId": {
            "type": ["string", "null"],
            "description": "Owner's user id."
          },
          "email": {
            "type": "string",
            "description": "Owner's email."
          },
          "updatedByEmail": {
            "type": "string",
            "description": "Email of whoever last changed the layout."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the layout was created."
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the layout last changed."
          },
          "tags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TagRef"
            },
            "description": "Tags applied to the layout."
          },
          "canEdit": {
            "type": "boolean",
            "description": "Whether the caller may change this layout."
          }
        },
        "required": ["id", "name", "data", "isPersonal", "teamId", "createdAt", "updatedAt"],
        "description": "A workspace layout.",
        "additionalProperties": false
      },
      "LayoutResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Layout"
          }
        },
        "required": ["data"],
        "description": "One layout.",
        "additionalProperties": false
      },
      "LayoutListResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Layout"
            },
            "description": "Layouts visible to the caller, by name."
          },
          "page": {
            "$ref": "#/components/schemas/ListPage"
          },
          "filters": {
            "$ref": "#/components/schemas/ListFilters"
          }
        },
        "required": ["data"],
        "description": "Layouts visible to the caller. `page` is ABSENT from a console that predates keyset paging on this list; read that as \"this single response is the complete list\", which is what such a server means. A present envelope drives the walk.",
        "additionalProperties": false
      },
      "CreateLayoutRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Display name. At most 100 characters."
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "The layout document."
          },
          "isPersonal": {
            "type": "boolean",
            "description": "Keep the layout private to the caller. Defaults to false."
          }
        },
        "required": ["name", "data"],
        "description": "Create a layout.",
        "additionalProperties": false
      },
      "UpdateLayoutRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "New display name."
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "New layout document."
          },
          "isPersonal": {
            "type": "boolean",
            "description": "New personal flag."
          }
        },
        "description": "Change a layout. Omitted fields keep their current value.",
        "additionalProperties": false
      },
      "TransferLayoutRequest": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "description": "User id of the member to transfer ownership to. Must be a member of the same organization."
          }
        },
        "required": ["userId"],
        "description": "Transfer layout ownership.",
        "additionalProperties": false
      },
      "LayoutVersion": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The version row's id."
          },
          "version": {
            "type": "integer",
            "description": "Monotonic version number within the layout."
          },
          "label": {
            "type": ["string", "null"],
            "description": "Optional label. A labeled version is exempt from retention pruning."
          },
          "restoredFromVersion": {
            "type": ["integer", "null"],
            "description": "Set when this version was produced by restoring an earlier one."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the version was written."
          },
          "createdByEmail": {
            "type": ["string", "null"],
            "description": "Email of whoever produced the version."
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "The layout snapshot. Present on `getLayoutVersion`, omitted from list responses."
          }
        },
        "required": ["id", "version", "createdAt"],
        "description": "One point in a layout's history.",
        "additionalProperties": false
      },
      "LayoutVersionResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/LayoutVersion"
          }
        },
        "required": ["data"],
        "description": "One layout version.",
        "additionalProperties": false
      },
      "LayoutVersionListResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LayoutVersion"
            },
            "description": "Versions, newest first."
          }
        },
        "required": ["data"],
        "description": "A layout's version history.",
        "additionalProperties": false
      },
      "LayoutVersionLabelRequest": {
        "type": "object",
        "properties": {
          "label": {
            "type": ["string", "null"],
            "description": "The label to set, or null to clear it."
          }
        },
        "required": ["label"],
        "description": "Set or clear a version label.",
        "additionalProperties": false
      },
      "Member": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The membership row's id. This is what member operations take, not the user id."
          },
          "userId": {
            "type": "string",
            "description": "The member's user id."
          },
          "email": {
            "type": "string",
            "description": "The member's email."
          },
          "name": {
            "type": ["string", "null"],
            "description": "The member's display name."
          },
          "image": {
            "type": ["string", "null"],
            "description": "Profile picture URL from the member's identity provider."
          },
          "role": {
            "type": "string",
            "enum": ["owner", "admin", "member"],
            "description": "Role in this organization."
          },
          "joinedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the membership was created."
          }
        },
        "required": ["id", "email", "role", "joinedAt"],
        "description": "One organization member.",
        "additionalProperties": false
      },
      "MemberResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Member"
          }
        },
        "required": ["data"],
        "description": "One member.",
        "additionalProperties": false
      },
      "MemberListResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Member"
            },
            "description": "Every member of the organization."
          }
        },
        "required": ["data"],
        "description": "The organization's members.",
        "additionalProperties": false
      },
      "AddMemberRequest": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "description": "Email of an existing Zelos user."
          },
          "role": {
            "type": "string",
            "enum": ["member", "admin"],
            "default": "member",
            "description": "Role to grant. Only owners may grant `admin`."
          }
        },
        "required": ["email"],
        "description": "Add an existing user as a member.",
        "additionalProperties": false
      },
      "UpdateMemberRoleRequest": {
        "type": "object",
        "properties": {
          "role": {
            "type": "string",
            "enum": ["owner", "admin", "member"],
            "description": "The member's new role."
          }
        },
        "required": ["role"],
        "description": "Change a member's role.",
        "additionalProperties": false
      },
      "Invitation": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The invitation's id."
          },
          "email": {
            "type": "string",
            "description": "Address the invitation was created for."
          },
          "role": {
            "type": "string",
            "enum": ["admin", "member"],
            "description": "Role the invitee will receive."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the invitation stops being acceptable."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the invitation was created."
          }
        },
        "required": ["id", "email", "role", "expiresAt", "createdAt"],
        "description": "A pending organization invitation.",
        "additionalProperties": false
      },
      "InvitationResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Invitation"
          }
        },
        "required": ["data"],
        "description": "One invitation.",
        "additionalProperties": false
      },
      "InvitationListResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Invitation"
            },
            "description": "Invitations not yet accepted, cancelled, or expired."
          }
        },
        "required": ["data"],
        "description": "Pending invitations.",
        "additionalProperties": false
      },
      "CreateInvitationRequest": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "description": "Address to invite."
          },
          "role": {
            "type": "string",
            "enum": ["member", "admin"],
            "default": "member",
            "description": "Role the invitee will receive."
          }
        },
        "required": ["email"],
        "description": "Invite someone by email.",
        "additionalProperties": false
      }
    }
  }
}
