{
  "openapi": "3.1.0",
  "info": {
    "title": "Ultiplace Public API",
    "version": "1.0.0",
    "description": "API publique Ultiplace. Permet de creer, publier, lister, modifier des salons virtuels, des stands, des conferences. Conforme OpenAPI 3.1 et utilisable comme Action ChatGPT, plugin Le Chat ou outil MCP.",
    "termsOfService": "https://www.ultiplace.com/legal/mentions-legales",
    "contact": {
      "name": "Ultiplace Support",
      "email": "contact@ultiplace.com",
      "url": "https://www.ultiplace.com"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    { "url": "https://www.ultiplace.com", "description": "Production" }
  ],
  "tags": [
    { "name": "salons", "description": "Operations sur les salons virtuels" },
    { "name": "stands", "description": "Stands d'exposants" },
    { "name": "halls", "description": "Halls thematiques" },
    { "name": "conferences", "description": "Programme de conferences" },
    { "name": "team", "description": "Equipe et invitations" },
    { "name": "search", "description": "Recherche annuaire" },
    { "name": "analytics", "description": "Statistiques" },
    { "name": "directory", "description": "Annuaire des salons professionnels physiques (presentiel)" }
  ],
  "components": {
    "securitySchemes": {
      "oauth2": {
        "type": "oauth2",
        "description": "OAuth 2.0 Authorization Code Flow (PKCE supporte). Backend supporte aussi en interne X-Ultiplace-Key et Bearer Firebase ID Token pour les integrations directes (voir https://www.ultiplace.com/ai/api-reference).",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://www.ultiplace.com/api/mcp/oauth/authorize",
            "tokenUrl": "https://www.ultiplace.com/api/mcp/oauth/token",
            "scopes": {
              "salon:read": "Lire la liste et le detail des salons virtuels",
              "salon:write": "Creer et modifier des salons virtuels",
              "salon:publish": "Publier (rendre visible publiquement) un salon",
              "stand:write": "Creer et personnaliser des stands d'exposants",
              "team:write": "Inviter des exposants et membres d'equipe",
              "analytics:read": "Lire les statistiques d'un salon",
              "directory:read": "Rechercher dans l'annuaire des salons physiques",
              "directory:write": "Ajouter, enrichir et soumettre des salons physiques a l'annuaire"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": { "type": "string" },
              "message": { "type": "string" }
            }
          }
        }
      },
      "SalonDsl": {
        "type": "object",
        "required": ["name"],
        "properties": {
          "name": { "type": "string", "maxLength": 120 },
          "slug": { "type": "string", "pattern": "^[a-z0-9-]{3,80}$" },
          "description": { "type": "string", "maxLength": 5000 },
          "shortDescription": { "type": "string", "maxLength": 320 },
          "theme": { "type": "string", "enum": ["classique", "moderne", "gaming", "industriel", "corporate", "innovation", "art"] },
          "dateStart": { "type": "string", "format": "date-time" },
          "dateEnd": { "type": "string", "format": "date-time" },
          "isPermanent": { "type": "boolean" },
          "sectors": { "type": "array", "items": { "type": "string" } },
          "language": { "type": "string", "enum": ["fr", "en", "es", "de", "it"] },
          "branding": {
            "type": "object",
            "properties": {
              "logo": { "type": "string", "format": "uri" },
              "banner": { "type": "string", "format": "uri" },
              "primaryColor": { "type": "string" },
              "accentColor": { "type": "string" }
            }
          },
          "halls": { "type": "array", "items": { "$ref": "#/components/schemas/HallSpec" } },
          "stands": { "type": "array", "items": { "$ref": "#/components/schemas/StandSpec" } },
          "conferences": { "type": "array", "items": { "$ref": "#/components/schemas/ConferenceSpec" } }
        }
      },
      "SalonSummary": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "slug": { "type": "string" },
          "status": { "type": "string" },
          "dateStart": { "type": "number" },
          "dateEnd": { "type": "number" },
          "exhibitorsCount": { "type": "integer" }
        }
      },
      "CreateSalonResponse": {
        "type": "object",
        "required": ["id"],
        "properties": {
          "id": { "type": "string" },
          "slug": { "type": "string" },
          "status": { "type": "string" },
          "url": { "type": "string", "format": "uri" }
        }
      },
      "HallSpec": {
        "type": "object",
        "required": ["name"],
        "properties": {
          "name": { "type": "string", "maxLength": 80 },
          "description": { "type": "string", "maxLength": 1000 },
          "theme": { "type": "string" },
          "image": { "type": "string", "format": "uri" },
          "maxStands": { "type": "integer", "minimum": 1, "maximum": 500 }
        }
      },
      "StandSpec": {
        "type": "object",
        "required": ["exhibitorName"],
        "properties": {
          "exhibitorName": { "type": "string" },
          "exhibitorEmail": { "type": "string", "format": "email" },
          "hallId": { "type": "string" },
          "template": { "type": "string", "enum": ["classique", "premium", "vitrine", "mini", "compact"] },
          "pitch": { "type": "string", "maxLength": 280 },
          "description": { "type": "string", "maxLength": 5000 },
          "logo": { "type": "string", "format": "uri" },
          "website": { "type": "string", "format": "uri" }
        }
      },
      "ConferenceSpec": {
        "type": "object",
        "required": ["title", "dateStart"],
        "properties": {
          "title": { "type": "string" },
          "description": { "type": "string" },
          "format": { "type": "string", "enum": ["keynote", "panel", "atelier", "networking", "demo", "pitch"] },
          "dateStart": { "type": "string", "format": "date-time" },
          "durationMinutes": { "type": "integer", "minimum": 5, "maximum": 480 },
          "speakers": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "title": { "type": "string" }, "company": { "type": "string" }, "photoUrl": { "type": "string", "format": "uri" }, "bio": { "type": "string" } } } }
        }
      },
      "PhysicalSalon": {
        "type": "object",
        "required": ["name", "dateStart", "dateEnd", "city", "description", "thematic"],
        "properties": {
          "name": { "type": "string", "minLength": 3, "maxLength": 200 },
          "slug": { "type": "string", "pattern": "^[a-z0-9-]{3,100}$" },
          "description": { "type": "string", "minLength": 80, "maxLength": 8000 },
          "shortDescription": { "type": "string", "maxLength": 320 },
          "dateStart": { "type": "string", "format": "date-time" },
          "dateEnd": { "type": "string", "format": "date-time" },
          "city": { "type": "string", "minLength": 2, "maxLength": 120 },
          "country": { "type": "string", "default": "FR" },
          "venue": { "type": "string", "maxLength": 200 },
          "address": { "type": "string", "maxLength": 400 },
          "postalCode": { "type": "string", "maxLength": 20 },
          "website": { "type": "string", "format": "uri" },
          "organizerName": { "type": "string", "maxLength": 200 },
          "organizerEmail": { "type": "string", "format": "email" },
          "organizerWebsite": { "type": "string", "format": "uri" },
          "expectedVisitors": { "type": "integer", "minimum": 0 },
          "expectedExhibitors": { "type": "integer", "minimum": 0 },
          "audienceType": { "type": "string", "enum": ["B2B", "B2C", "B2B2C"], "default": "B2B" },
          "sector": { "type": "string" },
          "thematic": { "type": "array", "minItems": 1, "maxItems": 12, "items": { "type": "string" } },
          "tags": { "type": "array", "maxItems": 30, "items": { "type": "string" } },
          "imageUrl": { "type": "string", "format": "uri" },
          "logoUrl": { "type": "string", "format": "uri" },
          "isRecurring": { "type": "boolean" },
          "recurringFrequency": { "type": "string", "enum": ["annual", "biennial", "quarterly", "monthly", "other"] },
          "registrationUrl": { "type": "string", "format": "uri" }
        }
      },
      "CreatePhysicalSalonResponse": {
        "type": "object",
        "required": ["salonId", "status", "qualityScore"],
        "properties": {
          "salonId": { "type": "string" },
          "slug": { "type": "string" },
          "status": { "type": "string", "enum": ["draft", "pending_review", "published", "rejected", "archived"] },
          "qualityScore": { "type": "integer", "minimum": 0, "maximum": 100 },
          "requiresHumanReview": { "type": "boolean" },
          "duplicated": { "type": "boolean" },
          "publicUrl": { "type": "string", "format": "uri", "nullable": true }
        }
      }
    }
  },
  "security": [
    {
      "oauth2": [
        "salon:read",
        "salon:write",
        "salon:publish",
        "stand:write",
        "team:write",
        "analytics:read",
        "directory:read",
        "directory:write"
      ]
    }
  ],
  "paths": {
    "/api/v1/salons": {
      "get": {
        "operationId": "listSalons",
        "tags": ["salons"],
        "summary": "Liste les salons de l'utilisateur",
        "parameters": [
          { "name": "status", "in": "query", "required": false, "schema": { "type": "string", "enum": ["draft", "published", "archived"] } },
          { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 100 } }
        ],
        "responses": {
          "200": {
            "description": "Liste des salons",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "salons": { "type": "array", "items": { "$ref": "#/components/schemas/SalonSummary" } },
                    "count": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createSalon",
        "tags": ["salons"],
        "summary": "Cree un salon a partir d'un DSL",
        "parameters": [
          { "name": "Idempotency-Key", "in": "header", "required": false, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["spec"],
                "properties": {
                  "spec": { "$ref": "#/components/schemas/SalonDsl" },
                  "publishImmediately": { "type": "boolean", "default": false }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Salon cree",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/CreateSalonResponse" } }
            }
          },
          "400": { "description": "DSL invalide", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Authentification manquante", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Quota depasse", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/v1/salons/{id}": {
      "get": {
        "operationId": "getSalon",
        "tags": ["salons"],
        "summary": "Detail d'un salon",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Detail du salon",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Detail complet du salon",
                  "properties": {
                    "id": { "type": "string" },
                    "name": { "type": "string" },
                    "slug": { "type": "string" },
                    "status": { "type": "string", "enum": ["draft", "published", "archived"] },
                    "description": { "type": "string" },
                    "dateStart": { "type": "number" },
                    "dateEnd": { "type": "number" },
                    "halls": { "type": "array", "items": { "type": "string" }, "description": "IDs des halls" },
                    "stands": { "type": "array", "items": { "type": "string" }, "description": "IDs des stands" },
                    "conferences": { "type": "array", "items": { "type": "string" } },
                    "url": { "type": "string", "format": "uri" }
                  },
                  "additionalProperties": true
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateSalon",
        "tags": ["salons"],
        "summary": "Mise a jour partielle d'un salon",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Champs partiels du salon a mettre a jour",
                "properties": {
                  "name": { "type": "string", "maxLength": 120 },
                  "description": { "type": "string", "maxLength": 5000 },
                  "shortDescription": { "type": "string", "maxLength": 320 },
                  "dateStart": { "type": "string", "format": "date-time" },
                  "dateEnd": { "type": "string", "format": "date-time" },
                  "sectors": { "type": "array", "items": { "type": "string" } },
                  "branding": {
                    "type": "object",
                    "properties": {
                      "logo": { "type": "string", "format": "uri" },
                      "banner": { "type": "string", "format": "uri" },
                      "primaryColor": { "type": "string" },
                      "accentColor": { "type": "string" }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Salon mis a jour" }
        }
      },
      "delete": {
        "operationId": "archiveSalon",
        "tags": ["salons"],
        "summary": "Archive un salon",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "Salon archive" } }
      }
    },
    "/api/v1/salons/{id}/publish": {
      "post": {
        "operationId": "publishSalon",
        "tags": ["salons"],
        "summary": "Publie un salon",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "Salon publie" } }
      }
    },
    "/api/v1/salons/{id}/halls": {
      "post": {
        "operationId": "addHall",
        "tags": ["halls"],
        "summary": "Ajoute un hall a un salon",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HallSpec" } } }
        },
        "responses": { "201": { "description": "Hall cree" } }
      }
    },
    "/api/v1/salons/{id}/stands": {
      "post": {
        "operationId": "createStand",
        "tags": ["stands"],
        "summary": "Cree un stand dans un salon",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StandSpec" } } }
        },
        "responses": { "201": { "description": "Stand cree" } }
      }
    },
    "/api/v1/salons/{id}/conferences": {
      "post": {
        "operationId": "scheduleConference",
        "tags": ["conferences"],
        "summary": "Planifie une conference",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConferenceSpec" } } }
        },
        "responses": { "201": { "description": "Conference planifiee" } }
      }
    },
    "/api/v1/salons/{id}/exhibitors/invite": {
      "post": {
        "operationId": "inviteExhibitors",
        "tags": ["team"],
        "summary": "Invite des exposants sur un salon",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["emails"],
                "properties": {
                  "emails": { "type": "array", "items": { "type": "string", "format": "email" } },
                  "hallId": { "type": "string" },
                  "message": { "type": "string", "maxLength": 2000 }
                }
              }
            }
          }
        },
        "responses": { "201": { "description": "Invitations creees" } }
      }
    },
    "/api/v1/stands/{id}/customize": {
      "patch": {
        "operationId": "customizeStand",
        "tags": ["stands"],
        "summary": "Personnalise un stand (template, couleurs, logo, pitch)",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Personnalisation partielle du stand",
                "properties": {
                  "template": { "type": "string", "enum": ["classique", "premium", "vitrine", "mini", "compact"] },
                  "primaryColor": { "type": "string", "description": "Couleur primaire (hex)" },
                  "accentColor": { "type": "string", "description": "Couleur accent (hex)" },
                  "logo": { "type": "string", "format": "uri" },
                  "banner": { "type": "string", "format": "uri" },
                  "pitch": { "type": "string", "maxLength": 280 },
                  "description": { "type": "string", "maxLength": 5000 },
                  "website": { "type": "string", "format": "uri" }
                }
              }
            }
          }
        },
        "responses": { "200": { "description": "Stand personnalise" } }
      }
    },
    "/api/v1/stands/{id}/resources": {
      "post": {
        "operationId": "uploadStandResource",
        "tags": ["stands"],
        "summary": "Ajoute une ressource (PDF, video, lien, image) a un stand",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["type", "url"],
                "properties": {
                  "type": { "type": "string", "enum": ["pdf", "video", "link", "image"] },
                  "title": { "type": "string" },
                  "url": { "type": "string", "format": "uri" }
                }
              }
            }
          }
        },
        "responses": { "201": { "description": "Ressource ajoutee" } }
      }
    },
    "/api/v1/search": {
      "get": {
        "operationId": "searchSalons",
        "tags": ["search"],
        "summary": "Recherche dans l'annuaire de salons publies",
        "parameters": [
          { "name": "q", "in": "query", "schema": { "type": "string" } },
          { "name": "sector", "in": "query", "schema": { "type": "string" } },
          { "name": "city", "in": "query", "schema": { "type": "string" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 50 } }
        ],
        "responses": {
          "200": {
            "description": "Resultats de recherche",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": { "type": "array", "items": { "$ref": "#/components/schemas/SalonSummary" } },
                    "count": { "type": "integer" },
                    "engine": { "type": "string", "description": "Moteur utilise (algolia | firestore)" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/analytics": {
      "get": {
        "operationId": "queryAnalytics",
        "tags": ["analytics"],
        "summary": "Statistiques d'un salon",
        "parameters": [
          { "name": "salonId", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "metric", "in": "query", "schema": { "type": "string", "enum": ["visitorCount", "viewCount", "exhibitorsCount", "conferencesCount", "leadsCount", "conversationsCount"] } }
        ],
        "responses": { "200": { "description": "Metriques" } }
      }
    },
    "/api/v1/directory/physical-salons": {
      "get": {
        "operationId": "searchPhysicalSalons",
        "tags": ["directory"],
        "summary": "Recherche dans l'annuaire des salons physiques",
        "description": "Recherche dans l'annuaire. Avec authentification : limite 200 resultats.",
        "parameters": [
          { "name": "q", "in": "query", "schema": { "type": "string" } },
          { "name": "city", "in": "query", "schema": { "type": "string" } },
          { "name": "sector", "in": "query", "schema": { "type": "string" } },
          { "name": "thematic", "in": "query", "schema": { "type": "string" } },
          { "name": "dateFrom", "in": "query", "schema": { "type": "string", "format": "date" } },
          { "name": "dateTo", "in": "query", "schema": { "type": "string", "format": "date" } },
          { "name": "expectedVisitorsMin", "in": "query", "schema": { "type": "integer" } },
          { "name": "audienceType", "in": "query", "schema": { "type": "string", "enum": ["B2B", "B2C", "B2B2C"] } },
          { "name": "recurring", "in": "query", "schema": { "type": "boolean" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 200 } }
        ],
        "responses": {
          "200": {
            "description": "Resultats annuaire",
            "content": { "application/json": { "schema": { "type": "object", "properties": {
              "results": { "type": "array", "items": { "$ref": "#/components/schemas/PhysicalSalon" } },
              "count": { "type": "integer" },
              "engine": { "type": "string", "description": "Moteur utilise (algolia | firestore)" }
            } } } }
          }
        }
      },
      "post": {
        "operationId": "createPhysicalSalon",
        "tags": ["directory"],
        "summary": "Ajoute un salon physique a l'annuaire (brouillon)",
        "description": "Cree un salon professionnel physique en brouillon. Idempotent via (name + city + dateStart). Si doublon, retourne le salon existant.",
        "parameters": [
          { "name": "Idempotency-Key", "in": "header", "required": false, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["salon"],
                "properties": {
                  "salon": { "$ref": "#/components/schemas/PhysicalSalon" },
                  "sourceUrl": { "type": "string", "format": "uri" },
                  "sourcePrompt": { "type": "string", "maxLength": 1000 }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Salon physique cree (ou retrouve si doublon)",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreatePhysicalSalonResponse" } } }
          },
          "400": { "description": "Donnees invalides", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Authentification manquante", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Quota journalier annuaire atteint", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/v1/directory/physical-salons/{id}": {
      "get": {
        "operationId": "getPhysicalSalon",
        "tags": ["directory"],
        "summary": "Detail d'un salon physique",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Detail du salon physique",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PhysicalSalon" } } }
          },
          "404": { "description": "Salon introuvable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "patch": {
        "operationId": "updatePhysicalSalon",
        "tags": ["directory"],
        "summary": "Mise a jour partielle d'un salon physique",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["patch"],
                "properties": {
                  "patch": {
                    "type": "object",
                    "description": "Champs partiels du PhysicalSalon a mettre a jour",
                    "properties": {
                      "name": { "type": "string", "maxLength": 200 },
                      "description": { "type": "string" },
                      "shortDescription": { "type": "string", "maxLength": 320 },
                      "dateStart": { "type": "string", "format": "date-time" },
                      "dateEnd": { "type": "string", "format": "date-time" },
                      "city": { "type": "string" },
                      "venue": { "type": "string" },
                      "website": { "type": "string", "format": "uri" },
                      "thematic": { "type": "array", "items": { "type": "string" } },
                      "expectedVisitors": { "type": "integer", "minimum": 0 },
                      "expectedExhibitors": { "type": "integer", "minimum": 0 },
                      "imageUrl": { "type": "string", "format": "uri" },
                      "logoUrl": { "type": "string", "format": "uri" }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": { "200": { "description": "Salon mis a jour" } }
      },
      "delete": {
        "operationId": "archivePhysicalSalon",
        "tags": ["directory"],
        "summary": "Archive un salon physique",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "Salon archive" } }
      }
    },
    "/api/v1/directory/physical-salons/{id}/enrich": {
      "post": {
        "operationId": "enrichPhysicalSalon",
        "tags": ["directory"],
        "summary": "Enrichit un salon depuis son site web",
        "description": "Recupere le site officiel et complete les champs manquants (description, image, logo, tags).",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "websiteUrl": { "type": "string", "format": "uri" },
                  "autoFromWeb": { "type": "boolean", "default": true }
                }
              }
            }
          }
        },
        "responses": { "200": { "description": "Salon enrichi" } }
      }
    },
    "/api/v1/directory/physical-salons/{id}/submit-review": {
      "post": {
        "operationId": "submitPhysicalSalonForReview",
        "tags": ["directory"],
        "summary": "Soumet un salon physique a la review humaine",
        "description": "Passe le salon en pending_review. Notifie l'admin Ultiplace par email + dashboard. Etape obligatoire avant publication pour les contributions IA.",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": { "note": { "type": "string", "maxLength": 1000 } }
              }
            }
          }
        },
        "responses": { "200": { "description": "Salon soumis a review" } }
      }
    },
    "/api/v1/directory/physical-salons/by-slug/{slug}": {
      "get": {
        "operationId": "getPhysicalSalonBySlug",
        "tags": ["directory"],
        "summary": "Lecture d'un salon physique par slug",
        "parameters": [{ "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Detail du salon physique",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PhysicalSalon" } } }
          },
          "404": { "description": "Salon introuvable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    }
  }
}
