{
  "openapi": "3.1.0",
  "info": {
    "title": "SmaCH API",
    "description": "Artworks, their monitorings, the equipment that records them, and the organizations and members that own them.",
    "contact": {
      "name": "SmaCH",
      "url": "https://smach.science",
      "email": "contact@smach.science"
    },
    "version": "v1"
  },
  "tags": [
    {
      "name": "monitorings",
      "description": "Monitorings: recording sessions, the equipment they engage",
      "x-displayName": "monitorings"
    },
    {
      "name": "events",
      "description": "Events: what a recording session recorded",
      "x-displayName": "events"
    },
    {
      "name": "artworks",
      "description": "Artworks: the monitored works, their packaging, checklist and configuration",
      "x-displayName": "artworks"
    },
    {
      "name": "inventories",
      "description": "Equipment: dataloggers, sensors, gateways and packaging",
      "x-displayName": "inventories"
    },
    {
      "name": "me",
      "description": "The current account",
      "x-displayName": "me"
    },
    {
      "name": "users",
      "description": "User management (super-admin)",
      "x-displayName": "users"
    },
    {
      "name": "organizations",
      "description": "Organization management (super-admin)",
      "x-displayName": "organizations"
    },
    {
      "name": "members",
      "description": "Organization membership and roles (super-admin or organization administrator)",
      "x-displayName": "members"
    }
  ],
  "paths": {
    "/inventories/sensors/{identity}": {
      "get": {
        "tags": [
          "inventories"
        ],
        "summary": "Get a sensor by identity",
        "operationId": "getSensor",
        "parameters": [
          {
            "name": "identity",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "404": {
            "description": "Unknown identity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Sensor"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "put": {
        "tags": [
          "inventories"
        ],
        "summary": "Update a sensor",
        "operationId": "updateSensor",
        "parameters": [
          {
            "name": "identity",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Sensor"
              }
            }
          },
          "required": true
        },
        "responses": {
          "404": {
            "description": "Unknown identity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Sensor"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "delete": {
        "tags": [
          "inventories"
        ],
        "summary": "Delete a sensor",
        "operationId": "deleteSensor",
        "parameters": [
          {
            "name": "identity",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "404": {
            "description": "Unknown identity"
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/inventories/packaging/{identity}": {
      "get": {
        "tags": [
          "inventories"
        ],
        "summary": "Get a packaging by identity",
        "operationId": "getPackaging",
        "parameters": [
          {
            "name": "identity",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "404": {
            "description": "Unknown identity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Packaging"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "put": {
        "tags": [
          "inventories"
        ],
        "summary": "Update a packaging",
        "operationId": "updatePackaging",
        "parameters": [
          {
            "name": "identity",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Packaging"
              }
            }
          },
          "required": true
        },
        "responses": {
          "404": {
            "description": "Unknown identity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Packaging"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "delete": {
        "tags": [
          "inventories"
        ],
        "summary": "Delete a packaging",
        "operationId": "deletePackaging",
        "parameters": [
          {
            "name": "identity",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "404": {
            "description": "Unknown identity"
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/inventories/loggers/{identity}": {
      "get": {
        "tags": [
          "inventories"
        ],
        "summary": "Get a logger by identity",
        "operationId": "getLogger",
        "parameters": [
          {
            "name": "identity",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "404": {
            "description": "Unknown identity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Logger"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "put": {
        "tags": [
          "inventories"
        ],
        "summary": "Update a logger",
        "operationId": "updateLogger",
        "parameters": [
          {
            "name": "identity",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Logger"
              }
            }
          },
          "required": true
        },
        "responses": {
          "404": {
            "description": "Unknown identity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Logger"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "delete": {
        "tags": [
          "inventories"
        ],
        "summary": "Delete a logger",
        "operationId": "deleteLogger",
        "parameters": [
          {
            "name": "identity",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "404": {
            "description": "Unknown identity"
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/inventories/gateways/{identity}": {
      "get": {
        "tags": [
          "inventories"
        ],
        "summary": "Get a gateway by identity",
        "operationId": "getGateway",
        "parameters": [
          {
            "name": "identity",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "404": {
            "description": "Unknown identity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Gateway"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "put": {
        "tags": [
          "inventories"
        ],
        "summary": "Update a gateway",
        "operationId": "updateGateway",
        "parameters": [
          {
            "name": "identity",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Gateway"
              }
            }
          },
          "required": true
        },
        "responses": {
          "404": {
            "description": "Unknown identity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Gateway"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "delete": {
        "tags": [
          "inventories"
        ],
        "summary": "Delete a gateway",
        "operationId": "deleteGateway",
        "parameters": [
          {
            "name": "identity",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "404": {
            "description": "Unknown identity"
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/artworks/{reference}": {
      "get": {
        "tags": [
          "artworks"
        ],
        "summary": "Get an artwork by reference",
        "operationId": "getArtwork",
        "parameters": [
          {
            "name": "reference",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "404": {
            "description": "Unknown reference",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Artwork"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "put": {
        "tags": [
          "artworks"
        ],
        "summary": "Update an artwork",
        "description": "Replaces the artwork whole. The reference cannot change, and a body without a status keeps the stored one. standby and monitoring follow the monitoring of the artwork: a request neither sets them nor leaves them.",
        "operationId": "updateArtwork",
        "parameters": [
          {
            "name": "reference",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Artwork"
              }
            }
          },
          "required": true
        },
        "responses": {
          "400": {
            "description": "Invalid body, packaging, or a reference that differs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Artwork"
                }
              }
            }
          },
          "404": {
            "description": "Unknown reference",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Artwork"
                }
              }
            }
          },
          "409": {
            "description": "Status owned by the monitoring lifecycle",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Artwork"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "delete": {
        "tags": [
          "artworks"
        ],
        "summary": "Delete an artwork and its history",
        "description": "Removes the artwork with its monitorings and their events. Archiving, a status, is the ordinary way to retire an artwork; deletion is for a confirmed purge.",
        "operationId": "deleteArtwork",
        "parameters": [
          {
            "name": "reference",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "404": {
            "description": "Unknown reference"
          },
          "409": {
            "description": "A monitoring is set up or in progress"
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/inventories/sensors": {
      "get": {
        "tags": [
          "inventories"
        ],
        "summary": "List sensors",
        "description": "Follow the Link header to page through the collection rather than building page URLs. The search parameter matches the name, the identity and the description, anywhere in the field. Sortable on name, identity, created, updated.",
        "operationId": "listSensors",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "0-indexed page number",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 0,
              "minimum": 0
            }
          },
          {
            "name": "size",
            "in": "query",
            "description": "Page size",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 50,
              "maximum": 200,
              "minimum": 1
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Free-text search. The fields it matches are documented per endpoint.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Sort field, prefixed with '-' for descending (e.g. -createdAt), composed with commas. The sortable fields are documented per endpoint.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "captor",
            "in": "query",
            "description": "Keep only probes carrying this captor",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/Captor"
            }
          },
          {
            "name": "inService",
            "in": "query",
            "description": "true keeps only the items engaged by a monitoring that is set up or recording, false only the others, which are the ones available for a Setup. A monitoring that ended does not count: an item it used is free again. Requires the X-Organization header.",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of results",
            "headers": {
              "X-Total-Count": {
                "description": "Total number of resources matching the filters. Absent where counting is too expensive, in which case rel=\"last\" is absent too.",
                "style": "simple",
                "schema": {
                  "type": "integer"
                }
              },
              "Link": {
                "description": "RFC 8288 navigation links: first, prev, next, last. The absence of rel=\"next\" is the authoritative end-of-collection signal; do not infer it from the array length, and follow these links rather than building page URLs by hand.",
                "style": "simple",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Sensor"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "post": {
        "tags": [
          "inventories"
        ],
        "summary": "Create a sensor",
        "operationId": "createSensor",
        "parameters": [
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Sensor"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Sensor"
                }
              }
            }
          },
          "409": {
            "description": "Identity already in use",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Sensor"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/inventories/packaging": {
      "get": {
        "tags": [
          "inventories"
        ],
        "summary": "List packaging",
        "description": "Follow the Link header to page through the collection rather than building page URLs. The search parameter matches the name, the identity and the description, anywhere in the field. Search also matches the category. Sortable on name, identity, created, updated, kind, category, weight.",
        "operationId": "listPackaging",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "0-indexed page number",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 0,
              "minimum": 0
            }
          },
          {
            "name": "size",
            "in": "query",
            "description": "Page size",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 50,
              "maximum": 200,
              "minimum": 1
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Free-text search. The fields it matches are documented per endpoint.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Sort field, prefixed with '-' for descending (e.g. -createdAt), composed with commas. The sortable fields are documented per endpoint.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "description": "Keep only inclosures, or only crates",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/Kind"
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of results",
            "headers": {
              "X-Total-Count": {
                "description": "Total number of resources matching the filters. Absent where counting is too expensive, in which case rel=\"last\" is absent too.",
                "style": "simple",
                "schema": {
                  "type": "integer"
                }
              },
              "Link": {
                "description": "RFC 8288 navigation links: first, prev, next, last. The absence of rel=\"next\" is the authoritative end-of-collection signal; do not infer it from the array length, and follow these links rather than building page URLs by hand.",
                "style": "simple",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Packaging"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "post": {
        "tags": [
          "inventories"
        ],
        "summary": "Create a packaging",
        "operationId": "createPackaging",
        "parameters": [
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Packaging"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Packaging"
                }
              }
            }
          },
          "409": {
            "description": "Identity already in use",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Packaging"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/inventories/loggers": {
      "get": {
        "tags": [
          "inventories"
        ],
        "summary": "List loggers",
        "description": "Follow the Link header to page through the collection rather than building page URLs. The search parameter matches the name, the identity and the description, anywhere in the field. Sortable on name, identity, created, updated.",
        "operationId": "listLoggers",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "0-indexed page number",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 0,
              "minimum": 0
            }
          },
          {
            "name": "size",
            "in": "query",
            "description": "Page size",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 50,
              "maximum": 200,
              "minimum": 1
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Free-text search. The fields it matches are documented per endpoint.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Sort field, prefixed with '-' for descending (e.g. -createdAt), composed with commas. The sortable fields are documented per endpoint.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "inService",
            "in": "query",
            "description": "true keeps only the items engaged by a monitoring that is set up or recording, false only the others, which are the ones available for a Setup. A monitoring that ended does not count: an item it used is free again. Requires the X-Organization header.",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of results",
            "headers": {
              "X-Total-Count": {
                "description": "Total number of resources matching the filters. Absent where counting is too expensive, in which case rel=\"last\" is absent too.",
                "style": "simple",
                "schema": {
                  "type": "integer"
                }
              },
              "Link": {
                "description": "RFC 8288 navigation links: first, prev, next, last. The absence of rel=\"next\" is the authoritative end-of-collection signal; do not infer it from the array length, and follow these links rather than building page URLs by hand.",
                "style": "simple",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Logger"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "post": {
        "tags": [
          "inventories"
        ],
        "summary": "Create a logger",
        "operationId": "createLogger",
        "parameters": [
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Logger"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Logger"
                }
              }
            }
          },
          "409": {
            "description": "Identity already in use",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Logger"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/inventories/gateways": {
      "get": {
        "tags": [
          "inventories"
        ],
        "summary": "List gateways",
        "description": "Follow the Link header to page through the collection rather than building page URLs. The search parameter matches the name, the identity and the description, anywhere in the field. Sortable on name, identity, created, updated.",
        "operationId": "listGateways",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "0-indexed page number",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 0,
              "minimum": 0
            }
          },
          {
            "name": "size",
            "in": "query",
            "description": "Page size",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 50,
              "maximum": 200,
              "minimum": 1
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Free-text search. The fields it matches are documented per endpoint.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Sort field, prefixed with '-' for descending (e.g. -createdAt), composed with commas. The sortable fields are documented per endpoint.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "inService",
            "in": "query",
            "description": "true keeps only the items engaged by a monitoring that is set up or recording, false only the others, which are the ones available for a Setup. A monitoring that ended does not count: an item it used is free again. Requires the X-Organization header.",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of results",
            "headers": {
              "X-Total-Count": {
                "description": "Total number of resources matching the filters. Absent where counting is too expensive, in which case rel=\"last\" is absent too.",
                "style": "simple",
                "schema": {
                  "type": "integer"
                }
              },
              "Link": {
                "description": "RFC 8288 navigation links: first, prev, next, last. The absence of rel=\"next\" is the authoritative end-of-collection signal; do not infer it from the array length, and follow these links rather than building page URLs by hand.",
                "style": "simple",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Gateway"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "post": {
        "tags": [
          "inventories"
        ],
        "summary": "Create a gateway",
        "operationId": "createGateway",
        "parameters": [
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Gateway"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Gateway"
                }
              }
            }
          },
          "409": {
            "description": "Identity already in use",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Gateway"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/artworks": {
      "get": {
        "tags": [
          "artworks"
        ],
        "summary": "List artworks",
        "description": "Follow the Link header to page through the collection rather than building page URLs. The search parameter matches the name, the reference, the summary and the description, anywhere in the field. Sortable on name, reference, status, created, updated; newest first by default.",
        "operationId": "listArtworks",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "0-indexed page number",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 0,
              "minimum": 0
            }
          },
          {
            "name": "size",
            "in": "query",
            "description": "Page size",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 50,
              "maximum": 200,
              "minimum": 1
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Free-text search. The fields it matches are documented per endpoint.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Sort field, prefixed with '-' for descending (e.g. -createdAt), composed with commas. The sortable fields are documented per endpoint.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Keep only artworks in one of these statuses",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "draft",
                  "ready",
                  "standby",
                  "monitoring",
                  "archived",
                  "cancelled"
                ]
              }
            }
          },
          {
            "name": "tags",
            "in": "query",
            "description": "Keep only artworks carrying at least one of these tags",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "packaging",
            "in": "query",
            "description": "Keep only artworks using this packaging, as inclosure or as crate",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of results",
            "headers": {
              "X-Total-Count": {
                "description": "Total number of resources matching the filters. Absent where counting is too expensive, in which case rel=\"last\" is absent too.",
                "style": "simple",
                "schema": {
                  "type": "integer"
                }
              },
              "Link": {
                "description": "RFC 8288 navigation links: first, prev, next, last. The absence of rel=\"next\" is the authoritative end-of-collection signal; do not infer it from the array length, and follow these links rather than building page URLs by hand.",
                "style": "simple",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Artwork"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "post": {
        "tags": [
          "artworks"
        ],
        "summary": "Create an artwork",
        "description": "Created as draft, or as ready when the body says so. The inclosure and the crate, when given, must be packaging of that kind held by the inventory.",
        "operationId": "createArtwork",
        "parameters": [
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Artwork"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Artwork"
                }
              }
            }
          },
          "400": {
            "description": "Invalid body, status or packaging",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Artwork"
                }
              }
            }
          },
          "409": {
            "description": "Reference already in use",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Artwork"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/artworks/{reference}/monitorings": {
      "get": {
        "tags": [
          "monitorings"
        ],
        "summary": "List the monitorings of an artwork",
        "description": "The history of one artwork, newest first. Follow the Link header to page through it.",
        "operationId": "listArtworkMonitorings",
        "parameters": [
          {
            "name": "reference",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "0-indexed page number",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 0,
              "minimum": 0
            }
          },
          {
            "name": "size",
            "in": "query",
            "description": "Page size",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 50,
              "maximum": 200,
              "minimum": 1
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Free-text search. The fields it matches are documented per endpoint.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Sort field, prefixed with '-' for descending (e.g. -createdAt), composed with commas. The sortable fields are documented per endpoint.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of results",
            "headers": {
              "X-Total-Count": {
                "description": "Total number of resources matching the filters. Absent where counting is too expensive, in which case rel=\"last\" is absent too.",
                "style": "simple",
                "schema": {
                  "type": "integer"
                }
              },
              "Link": {
                "description": "RFC 8288 navigation links: first, prev, next, last. The absence of rel=\"next\" is the authoritative end-of-collection signal; do not infer it from the array length, and follow these links rather than building page URLs by hand.",
                "style": "simple",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Monitoring"
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown artwork reference",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Monitoring"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "post": {
        "tags": [
          "monitorings"
        ],
        "summary": "Set up a monitoring on an artwork",
        "description": "Opens a pending session and moves the artwork to standby. Every logger, sensor and gateway named must be held by the inventory and free of any other active monitoring. The status is the lifecycle's: a request neither sets it nor leaves it.",
        "operationId": "createMonitoring",
        "parameters": [
          {
            "name": "reference",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Monitoring"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Set up, waiting for the start",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Monitoring"
                }
              }
            }
          },
          "400": {
            "description": "Invalid body, or equipment the inventory does not hold",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Monitoring"
                }
              }
            }
          },
          "404": {
            "description": "Unknown artwork reference",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Monitoring"
                }
              }
            }
          },
          "409": {
            "description": "The artwork is not ready, or equipment is already engaged",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Monitoring"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/artworks/{reference}/monitorings/events": {
      "post": {
        "tags": [
          "events"
        ],
        "summary": "Push an event on the monitoring of an artwork",
        "description": "Records what happened, on the session the source belongs to. A start moves that session to in progress and the artwork to monitoring; a stop ends it and sends the artwork back to ready. Either one arriving twice is accepted and changes nothing, so a retry never loses the event it carries. An ended session records nothing more.",
        "operationId": "pushArtworkMonitoringEvent",
        "parameters": [
          {
            "name": "reference",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Event"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Event"
                }
              }
            }
          },
          "400": {
            "description": "Invalid body, or a source that is not a monitoring id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Event"
                }
              }
            }
          },
          "404": {
            "description": "Unknown monitoring id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Event"
                }
              }
            }
          },
          "409": {
            "description": "No monitoring under way for that source, one that belongs to another artwork, one that has ended, or a start on a session that cannot restart",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Event"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/monitorings": {
      "get": {
        "tags": [
          "monitorings"
        ],
        "summary": "List monitorings",
        "description": "Follow the Link header to page through the collection rather than building page URLs. The search parameter matches the reference of the monitored artwork, anywhere in the field. Sortable on created, updated, status, type, artwork; newest first by default.",
        "operationId": "listMonitorings",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "0-indexed page number",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 0,
              "minimum": 0
            }
          },
          {
            "name": "size",
            "in": "query",
            "description": "Page size",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 50,
              "maximum": 200,
              "minimum": 1
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Free-text search. The fields it matches are documented per endpoint.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Sort field, prefixed with '-' for descending (e.g. -createdAt), composed with commas. The sortable fields are documented per endpoint.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Keep only monitorings in one of these statuses",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "description": "Status of a monitoring session",
                "enum": [
                  "pending",
                  "in_progress",
                  "ended"
                ]
              }
            }
          },
          {
            "name": "type",
            "in": "query",
            "description": "Keep only monitorings of one of these types",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "description": "What is being monitored: a work at rest, or a work on the move",
                "enum": [
                  "stationary",
                  "transport"
                ]
              }
            }
          },
          {
            "name": "artwork",
            "in": "query",
            "description": "Keep only monitorings of this artwork, by reference",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of results",
            "headers": {
              "X-Total-Count": {
                "description": "Total number of resources matching the filters. Absent where counting is too expensive, in which case rel=\"last\" is absent too.",
                "style": "simple",
                "schema": {
                  "type": "integer"
                }
              },
              "Link": {
                "description": "RFC 8288 navigation links: first, prev, next, last. The absence of rel=\"next\" is the authoritative end-of-collection signal; do not infer it from the array length, and follow these links rather than building page URLs by hand.",
                "style": "simple",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Monitoring"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/monitorings/{id}": {
      "get": {
        "tags": [
          "monitorings"
        ],
        "summary": "Get a monitoring by id",
        "operationId": "getMonitoring",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "404": {
            "description": "Unknown id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Monitoring"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/monitorings/{id}/events": {
      "get": {
        "tags": [
          "events"
        ],
        "summary": "List the events of a monitoring",
        "description": "The timeline of one session, oldest first. Follow the Link header to page through it. The search parameter matches the message of an alert, anywhere in the field. Sortable on date, type, severity, created.",
        "operationId": "listMonitoringEvents",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "0-indexed page number",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 0,
              "minimum": 0
            }
          },
          {
            "name": "size",
            "in": "query",
            "description": "Page size",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 50,
              "maximum": 200,
              "minimum": 1
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Free-text search. The fields it matches are documented per endpoint.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Sort field, prefixed with '-' for descending (e.g. -createdAt), composed with commas. The sortable fields are documented per endpoint.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "description": "Keep only events of one of these types",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "description": "What happened during a recording session",
                "enum": [
                  "start",
                  "alert",
                  "position",
                  "notification",
                  "stop"
                ]
              }
            }
          },
          {
            "name": "severity",
            "in": "query",
            "description": "Keep only events of one of these severities",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "description": "How much an event matters",
                "enum": [
                  "info",
                  "warning",
                  "critical"
                ]
              }
            }
          },
          {
            "name": "sourceType",
            "in": "query",
            "description": "Keep only events reported by that kind of source",
            "required": false,
            "schema": {
              "type": "string",
              "description": "What the source names, which decides how the event is routed",
              "enum": [
                "monitoring",
                "sensor",
                "logger",
                "user"
              ]
            }
          },
          {
            "name": "source",
            "in": "query",
            "description": "Keep only events reported by that exact source",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sensor",
            "in": "query",
            "description": "Keep only alerts raised by that captor",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/Captor"
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of results",
            "headers": {
              "X-Total-Count": {
                "description": "Total number of resources matching the filters. Absent where counting is too expensive, in which case rel=\"last\" is absent too.",
                "style": "simple",
                "schema": {
                  "type": "integer"
                }
              },
              "Link": {
                "description": "RFC 8288 navigation links: first, prev, next, last. The absence of rel=\"next\" is the authoritative end-of-collection signal; do not infer it from the array length, and follow these links rather than building page URLs by hand.",
                "style": "simple",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Event"
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown monitoring id",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Event"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/monitorings/{id}/events/count": {
      "get": {
        "tags": [
          "events"
        ],
        "summary": "Count what a monitoring recorded",
        "description": "Totals per type, and for alerts per severity and per captor. A breakdown carries only the keys the session has; an absent key reads as zero.",
        "operationId": "countMonitoringEvents",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "404": {
            "description": "Unknown monitoring id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventCountReport"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/monitorings/{id}/charts": {
      "get": {
        "tags": [
          "events"
        ],
        "summary": "Chart the alerts of a monitoring",
        "description": "The alerts of one session that name a captor, oldest first, one series per captor, laid out in parallel columns. Values are raw device units. The thresholds to draw are the configuration of the monitoring. Alerts on the battery or the storage are not charted.",
        "operationId": "getMonitoringCharts",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Keep alerts dated from this instant, inclusive (ISO 8601)",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "example": "2026-09-24T09:00:00Z"
          },
          {
            "name": "to",
            "in": "query",
            "description": "Keep alerts dated up to this instant, inclusive (ISO 8601)",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "example": "2026-09-24T18:00:00Z"
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "400": {
            "description": "from is after to, or a bound is not an ISO 8601 instant",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MonitoringCharts"
                }
              }
            }
          },
          "404": {
            "description": "Unknown monitoring id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MonitoringCharts"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/monitorings/overview": {
      "get": {
        "tags": [
          "events"
        ],
        "summary": "Where the monitorings stand",
        "description": "Monitorings, each with what its events say: when it started and ended, when it last reported, its alerts per severity, its latest alert and its latest position. Without a status, the ones that are set up or recording. Filters, searches, pages and sorts as the monitoring list does. Facts only: how long a silence has to last before it matters is left to the reader, since events are relayed by the mobile application rather than sent by the devices.",
        "operationId": "listMonitoringOverviews",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "0-indexed page number",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 0,
              "minimum": 0
            }
          },
          {
            "name": "size",
            "in": "query",
            "description": "Page size",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 50,
              "maximum": 200,
              "minimum": 1
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Free-text search. The fields it matches are documented per endpoint.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Sort field, prefixed with '-' for descending (e.g. -createdAt), composed with commas. The sortable fields are documented per endpoint.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Keep only monitorings in one of these statuses; set up or recording when absent",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "description": "Status of a monitoring session",
                "enum": [
                  "pending",
                  "in_progress",
                  "ended"
                ]
              }
            }
          },
          {
            "name": "type",
            "in": "query",
            "description": "Keep only monitorings of one of these types",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "description": "What is being monitored: a work at rest, or a work on the move",
                "enum": [
                  "stationary",
                  "transport"
                ]
              }
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of results",
            "headers": {
              "X-Total-Count": {
                "description": "Total number of resources matching the filters. Absent where counting is too expensive, in which case rel=\"last\" is absent too.",
                "style": "simple",
                "schema": {
                  "type": "integer"
                }
              },
              "Link": {
                "description": "RFC 8288 navigation links: first, prev, next, last. The absence of rel=\"next\" is the authoritative end-of-collection signal; do not infer it from the array length, and follow these links rather than building page URLs by hand.",
                "style": "simple",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/MonitoringOverview"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/monitorings/events/stream": {
      "get": {
        "tags": [
          "events"
        ],
        "summary": "Subscribe to events as they happen",
        "description": "Opens a Server-Sent Events stream carrying every event recorded by the organization, whichever session recorded it, in the same JSON form the timeline endpoints return. The stream ends on its own after a while and is meant to be reopened; a browser EventSource does that by itself.\n\nTwo things are worth knowing before reading from it. Alerts are rate limited per captor and per session, so a burst arrives as one alert per captor rather than as hundreds; the timeline endpoint is what holds all of them. And a comment line is sent down a quiet stream at regular intervals to keep intermediaries from closing it, which a client never sees as a message.\n\nWhat reaches a subscriber is what their organization recorded, and nothing else.",
        "operationId": "streamMonitoringEvents",
        "parameters": [
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The stream is open",
            "content": {
              "text/event-stream": {
                "schema": {
                  "$ref": "#/components/schemas/Event"
                }
              }
            }
          },
          "400": {
            "description": "No organization named",
            "content": {
              "text/event-stream": {
                "schema": {
                  "$ref": "#/components/schemas/SseEmitter"
                }
              }
            }
          },
          "503": {
            "description": "This instance is shutting down",
            "content": {
              "text/event-stream": {
                "schema": {
                  "$ref": "#/components/schemas/SseEmitter"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/monitorings/count": {
      "get": {
        "tags": [
          "monitorings"
        ],
        "summary": "Count monitorings matching the filters",
        "description": "Answers a business question; paging is driven by the list endpoint's Link header. The filters have the same meaning as on the list endpoint.",
        "operationId": "countMonitorings",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "description": "Status of a monitoring session",
                "enum": [
                  "pending",
                  "in_progress",
                  "ended"
                ]
              }
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "description": "What is being monitored: a work at rest, or a work on the move",
                "enum": [
                  "stationary",
                  "transport"
                ]
              }
            }
          },
          {
            "name": "artwork",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Count"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/inventories/sensors/{identity}/exists": {
      "get": {
        "tags": [
          "inventories"
        ],
        "summary": "Check whether a sensor identity is taken",
        "description": "Whether this identity is already used in the current organization. Intended for live validation while a form is being filled: creating a duplicate answers 409 regardless.",
        "operationId": "sensorExists",
        "parameters": [
          {
            "name": "identity",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Exists"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/inventories/sensors/count": {
      "get": {
        "tags": [
          "inventories"
        ],
        "summary": "Count sensors matching the filters",
        "description": "Answers a business question; paging is driven by the list endpoint's Link header. The filters have the same meaning as on the list endpoint.",
        "operationId": "countSensors",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "captor",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/Captor"
            }
          },
          {
            "name": "inService",
            "in": "query",
            "description": "true keeps only the items engaged by a monitoring that is set up or recording, false only the others, which are the ones available for a Setup. A monitoring that ended does not count: an item it used is free again. Requires the X-Organization header.",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Count"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/inventories/packaging/{identity}/exists": {
      "get": {
        "tags": [
          "inventories"
        ],
        "summary": "Check whether a packaging identity is taken",
        "description": "Whether this identity is already used in the current organization. Intended for live validation while a form is being filled: creating a duplicate answers 409 regardless.",
        "operationId": "packagingExists",
        "parameters": [
          {
            "name": "identity",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Exists"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/inventories/packaging/count": {
      "get": {
        "tags": [
          "inventories"
        ],
        "summary": "Count packaging matching the filters",
        "description": "Answers a business question; paging is driven by the list endpoint's Link header. The filters have the same meaning as on the list endpoint.",
        "operationId": "countPackaging",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/Kind"
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Count"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/inventories/loggers/{identity}/exists": {
      "get": {
        "tags": [
          "inventories"
        ],
        "summary": "Check whether a logger identity is taken",
        "description": "Whether this identity is already used in the current organization. Intended for live validation while a form is being filled: creating a duplicate answers 409 regardless.",
        "operationId": "loggerExists",
        "parameters": [
          {
            "name": "identity",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Exists"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/inventories/loggers/count": {
      "get": {
        "tags": [
          "inventories"
        ],
        "summary": "Count loggers matching the filters",
        "description": "Answers a business question; paging is driven by the list endpoint's Link header. The filters have the same meaning as on the list endpoint.",
        "operationId": "countLoggers",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "inService",
            "in": "query",
            "description": "true keeps only the items engaged by a monitoring that is set up or recording, false only the others, which are the ones available for a Setup. A monitoring that ended does not count: an item it used is free again. Requires the X-Organization header.",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Count"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/inventories/gateways/{identity}/exists": {
      "get": {
        "tags": [
          "inventories"
        ],
        "summary": "Check whether a gateway identity is taken",
        "description": "Whether this identity is already used in the current organization. Intended for live validation while a form is being filled: creating a duplicate answers 409 regardless.",
        "operationId": "gatewayExists",
        "parameters": [
          {
            "name": "identity",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Exists"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/inventories/gateways/count": {
      "get": {
        "tags": [
          "inventories"
        ],
        "summary": "Count gateways matching the filters",
        "description": "Answers a business question; paging is driven by the list endpoint's Link header. The filters have the same meaning as on the list endpoint.",
        "operationId": "countGateways",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "inService",
            "in": "query",
            "description": "true keeps only the items engaged by a monitoring that is set up or recording, false only the others, which are the ones available for a Setup. A monitoring that ended does not count: an item it used is free again. Requires the X-Organization header.",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Count"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/artworks/tags": {
      "get": {
        "tags": [
          "artworks"
        ],
        "summary": "List the tags in use",
        "description": "Every tag carried by at least one artwork, once, in alphabetical order.",
        "operationId": "listArtworkTags",
        "parameters": [
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tags"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/artworks/count": {
      "get": {
        "tags": [
          "artworks"
        ],
        "summary": "Count artworks matching the filters",
        "description": "Answers a business question; paging is driven by the list endpoint's Link header. The filters have the same meaning as on the list endpoint.",
        "operationId": "countArtworks",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "draft",
                  "ready",
                  "standby",
                  "monitoring",
                  "archived",
                  "cancelled"
                ]
              }
            }
          },
          {
            "name": "tags",
            "in": "query",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "packaging",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Count"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/monitorings/events/{id}": {
      "delete": {
        "tags": [
          "events"
        ],
        "summary": "Delete one recorded event",
        "description": "Removes one measurement from a session. A start or a stop is refused: those are the bounds of the session and its status was moved by them.",
        "operationId": "deleteMonitoringEvent",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "X-Organization",
            "in": "header",
            "description": "The organization alias the request applies to, not its display name.\n\nOptional for an account belonging to a single organization, which is resolved from the token. Required beyond that, since no default is defensible. A super-admin may name any organization, or none, which reads across all of them.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "bayeux"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "404": {
            "description": "Unknown event id"
          },
          "409": {
            "description": "The event is the start or the stop of its session"
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/users/{id}": {
      "get": {
        "tags": [
          "users"
        ],
        "summary": "Get a user by id",
        "operationId": "getUser",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "404": {
            "description": "Unknown id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "put": {
        "tags": [
          "users"
        ],
        "summary": "Update a user",
        "description": "Updates email, names, language, active state and the super-admin role; disabling (enabled=false) is the soft alternative to deletion. Fields left out are kept.",
        "operationId": "updateUser",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/User"
              }
            }
          },
          "required": true
        },
        "responses": {
          "404": {
            "description": "Unknown id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          },
          "409": {
            "description": "A super-admin removing their own super-admin role",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "delete": {
        "tags": [
          "users"
        ],
        "summary": "Delete a user permanently",
        "operationId": "deleteUser",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "User deleted"
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/organizations/{alias}": {
      "get": {
        "tags": [
          "organizations"
        ],
        "summary": "Get an organization by alias",
        "operationId": "getOrganization",
        "parameters": [
          {
            "name": "alias",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "404": {
            "description": "Unknown alias",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organization"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "put": {
        "tags": [
          "organizations"
        ],
        "summary": "Update an organization",
        "description": "Updates the name, state and description; the alias is immutable",
        "operationId": "updateOrganization",
        "parameters": [
          {
            "name": "alias",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Organization"
              }
            }
          },
          "required": true
        },
        "responses": {
          "404": {
            "description": "Unknown alias",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organization"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "delete": {
        "tags": [
          "organizations"
        ],
        "summary": "Delete an organization",
        "description": "Deletes the organization and the memberships; the accounts of its members stay. Refused while it still has members, unless force is true.",
        "operationId": "deleteOrganization",
        "parameters": [
          {
            "name": "alias",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "force",
            "in": "query",
            "description": "Delete even if the organization still has members",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Organization deleted"
          },
          "404": {
            "description": "Unknown alias"
          },
          "409": {
            "description": "The organization still has members and force is not set"
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/organizations/{alias}/members/{userId}": {
      "put": {
        "tags": [
          "members"
        ],
        "summary": "Add a member or set their role",
        "description": "Attaches the user to the organization if needed and grants the given role",
        "operationId": "setMemberRole",
        "parameters": [
          {
            "name": "alias",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RoleAssignment"
              }
            }
          },
          "required": true
        },
        "responses": {
          "404": {
            "description": "Unknown organization or user",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "409": {
            "description": "An organization administrator changing their own role",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "delete": {
        "tags": [
          "members"
        ],
        "summary": "Remove a member from the organization",
        "operationId": "removeMember",
        "parameters": [
          {
            "name": "alias",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Member removed"
          },
          "409": {
            "description": "An organization administrator removing themselves"
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/me/profile": {
      "get": {
        "tags": [
          "me"
        ],
        "summary": "Get the current account's profile",
        "description": "Names, language and email of the account behind the token, read from the directory.",
        "operationId": "getCurrentProfile",
        "responses": {
          "200": {
            "description": "The profile",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Profile"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "put": {
        "tags": [
          "me"
        ],
        "summary": "Update the current account's profile",
        "description": "Changes the names and the language of the account behind the token. The email, the state and the roles of the account cannot be changed here; the password is changed on the Keycloak page, never through this API. The next token carries the new names.",
        "operationId": "updateCurrentProfile",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Profile"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "The updated profile",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Profile"
                }
              }
            }
          },
          "400": {
            "description": "A name is missing or the language is malformed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Profile"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/users": {
      "get": {
        "tags": [
          "users"
        ],
        "summary": "List users",
        "description": "Follow the Link header to page through the collection rather than building page URLs. The search parameter matches the username, the email and both names, anywhere in the field; a term carrying a * wildcard is passed through as written, so term* stays anchored at the start.",
        "operationId": "listUsers",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "0-indexed page number",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 0,
              "minimum": 0
            }
          },
          {
            "name": "size",
            "in": "query",
            "description": "Page size",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 50,
              "maximum": 200,
              "minimum": 1
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Free-text search. The fields it matches are documented per endpoint.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Sort field, prefixed with '-' for descending (e.g. -createdAt). Only present on endpoints whose backing store supports sorting.",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of results",
            "headers": {
              "X-Total-Count": {
                "description": "Total number of resources matching the filters. Absent where counting is too expensive, in which case rel=\"last\" is absent too.",
                "style": "simple",
                "schema": {
                  "type": "integer"
                }
              },
              "Link": {
                "description": "RFC 8288 navigation links: first, prev, next, last. The absence of rel=\"next\" is the authoritative end-of-collection signal; do not infer it from the array length, and follow these links rather than building page URLs by hand.",
                "style": "simple",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/User"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "post": {
        "tags": [
          "users"
        ],
        "summary": "Create a user",
        "description": "Creates a user without credentials. Send the invitation next (POST /users/{id}/invitation): a failed email then leaves the user created, and the invitation alone is retried.",
        "operationId": "createUser",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/User"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "User created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          },
          "409": {
            "description": "Email already in use",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/users/{id}/password-reset": {
      "post": {
        "tags": [
          "users"
        ],
        "summary": "Email a password reset link to a user",
        "description": "Keycloak emails a link where the person chooses a new password. The current password stays valid until then.",
        "operationId": "resetUserPassword",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Email sent"
          },
          "404": {
            "description": "Unknown id"
          },
          "409": {
            "description": "User disabled"
          },
          "502": {
            "description": "Keycloak could not send the email; worth retrying"
          },
          "504": {
            "description": "Keycloak did not answer in time, the mail server being slow or down; worth retrying"
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/users/{id}/invitation": {
      "post": {
        "tags": [
          "users"
        ],
        "summary": "Email the invitation to a user",
        "description": "Keycloak emails a link, valid three days, where the person chooses a password and confirms the address, then lands on SmaRT. Sending it again resends the invitation.",
        "operationId": "inviteUser",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Email sent"
          },
          "404": {
            "description": "Unknown id"
          },
          "409": {
            "description": "User disabled"
          },
          "502": {
            "description": "Keycloak could not send the email; worth retrying"
          },
          "504": {
            "description": "Keycloak did not answer in time, the mail server being slow or down; worth retrying"
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/organizations": {
      "get": {
        "tags": [
          "organizations"
        ],
        "summary": "List organizations",
        "description": "Follow the Link header to page through the collection rather than building page URLs. The search parameter matches the organization name and its domains, never the alias: to reach one organization by alias, use GET /organizations/{alias}.",
        "operationId": "listOrganizations",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "0-indexed page number",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 0,
              "minimum": 0
            }
          },
          {
            "name": "size",
            "in": "query",
            "description": "Page size",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 50,
              "maximum": 200,
              "minimum": 1
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Free-text search. The fields it matches are documented per endpoint.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Sort field, prefixed with '-' for descending (e.g. -createdAt). Only present on endpoints whose backing store supports sorting.",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of results",
            "headers": {
              "X-Total-Count": {
                "description": "Total number of resources matching the filters. Absent where counting is too expensive, in which case rel=\"last\" is absent too.",
                "style": "simple",
                "schema": {
                  "type": "integer"
                }
              },
              "Link": {
                "description": "RFC 8288 navigation links: first, prev, next, last. The absence of rel=\"next\" is the authoritative end-of-collection signal; do not infer it from the array length, and follow these links rather than building page URLs by hand.",
                "style": "simple",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Organization"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "post": {
        "tags": [
          "organizations"
        ],
        "summary": "Create an organization",
        "description": "Creates the organization and its administrators/viewers role groups",
        "operationId": "createOrganization",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Organization"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Organization created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organization"
                }
              }
            }
          },
          "409": {
            "description": "Alias already in use",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organization"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/organizations/{alias}/members": {
      "get": {
        "tags": [
          "members"
        ],
        "summary": "List an organization's members with their role",
        "description": "Follow the Link header to page through the collection. X-Total-Count is absent when a search narrows the list, because Keycloak cannot count members matching a search.",
        "operationId": "listMembers",
        "parameters": [
          {
            "name": "alias",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "0-indexed page number",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 0,
              "minimum": 0
            }
          },
          {
            "name": "size",
            "in": "query",
            "description": "Page size",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 50,
              "maximum": 200,
              "minimum": 1
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Free-text search. The fields it matches are documented per endpoint.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Sort field, prefixed with '-' for descending (e.g. -createdAt). Only present on endpoints whose backing store supports sorting.",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of results",
            "headers": {
              "X-Total-Count": {
                "description": "Total number of resources matching the filters. Absent where counting is too expensive, in which case rel=\"last\" is absent too.",
                "style": "simple",
                "schema": {
                  "type": "integer"
                }
              },
              "Link": {
                "description": "RFC 8288 navigation links: first, prev, next, last. The absence of rel=\"next\" is the authoritative end-of-collection signal; do not infer it from the array length, and follow these links rather than building page URLs by hand.",
                "style": "simple",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Member"
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown organization",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Member"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      },
      "post": {
        "tags": [
          "members"
        ],
        "summary": "Invite a person into the organization",
        "description": "Creates the account and emails the invitation (valid three days) when no account holds the email; otherwise attaches the existing account, whose owner already has a password. The answer is the same either way.",
        "operationId": "inviteMember",
        "parameters": [
          {
            "name": "alias",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MemberInvitation"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Member added",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "404": {
            "description": "Unknown organization",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "409": {
            "description": "Already a member of this organization",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "502": {
            "description": "Member added, but Keycloak could not send the invitation; resend it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/organizations/{alias}/members/{userId}/invitation": {
      "post": {
        "tags": [
          "members"
        ],
        "summary": "Email the invitation again to a member",
        "description": "For a member who has not accepted the invitation yet",
        "operationId": "resendMemberInvitation",
        "parameters": [
          {
            "name": "alias",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Email sent"
          },
          "404": {
            "description": "Unknown organization, or not a member of it"
          },
          "409": {
            "description": "The invitation was already accepted"
          },
          "502": {
            "description": "Keycloak could not send the email; worth retrying"
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/users/{id}/organizations": {
      "get": {
        "tags": [
          "users"
        ],
        "summary": "List the organizations a user belongs to, with their role in each",
        "operationId": "listUserOrganizations",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "404": {
            "description": "Unknown id",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/OrganizationMembership"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/users/count": {
      "get": {
        "tags": [
          "users"
        ],
        "summary": "Count users matching the filters",
        "description": "Answers a business question; paging is driven by the list endpoint's Link header. search has the same meaning as on the list endpoint.",
        "operationId": "countUsers",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Count"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/organizations/{alias}/members/count": {
      "get": {
        "tags": [
          "members"
        ],
        "summary": "Count an organization's members",
        "description": "Always the unfiltered total: Keycloak's count endpoint takes no filter",
        "operationId": "countMembers",
        "parameters": [
          {
            "name": "alias",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "404": {
            "description": "Unknown organization",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Count"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/organizations/count": {
      "get": {
        "tags": [
          "organizations"
        ],
        "summary": "Count organizations matching the filters",
        "description": "Answers a business question; paging is driven by the list endpoint's Link header. search has the same meaning as on the list endpoint: name and domains, never the alias.",
        "operationId": "countOrganizations",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Count"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    },
    "/me": {
      "get": {
        "tags": [
          "me"
        ],
        "summary": "Get the current account",
        "description": "The account behind the token, its organizations and the role held in each. Everything is read from the token itself, so the answer costs no directory lookup and reflects exactly what the token grants.",
        "operationId": "getCurrentUser",
        "responses": {
          "200": {
            "description": "The current account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CurrentUser"
                }
              }
            }
          }
        },
        "security": [
          {
            "smach": []
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "Captor": {
        "type": "string",
        "enum": [
          "accelerometer",
          "temperature",
          "humidity"
        ]
      },
      "Sensor": {
        "type": "object",
        "description": "Sensor - IoT sensor probe (temperature, humidity, accelerometer)",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Storage identifier",
            "readOnly": true
          },
          "identity": {
            "type": "string",
            "description": "Unique business identifier (IMEI or reference)",
            "example": "IMEI-LOGGER-001",
            "minLength": 1,
            "pattern": "\\S(.*\\S)?"
          },
          "created": {
            "type": "string",
            "format": "date-time",
            "description": "Creation timestamp",
            "readOnly": true
          },
          "updated": {
            "type": "string",
            "format": "date-time",
            "description": "Last update timestamp",
            "readOnly": true
          },
          "kinds": {
            "type": "array",
            "description": "Captors available on this probe",
            "items": {
              "$ref": "#/components/schemas/Captor"
            },
            "minItems": 1
          },
          "name": {
            "type": "string",
            "description": "Display name",
            "example": "Datalogger 001",
            "minLength": 1
          },
          "description": {
            "type": "string",
            "description": "Description or model information"
          },
          "type": {
            "type": "string",
            "description": "Inventory item type",
            "example": "logger",
            "readOnly": true
          }
        },
        "required": [
          "identity",
          "kinds",
          "name"
        ]
      },
      "Kind": {
        "type": "string",
        "description": "Packaging kind",
        "enum": [
          "inclosure",
          "crate"
        ]
      },
      "Packaging": {
        "type": "object",
        "description": "Packaging - Protection container (inclosure for static, crate for transport)",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Storage identifier",
            "readOnly": true
          },
          "identity": {
            "type": "string",
            "description": "Unique business identifier (IMEI or reference)",
            "example": "IMEI-LOGGER-001",
            "minLength": 1,
            "pattern": "\\S(.*\\S)?"
          },
          "created": {
            "type": "string",
            "format": "date-time",
            "description": "Creation timestamp",
            "readOnly": true
          },
          "updated": {
            "type": "string",
            "format": "date-time",
            "description": "Last update timestamp",
            "readOnly": true
          },
          "kind": {
            "$ref": "#/components/schemas/Kind",
            "description": "Packaging kind",
            "example": "inclosure"
          },
          "name": {
            "type": "string",
            "description": "Display name",
            "example": "Datalogger 001",
            "minLength": 1
          },
          "description": {
            "type": "string",
            "description": "Description or model information"
          },
          "category": {
            "type": "string",
            "description": "Packaging category",
            "example": "Isothermal"
          },
          "inside": {
            "type": "string",
            "description": "Internal material",
            "example": "Aluminum frame"
          },
          "outside": {
            "type": "string",
            "description": "External material",
            "example": "Stainless steel"
          },
          "thermal": {
            "type": "string",
            "description": "Thermal insulation material"
          },
          "thermalRate": {
            "type": "integer",
            "format": "int32",
            "description": "Thermal protection rating (0-5)",
            "example": 4,
            "maximum": 5,
            "minimum": 0
          },
          "vibration": {
            "type": "string",
            "description": "Vibration damping material"
          },
          "vibrationRate": {
            "type": "integer",
            "format": "int32",
            "description": "Vibration protection rating (0-5)",
            "example": 3,
            "maximum": 5,
            "minimum": 0
          },
          "size": {
            "$ref": "#/components/schemas/Size",
            "description": "Packaging dimensions (mm)"
          },
          "weight": {
            "type": "integer",
            "format": "int64",
            "description": "Packaging weight (grams)",
            "example": 180000
          },
          "pictures": {
            "type": "array",
            "description": "Photo URLs",
            "items": {
              "type": "string"
            }
          },
          "type": {
            "type": "string",
            "description": "Inventory item type",
            "example": "logger",
            "readOnly": true
          }
        },
        "required": [
          "identity",
          "kind",
          "name"
        ]
      },
      "Size": {
        "type": "object",
        "description": "Physical dimensions in millimeters",
        "properties": {
          "height": {
            "type": "integer",
            "format": "int64",
            "description": "Height in mm",
            "example": 700
          },
          "width": {
            "type": "integer",
            "format": "int64",
            "description": "Width in mm",
            "example": 68300
          },
          "depth": {
            "type": "integer",
            "format": "int64",
            "description": "Depth in mm",
            "example": 30
          }
        }
      },
      "Logger": {
        "type": "object",
        "description": "Logger - IoT datalogger central unit (GPS + Bluetooth box)",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Storage identifier",
            "readOnly": true
          },
          "identity": {
            "type": "string",
            "description": "Unique business identifier (IMEI or reference)",
            "example": "IMEI-LOGGER-001",
            "minLength": 1,
            "pattern": "\\S(.*\\S)?"
          },
          "created": {
            "type": "string",
            "format": "date-time",
            "description": "Creation timestamp",
            "readOnly": true
          },
          "updated": {
            "type": "string",
            "format": "date-time",
            "description": "Last update timestamp",
            "readOnly": true
          },
          "name": {
            "type": "string",
            "description": "Display name",
            "example": "Datalogger 001",
            "minLength": 1
          },
          "description": {
            "type": "string",
            "description": "Description or model information"
          },
          "type": {
            "type": "string",
            "description": "Inventory item type",
            "example": "logger",
            "readOnly": true
          }
        },
        "required": [
          "identity",
          "name"
        ]
      },
      "Gateway": {
        "type": "object",
        "description": "Gateway - IoT relay device (cabin/trailer)",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Storage identifier",
            "readOnly": true
          },
          "identity": {
            "type": "string",
            "description": "Unique business identifier (IMEI or reference)",
            "example": "IMEI-LOGGER-001",
            "minLength": 1,
            "pattern": "\\S(.*\\S)?"
          },
          "created": {
            "type": "string",
            "format": "date-time",
            "description": "Creation timestamp",
            "readOnly": true
          },
          "updated": {
            "type": "string",
            "format": "date-time",
            "description": "Last update timestamp",
            "readOnly": true
          },
          "name": {
            "type": "string",
            "description": "Display name",
            "example": "Datalogger 001",
            "minLength": 1
          },
          "description": {
            "type": "string",
            "description": "Description or model information"
          },
          "type": {
            "type": "string",
            "description": "Inventory item type",
            "example": "logger",
            "readOnly": true
          }
        },
        "required": [
          "identity",
          "name"
        ]
      },
      "Artwork": {
        "type": "object",
        "description": "Artwork - the monitored work, its packaging, checklist and base configuration",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Storage identifier",
            "readOnly": true
          },
          "tags": {
            "type": "array",
            "description": "Free tags, used to group and filter",
            "example": [
              "Medieval Art",
              "Tapestry"
            ],
            "items": {
              "type": "string"
            }
          },
          "reference": {
            "type": "string",
            "description": "Business key, unique within the organization, URL-safe",
            "example": "bayeux-tapestry",
            "minLength": 1,
            "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]*$"
          },
          "name": {
            "type": "string",
            "minLength": 1
          },
          "status": {
            "type": "string",
            "default": "draft",
            "description": "Lifecycle status. standby and monitoring are set by the monitoring lifecycle, never by a request",
            "enum": [
              "draft",
              "ready",
              "standby",
              "monitoring",
              "archived",
              "cancelled"
            ]
          },
          "summary": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "minLength": 1
          },
          "creator": {
            "type": "string"
          },
          "creation": {
            "type": "string",
            "description": "Date or period of creation, free text",
            "example": "11th century"
          },
          "picture": {
            "type": "string",
            "description": "Picture URL"
          },
          "medium": {
            "type": "string"
          },
          "size": {
            "$ref": "#/components/schemas/Size",
            "description": "Dimensions (mm)"
          },
          "weight": {
            "type": "integer",
            "format": "int64",
            "description": "Weight (grams)",
            "example": 350000
          },
          "metadata": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "inclosure": {
            "type": "string",
            "description": "Identity of the inclosure, the static protection",
            "example": "INC-001"
          },
          "crate": {
            "type": "string",
            "description": "Identity of the crate, the transport protection",
            "example": "CRT-001"
          },
          "checklist": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChecklistItem"
            }
          },
          "config": {
            "$ref": "#/components/schemas/Configuration"
          },
          "created": {
            "type": "string",
            "format": "date-time"
          },
          "updated": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "config",
          "description",
          "name",
          "reference"
        ]
      },
      "ChecklistItem": {
        "type": "object",
        "description": "One task to verify before a monitoring",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1
          },
          "description": {
            "type": "string"
          }
        },
        "required": [
          "title"
        ]
      },
      "Configuration": {
        "type": "object",
        "description": "Monitoring configuration (captor thresholds and logger parameters). Inherited: artwork, then monitoring, then datalogger",
        "properties": {
          "sensors": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/SensorConfig"
            },
            "description": "Settings keyed by captor (temperature, humidity, accelerometer)"
          },
          "logger": {
            "$ref": "#/components/schemas/LoggerConfig",
            "description": "Logger (datalogger box) settings"
          }
        }
      },
      "LoggerConfig": {
        "type": "object",
        "description": "Logger settings for battery, storage and recording frequency",
        "properties": {
          "battery": {
            "type": "integer",
            "format": "int32",
            "description": "Battery alert threshold (%)",
            "example": 15
          },
          "storage": {
            "type": "integer",
            "format": "int32",
            "description": "Storage alert threshold (MB)",
            "example": 100
          },
          "frequency": {
            "type": "integer",
            "format": "int32",
            "description": "Recording frequency (measurements per hour)",
            "example": 12
          }
        }
      },
      "SensorConfig": {
        "type": "object",
        "description": "Captor settings: interval, min and max for temperature and humidity, range and axis thresholds for the accelerometer",
        "properties": {
          "interval": {
            "type": "integer",
            "format": "int32",
            "description": "Measurement interval in seconds (temperature, humidity)",
            "example": 300
          },
          "min": {
            "type": "integer",
            "format": "int32",
            "description": "Minimum threshold: °C for temperature, %RH for humidity",
            "example": 15
          },
          "max": {
            "type": "integer",
            "format": "int32",
            "description": "Maximum threshold: °C for temperature, %RH for humidity",
            "example": 25
          },
          "range": {
            "type": "string",
            "description": "Accelerometer measurement range (2g, 4g, 8g, 16g)",
            "enum": [
              "2g",
              "4g",
              "8g",
              "16g"
            ],
            "example": "2g",
            "pattern": "2g|4g|8g|16g"
          },
          "xMin": {
            "type": "integer",
            "format": "int32",
            "description": "X-axis minimum threshold (raw int16, -32768 to 32767)",
            "example": -16384,
            "maximum": 32767,
            "minimum": -32768
          },
          "xMax": {
            "type": "integer",
            "format": "int32",
            "description": "X-axis maximum threshold (raw int16, -32768 to 32767)",
            "example": 16384,
            "maximum": 32767,
            "minimum": -32768
          },
          "yMin": {
            "type": "integer",
            "format": "int32",
            "description": "Y-axis minimum threshold (raw int16, -32768 to 32767)",
            "example": -16384,
            "maximum": 32767,
            "minimum": -32768
          },
          "yMax": {
            "type": "integer",
            "format": "int32",
            "description": "Y-axis maximum threshold (raw int16, -32768 to 32767)",
            "example": 16384,
            "maximum": 32767,
            "minimum": -32768
          },
          "zMin": {
            "type": "integer",
            "format": "int32",
            "description": "Z-axis minimum threshold (raw int16, -32768 to 32767)",
            "example": -16384,
            "maximum": 32767,
            "minimum": -32768
          },
          "zMax": {
            "type": "integer",
            "format": "int32",
            "description": "Z-axis maximum threshold (raw int16, -32768 to 32767)",
            "example": 16384,
            "maximum": 32767,
            "minimum": -32768
          }
        }
      },
      "Datalogger": {
        "type": "object",
        "description": "A logger paired with a sensor, as detected at setup",
        "properties": {
          "logger": {
            "type": "string",
            "description": "Identity of the logger, as the inventory holds it",
            "example": "18",
            "minLength": 1
          },
          "sensor": {
            "type": "string",
            "description": "Identity of the sensor, as the inventory holds it",
            "example": "s12",
            "minLength": 1
          },
          "gateway": {
            "type": "string",
            "description": "Identity of the gateway this datalogger reaches, when there is one",
            "example": "01000001"
          },
          "note": {
            "type": "string",
            "description": "Where it was placed, in words",
            "example": "Top right"
          },
          "position": {
            "$ref": "#/components/schemas/Location",
            "description": "Where it was placed, relative to the artwork"
          },
          "config": {
            "$ref": "#/components/schemas/Configuration",
            "description": "Overrides the configuration of the monitoring, for this datalogger alone"
          }
        },
        "required": [
          "logger",
          "sensor"
        ]
      },
      "GatewayAssignment": {
        "type": "object",
        "description": "A gateway and where it sits in the vehicle",
        "properties": {
          "gateway": {
            "type": "string",
            "description": "Identity of the gateway, as the inventory holds it",
            "example": "01000001",
            "minLength": 1
          },
          "position": {
            "type": "string",
            "description": "Where it sits in the vehicle, when the gateway reported it",
            "enum": [
              "cabin",
              "trailer"
            ]
          }
        },
        "required": [
          "gateway"
        ]
      },
      "Gsm": {
        "type": "object",
        "description": "Cell tower the device was reaching",
        "properties": {
          "id": {
            "type": "string",
            "description": "Cell tower identifier"
          },
          "rssi": {
            "type": "number",
            "format": "double",
            "description": "Received signal strength, in dBm",
            "example": -73
          }
        }
      },
      "Location": {
        "type": "object",
        "description": "Geographic coordinates, or a position relative to the artwork",
        "properties": {
          "latitude": {
            "type": "number",
            "format": "double",
            "description": "Latitude, or the relative Y position on the artwork",
            "example": 49.276
          },
          "longitude": {
            "type": "number",
            "format": "double",
            "description": "Longitude, or the relative X position on the artwork",
            "example": -0.702
          },
          "alt": {
            "type": "number",
            "format": "double",
            "description": "Altitude in metres",
            "example": 52
          },
          "gsm": {
            "$ref": "#/components/schemas/Gsm",
            "description": "Cell the position was resolved against, when there is no satellite fix"
          }
        }
      },
      "Monitoring": {
        "type": "object",
        "description": "Monitoring - one recording session of an artwork, with the equipment it engages",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifier, public: what a URL names and what events reference",
            "readOnly": true
          },
          "artwork": {
            "type": "string",
            "description": "Reference of the monitored artwork",
            "example": "bayeux-tapestry",
            "readOnly": true
          },
          "type": {
            "type": "string",
            "description": "Stationary for a museum or a store, transport for a movement",
            "enum": [
              "stationary",
              "transport"
            ]
          },
          "status": {
            "type": "string",
            "default": "pending",
            "description": "Where the session stands. Moved by the lifecycle, never by a request",
            "enum": [
              "pending",
              "in_progress",
              "ended"
            ],
            "readOnly": true
          },
          "config": {
            "$ref": "#/components/schemas/Configuration",
            "description": "The configuration this session was set up with, copied from the artwork"
          },
          "dataloggers": {
            "type": "array",
            "description": "The dataloggers engaged, at least one",
            "items": {
              "$ref": "#/components/schemas/Datalogger"
            },
            "minItems": 1
          },
          "gateways": {
            "type": "array",
            "description": "Where each gateway sits in the vehicle. Transport only",
            "items": {
              "$ref": "#/components/schemas/GatewayAssignment"
            }
          },
          "created": {
            "type": "string",
            "format": "date-time"
          },
          "updated": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "config",
          "dataloggers",
          "type"
        ]
      },
      "Coordinate": {
        "type": "object",
        "description": "A reading on three axes",
        "properties": {
          "x": {
            "type": "number",
            "format": "double",
            "description": "X axis",
            "example": 0.12
          },
          "y": {
            "type": "number",
            "format": "double",
            "description": "Y axis",
            "example": -0.34
          },
          "z": {
            "type": "number",
            "format": "double",
            "description": "Z axis",
            "example": 9.81
          }
        }
      },
      "Event": {
        "type": "object",
        "description": "Event - what happened during a recording session",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifier",
            "readOnly": true
          },
          "organization": {
            "type": "string",
            "description": "Organization of the monitoring this event was routed to",
            "readOnly": true
          },
          "monitoring": {
            "type": "string",
            "format": "uuid",
            "description": "Monitoring this event was routed to",
            "readOnly": true
          },
          "artwork": {
            "type": "string",
            "description": "Reference of the monitored artwork",
            "example": "bayeux-tapestry",
            "readOnly": true
          },
          "type": {
            "type": "string",
            "description": "What happened",
            "enum": [
              "start",
              "alert",
              "position",
              "notification",
              "stop"
            ]
          },
          "severity": {
            "type": "string",
            "description": "How much it matters",
            "enum": [
              "info",
              "warning",
              "critical"
            ]
          },
          "date": {
            "type": "string",
            "format": "date-time",
            "description": "When it happened, on the device",
            "example": "2026-09-23T10:00:00Z"
          },
          "location": {
            "$ref": "#/components/schemas/Location",
            "description": "Where the device thought it was. The object is required, a fix is not: a position resolved against a cell carries no latitude"
          },
          "sourceType": {
            "type": "string",
            "description": "What kind of thing the source names, which decides how the event is routed",
            "enum": [
              "monitoring",
              "sensor",
              "logger",
              "user"
            ]
          },
          "source": {
            "type": "string",
            "description": "Who or what reported it: a monitoring id, a sensor or logger identity, or the email of a user",
            "example": "s12",
            "minLength": 1
          },
          "message": {
            "type": "string",
            "description": "Why the threshold was crossed, on an alert",
            "example": "high_temperature"
          },
          "tag": {
            "type": "string",
            "description": "Free annotation carried by a notification",
            "example": "Road damaged"
          },
          "gyroscope": {
            "$ref": "#/components/schemas/Coordinate",
            "description": "Rotation of the device on three axes"
          },
          "accelerometer": {
            "$ref": "#/components/schemas/Coordinate",
            "description": "Acceleration of the device on three axes"
          },
          "speed": {
            "type": "number",
            "format": "double",
            "description": "Speed of the device, in metres per second",
            "example": 22.5
          },
          "sensor": {
            "$ref": "#/components/schemas/Captor",
            "description": "The captor that crossed its threshold, on an alert. Absent when the alert reports the state of the device itself, such as its battery or its storage"
          },
          "values": {
            "type": "array",
            "description": "What the captor measured: three axes for an accelerometer, one value for a temperature or a humidity",
            "example": [
              0.12,
              -0.34,
              9.81
            ],
            "items": {
              "type": "number",
              "format": "double"
            }
          },
          "created": {
            "type": "string",
            "format": "date-time",
            "description": "When this service received it",
            "readOnly": true
          }
        },
        "required": [
          "date",
          "location",
          "severity",
          "source",
          "sourceType",
          "type"
        ]
      },
      "EventCountReport": {
        "type": "object",
        "description": "What a recording session holds, counted",
        "properties": {
          "total": {
            "type": "integer",
            "format": "int64",
            "description": "Total number of events",
            "example": 1542
          },
          "byType": {
            "type": "object",
            "additionalProperties": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Count per type: start, alert, position, notification, stop"
          },
          "alertsBySeverity": {
            "type": "object",
            "additionalProperties": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Count per severity, alerts only: info, warning, critical"
          },
          "alertsBySensorType": {
            "type": "object",
            "additionalProperties": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Count per captor, alerts only: accelerometer, temperature, humidity"
          }
        }
      },
      "MonitoringCharts": {
        "type": "object",
        "description": "The alerts of a monitoring, one series per captor, laid out in columns for a chart",
        "properties": {
          "series": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/Series"
            },
            "description": "One series per captor that raised an alert. A captor with none is absent"
          }
        }
      },
      "Series": {
        "type": "object",
        "description": "The alerts of one captor, as parallel columns",
        "properties": {
          "dates": {
            "type": "array",
            "description": "When each alert happened, as reported by the device, oldest first",
            "items": {
              "type": "string",
              "format": "date-time"
            }
          },
          "values": {
            "type": "array",
            "description": "One column per component of the reading, each as long as dates: one for temperature and humidity, three (x, y, z) for the accelerometer, in raw device units. An entry the alert did not carry is null",
            "items": {
              "type": "array",
              "items": {
                "type": "number",
                "format": "double"
              }
            }
          },
          "severities": {
            "type": "array",
            "description": "The severity of each alert",
            "items": {
              "type": "string",
              "description": "How much an event matters",
              "enum": [
                "info",
                "warning",
                "critical"
              ]
            }
          }
        }
      },
      "MonitoringOverview": {
        "type": "object",
        "description": "A monitoring, with what its events say about it",
        "properties": {
          "monitoring": {
            "$ref": "#/components/schemas/Monitoring",
            "description": "The monitoring"
          },
          "started": {
            "type": "string",
            "format": "date-time",
            "description": "When recording started, from the start event. Absent while the monitoring is only set up"
          },
          "ended": {
            "type": "string",
            "format": "date-time",
            "description": "When recording stopped, from the stop event. Absent until the monitoring has ended"
          },
          "lastEvent": {
            "type": "string",
            "format": "date-time",
            "description": "Date of the latest event, as reported by the device. Absent when nothing was recorded"
          },
          "lastReceived": {
            "type": "string",
            "format": "date-time",
            "description": "When this service last received an event of the monitoring. A long gap is not by itself a fault: events are relayed by the mobile application, not sent by the devices"
          },
          "alerts": {
            "type": "object",
            "additionalProperties": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Alerts per severity. A severity with no alert is absent and reads as zero",
            "example": {
              "warning": 12,
              "critical": 1
            }
          },
          "lastAlert": {
            "$ref": "#/components/schemas/Event",
            "description": "The latest alert"
          },
          "lastPosition": {
            "$ref": "#/components/schemas/Location",
            "description": "Where the latest position placed it. Transport only"
          }
        }
      },
      "SseEmitter": {
        "type": "object",
        "properties": {
          "timeout": {
            "type": "integer",
            "format": "int64"
          }
        }
      },
      "Count": {
        "type": "object",
        "description": "Number of resources matching the filters",
        "properties": {
          "count": {
            "type": "integer",
            "format": "int64",
            "example": 137
          }
        }
      },
      "Exists": {
        "type": "object",
        "description": "Existence check result",
        "properties": {
          "exists": {
            "type": "boolean",
            "example": true
          }
        }
      },
      "Tags": {
        "type": "object",
        "description": "Tags in use",
        "properties": {
          "values": {
            "type": "array",
            "example": [
              "Medieval Art",
              "Tapestry"
            ],
            "items": {
              "type": "string"
            }
          }
        }
      },
      "User": {
        "type": "object",
        "description": "A platform user (backed by a Keycloak user; email is the username)",
        "properties": {
          "id": {
            "type": "string",
            "description": "Keycloak identifier, server-assigned",
            "readOnly": true
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Email, also used as the username",
            "example": "curator@bayeux.museum",
            "minLength": 1
          },
          "firstName": {
            "type": "string",
            "description": "Given name",
            "example": "Marie"
          },
          "lastName": {
            "type": "string",
            "description": "Family name",
            "example": "Durand"
          },
          "enabled": {
            "type": "boolean",
            "description": "Whether the account is active",
            "example": true
          },
          "emailVerified": {
            "type": "boolean",
            "description": "Whether the person confirmed the address; false while the invitation is pending",
            "readOnly": true
          },
          "passwordSet": {
            "type": "boolean",
            "description": "Whether a password exists; only on the single-user read",
            "readOnly": true
          },
          "locale": {
            "type": "string",
            "description": "Language of the emails and the login page",
            "example": "fr",
            "pattern": "[a-z]{2,3}(-[A-Z]{2})?"
          },
          "superAdmin": {
            "type": "boolean",
            "description": "Whether the user holds the platform-wide super-admin role",
            "example": false
          },
          "organizations": {
            "type": "array",
            "description": "Aliases of the organizations the user belongs to",
            "example": [
              "bayeux"
            ],
            "items": {
              "type": "string"
            },
            "readOnly": true
          },
          "lastLogin": {
            "type": "string",
            "format": "date-time",
            "description": "Last login, absent when none is recorded: never logged in, older than the event retention (90 days), or login events not readable by the service",
            "readOnly": true
          },
          "logins": {
            "type": "integer",
            "format": "int32",
            "description": "Logins over the event retention (90 days), counted up to 1000; only on the single-user read, and absent when login events are not readable by the service",
            "readOnly": true
          }
        },
        "required": [
          "email"
        ]
      },
      "Organization": {
        "type": "object",
        "description": "A platform organization (backed by a Keycloak Organization)",
        "properties": {
          "id": {
            "type": "string",
            "description": "Keycloak identifier, server-assigned",
            "readOnly": true
          },
          "alias": {
            "type": "string",
            "description": "Immutable technical alias",
            "example": "bayeux",
            "pattern": "[a-z0-9][a-z0-9-]*"
          },
          "name": {
            "type": "string",
            "description": "Display name",
            "example": "Bayeux Museum",
            "minLength": 1
          },
          "enabled": {
            "type": "boolean",
            "description": "Whether the organization is active",
            "example": true
          },
          "description": {
            "type": "string",
            "description": "Optional description"
          },
          "locale": {
            "type": "string",
            "description": "Default language of its members",
            "example": "fr",
            "pattern": "[a-z]{2,3}(-[A-Z]{2})?"
          },
          "members": {
            "type": "integer",
            "format": "int32",
            "description": "Number of members",
            "example": 7,
            "readOnly": true
          }
        },
        "required": [
          "name"
        ]
      },
      "RoleAssignment": {
        "type": "object",
        "description": "Role to grant to a member",
        "properties": {
          "role": {
            "type": "string",
            "enum": [
              "administrator",
              "viewer"
            ]
          }
        },
        "required": [
          "role"
        ]
      },
      "Member": {
        "type": "object",
        "description": "A member of an organization with their role",
        "properties": {
          "id": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "enabled": {
            "type": "boolean"
          },
          "emailVerified": {
            "type": "boolean",
            "description": "Whether the person confirmed the address; false while the invitation is pending"
          },
          "role": {
            "type": "string",
            "enum": [
              "administrator",
              "viewer"
            ]
          }
        }
      },
      "Profile": {
        "type": "object",
        "description": "The current account's own profile",
        "properties": {
          "email": {
            "type": "string",
            "description": "Email, also the username",
            "example": "curator@bayeux.museum",
            "readOnly": true
          },
          "firstName": {
            "type": "string",
            "description": "Given name",
            "example": "Marie",
            "maxLength": 255,
            "minLength": 0
          },
          "lastName": {
            "type": "string",
            "description": "Family name",
            "example": "Durand",
            "maxLength": 255,
            "minLength": 0
          },
          "locale": {
            "type": "string",
            "description": "Language of the emails and the login page",
            "example": "fr",
            "pattern": "[a-z]{2,3}(-[A-Z]{2})?"
          }
        },
        "required": [
          "firstName",
          "lastName"
        ]
      },
      "MemberInvitation": {
        "type": "object",
        "description": "A person to bring into the organization with a role",
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "description": "Email, also used as the username",
            "example": "curator@bayeux.museum",
            "minLength": 1
          },
          "firstName": {
            "type": "string",
            "description": "Given name, used when the account is created",
            "example": "Marie"
          },
          "lastName": {
            "type": "string",
            "description": "Family name, used when the account is created",
            "example": "Durand"
          },
          "locale": {
            "type": "string",
            "description": "Language of the emails and the login page, used when the account is created",
            "example": "fr",
            "pattern": "[a-z]{2,3}(-[A-Z]{2})?"
          },
          "role": {
            "type": "string",
            "description": "Role to grant in the organization",
            "enum": [
              "administrator",
              "viewer"
            ]
          }
        },
        "required": [
          "email",
          "role"
        ]
      },
      "OrganizationMembership": {
        "type": "object",
        "description": "An organization a user belongs to, with their role in it",
        "properties": {
          "alias": {
            "type": "string",
            "example": "bayeux"
          },
          "name": {
            "type": "string",
            "example": "Bayeux Museum"
          },
          "role": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "administrator",
              "viewer"
            ]
          }
        }
      },
      "CurrentUser": {
        "type": "object",
        "description": "The account behind the current token",
        "properties": {
          "id": {
            "type": "string",
            "description": "Account identifier",
            "example": "5f3c2b1a-0e4d-4a7b-9c8d-1e2f3a4b5c6d"
          },
          "email": {
            "type": "string",
            "description": "Account email, also its username",
            "example": "curator@bayeux.museum"
          },
          "role": {
            "type": "string",
            "description": "Realm-wide role, empty when the account holds none",
            "enum": [
              "super-admin",
              "administrator",
              "viewer",
              ""
            ],
            "example": "super-admin"
          },
          "organizations": {
            "type": "string",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Organizations the account belongs to, alias to display name",
            "example": {
              "bayeux": "Bayeux Museum"
            }
          },
          "roles": {
            "type": "string",
            "additionalProperties": {
              "type": "string",
              "enum": [
                "administrator",
                "viewer"
              ]
            },
            "description": "Role held per organization, absent where none is held",
            "example": {
              "bayeux": "administrator"
            }
          }
        }
      }
    },
    "securitySchemes": {
      "smach": {
        "type": "oauth2",
        "description": "Keycloak: authorization code with PKCE for a person, client credentials for a system",
        "flows": {
          "clientCredentials": {
            "tokenUrl": "https://id.smach.science/realms/smach/protocol/openid-connect/token",
            "scopes": {
              "organization:*": "The organizations of the account and the role held in each"
            }
          },
          "authorizationCode": {
            "authorizationUrl": "https://id.smach.science/realms/smach/protocol/openid-connect/auth",
            "tokenUrl": "https://id.smach.science/realms/smach/protocol/openid-connect/token",
            "scopes": {
              "organization:*": "The organizations of the account and the role held in each"
            }
          }
        }
      }
    }
  },
  "servers": [
    {
      "url": "https://api.smach.science"
    }
  ]
}
