{
  "openapi": "3.1.0",
  "info": {
    "title": "OPEN Public Client API",
    "version": "1.0.0",
    "summary": "Unauthenticated read API behind openclient.app: the studio directory, a studio's public page, its published schedule and pass prices, its workshops, and public teacher pages.",
    "description": "This is the read-only, key-free half of OPEN's API — the data that renders the public pages on https://openclient.app. Use it to answer \"which studios exist near me\", \"what is on the schedule at this studio\", \"what does a class or pass cost\" and \"who teaches here\".\n\nNo API key, no OAuth, no cookie. Send a plain HTTPS GET. Responses are UTF-8 JSON. Dates are ISO 8601 (YYYY-MM-DD) and times are local to the studio in HH:MM.\n\nWhat this API is NOT: it cannot book, cancel, pay, or read anyone's personal bookings. Booking is a per-student authenticated action — an agent should use OPEN's MCP booking connector for that (see externalDocs), not this API. A studio reading its OWN private data (students, memberships, revenue) uses the separate key-authenticated Studio API at https://app.openmanagerapp.com/api/v1/openapi.json.\n\nPlease be a good citizen: cache the directory rather than re-fetching it per question, and identify yourself with a descriptive User-Agent.",
    "contact": {
      "name": "OPEN support",
      "email": "support@openclient.app",
      "url": "https://openclient.app/support"
    },
    "license": { "name": "Proprietary — free to read, see Terms", "url": "https://openclient.app/terms" },
    "termsOfService": "https://openclient.app/terms"
  },
  "externalDocs": {
    "description": "OPEN developer + agent documentation, including the MCP booking connector",
    "url": "https://openclient.app/developers"
  },
  "servers": [
    { "url": "https://app.openmanagerapp.com", "description": "Production API host for openclient.app" }
  ],
  "tags": [
    { "name": "Studios", "description": "Find studios and read a studio's public page, schedule and prices." },
    { "name": "Experiences", "description": "Workshops, retreats and one-off events published by studios." },
    { "name": "Teachers", "description": "Public teacher profiles, aggregated across every studio a teacher works at." }
  ],
  "paths": {
    "/businesses/pub_d.json": {
      "get": {
        "operationId": "listPublicStudios",
        "tags": ["Studios"],
        "summary": "List every publicly listed studio",
        "description": "Returns the full public studio directory — every studio that is currently active (it has classes scheduled in roughly the last 30 or next 14 days) and has opted into public listing. This is the same set the /explore page on openclient.app renders.\n\nThe response is a flat array, not paginated, and is small enough (tens to low hundreds of studios) to fetch once and filter client-side by city, country or business type. Use the returned `id` with getPublicStudio to read a studio's schedule and prices.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "The studio directory.",
            "content": {
              "application/json": {
                "schema": { "type": "array", "items": { "$ref": "#/components/schemas/StudioSummary" } }
              }
            }
          }
        }
      }
    },
    "/businesses/{businessId}/public_business.json": {
      "get": {
        "operationId": "getPublicStudio",
        "tags": ["Studios"],
        "summary": "Get one studio's public page: schedule, pass prices, teachers, rooms",
        "description": "Returns everything openclient.app renders on a studio page: the studio's profile, its published upcoming classes (`classes`), the passes and memberships it sells with prices (`ticket_types`), its class types, teachers, rooms and photos.\n\nThis is the endpoint to answer \"what classes are on at this studio and what do they cost\". It is a large document — a busy studio can return well over 100 KB — so fetch it per studio on demand rather than looping the whole directory.\n\nFields that only mean something to a signed-in student (for example `is_owner`, `access_request_status`) are present but will reflect the anonymous case.",
        "parameters": [
          {
            "name": "businessId",
            "in": "path",
            "required": true,
            "description": "The studio's numeric id, as returned in the `id` field of listPublicStudios.",
            "schema": { "type": "integer", "format": "int64", "minimum": 1 },
            "example": 28437
          }
        ],
        "responses": {
          "200": {
            "description": "The studio's public page payload.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StudioDetail" } } }
          },
          "404": {
            "description": "No studio with that id.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/experiences/public/listing": {
      "get": {
        "operationId": "listPublicExperiences",
        "tags": ["Experiences"],
        "summary": "List every published workshop, retreat and special event",
        "description": "Returns the platform-wide list of published experiences — workshops, teacher trainings, retreats and other one-off events, across all studios. Unlike a regular class these usually have their own price, capacity, cover image and description.\n\nUse `created_by` to map an experience back to the studio that runs it, and `date` / `date_end` for multi-day events.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Published experiences across all studios.",
            "content": {
              "application/json": {
                "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Experience" } }
              }
            }
          }
        }
      }
    },
    "/experiences/public/studio/{businessId}": {
      "get": {
        "operationId": "listPublicStudioExperiences",
        "tags": ["Experiences"],
        "summary": "List one studio's published experiences",
        "description": "The same shape as listPublicExperiences, narrowed to a single studio. Returns an empty array — not a 404 — when the studio exists but publishes no experiences, and also when the id matches no studio.",
        "parameters": [
          {
            "name": "businessId",
            "in": "path",
            "required": true,
            "description": "The studio's numeric id, as returned by listPublicStudios.",
            "schema": { "type": "integer", "format": "int64", "minimum": 1 },
            "example": 28437
          }
        ],
        "responses": {
          "200": {
            "description": "That studio's published experiences, possibly empty.",
            "content": {
              "application/json": {
                "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Experience" } }
              }
            }
          }
        }
      }
    },
    "/public/teachers/{slug}": {
      "get": {
        "operationId": "getPublicTeacherPage",
        "tags": ["Teachers"],
        "summary": "Get a public teacher profile and their upcoming classes",
        "description": "Returns the data behind a teacher page at https://openclient.app/t/{slug}: the teacher's name, photo and bio, their social links, every studio they currently teach at, and their upcoming schedule aggregated across all of those studios.\n\n`page_status` is `claimed` when the teacher has taken over and edited their own page, and `unclaimed` when the page is still auto-generated from studio rosters.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The teacher's URL slug, as it appears in https://openclient.app/t/{slug}. Lowercase, hyphen-separated, no dots or slashes.",
            "schema": { "type": "string", "pattern": "^[^/.]+$", "maxLength": 120 },
            "example": "nikita-gudkovs"
          }
        ],
        "responses": {
          "200": {
            "description": "The teacher page payload.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TeacherPage" } } }
          },
          "404": {
            "description": "No teacher page with that slug.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "not_found" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Error envelope. Older endpoints return only `message`; newer ones return a machine-readable `error` code. Treat both as optional and fall back to the HTTP status.",
        "properties": {
          "error": { "type": "string", "description": "Stable machine-readable code, e.g. `not_found`.", "examples": ["not_found"] },
          "message": { "type": "string", "description": "Human-readable explanation.", "examples": ["Couldn't find User with 'id'=999999999"] }
        },
        "additionalProperties": true
      },
      "StudioSummary": {
        "type": "object",
        "description": "One studio as it appears in the public directory.",
        "required": ["id", "name"],
        "properties": {
          "id": { "type": "integer", "format": "int64", "description": "Studio id — pass to getPublicStudio." },
          "name": { "type": "string", "description": "Studio name." },
          "city": { "type": ["string", "null"], "description": "City the studio is in." },
          "address": { "type": ["string", "null"], "description": "Full postal address as one line." },
          "address_j": { "type": ["object", "null"], "description": "The same address split into parts, when the studio has filled them in.", "additionalProperties": true },
          "phone": { "type": ["string", "null"], "description": "Public phone number." },
          "email": { "type": ["string", "null"], "description": "Public contact email." },
          "web": { "type": ["string", "null"], "description": "The studio's own website." },
          "cur": { "type": ["string", "null"], "description": "Currency the studio prices in, e.g. `EUR`, `NOK`.", "examples": ["EUR"] },
          "b_type": { "type": ["string", "null"], "description": "Primary business type, e.g. yoga, gym, pilates, dance." },
          "b_types": { "type": ["array", "null"], "description": "All business types the studio identifies with.", "items": { "type": "string" } },
          "logo": { "type": ["string", "null"], "format": "uri", "description": "Logo image URL." },
          "banner": { "type": ["string", "null"], "format": "uri", "description": "Banner image URL." },
          "photo": { "type": ["string", "null"], "format": "uri", "description": "Primary photo URL." }
        },
        "additionalProperties": true
      },
      "StudioDetail": {
        "type": "object",
        "description": "A studio's full public page. Only the fields an agent is likely to need are described; the payload carries more.",
        "required": ["id", "name"],
        "properties": {
          "id": { "type": "integer", "format": "int64" },
          "name": { "type": "string" },
          "description": { "type": ["string", "null"], "description": "The studio's own description of itself." },
          "city": { "type": ["string", "null"] },
          "address": { "type": ["string", "null"] },
          "country": { "type": ["string", "null"], "description": "ISO country code." },
          "cur": { "type": ["string", "null"], "description": "Currency all prices on this page are in." },
          "coordinates": { "type": ["object", "null"], "description": "Latitude/longitude of the studio.", "additionalProperties": true },
          "timeslots_present": { "type": ["boolean", "null"], "description": "Whether the studio books by appointment slots rather than fixed classes." },
          "show_signups": { "type": ["boolean", "null"], "description": "Whether the studio publishes how many people have booked each class." },
          "classes": {
            "type": "array",
            "description": "Upcoming published classes.",
            "items": { "$ref": "#/components/schemas/ScheduledClass" }
          },
          "ticket_types": {
            "type": "array",
            "description": "Passes, drop-ins and memberships the studio sells, with prices.",
            "items": { "$ref": "#/components/schemas/TicketType" }
          },
          "class_types": { "type": "array", "description": "The kinds of class this studio teaches.", "items": { "type": "object", "additionalProperties": true } },
          "teachers": { "type": "array", "description": "Teachers on the studio's roster.", "items": { "type": "object", "additionalProperties": true } },
          "rooms": { "type": "array", "description": "Rooms classes are held in.", "items": { "type": "object", "additionalProperties": true } },
          "photos": { "type": "array", "description": "Gallery photo URLs.", "items": { "type": "string", "format": "uri" } }
        },
        "additionalProperties": true
      },
      "ScheduledClass": {
        "type": "object",
        "description": "One class on a studio's schedule.",
        "properties": {
          "id": { "type": "integer", "format": "int64", "description": "Class id. The bookable page is https://openclient.app/studio/{businessId}/schedule/{id}." },
          "name": { "type": ["string", "null"], "description": "Class name as shown to students." },
          "date": { "type": "string", "format": "date", "description": "Local calendar date of the class.", "examples": ["2026-09-24"] },
          "start": { "type": ["string", "null"], "description": "Local start time, HH:MM.", "examples": ["09:00"] },
          "end": { "type": ["string", "null"], "description": "Local end time, HH:MM.", "examples": ["10:15"] },
          "capacity": { "type": ["integer", "null"], "description": "Total spots, when the studio caps the class." },
          "confirmed_bookings": { "type": ["integer", "null"], "description": "Spots already taken, when the studio publishes it." },
          "is_online": { "type": ["boolean", "null"], "description": "Whether the class is taught online." },
          "teacher_id": { "type": ["integer", "null"], "format": "int64" },
          "room_id": { "type": ["integer", "null"], "format": "int64" },
          "class_type_id": { "type": ["integer", "null"], "format": "int64" }
        },
        "additionalProperties": true
      },
      "TicketType": {
        "type": "object",
        "description": "A pass, drop-in or membership on sale at a studio. Prices are in the studio's `cur` currency.",
        "properties": {
          "id": { "type": "integer", "format": "int64" },
          "name": { "type": ["string", "null"], "description": "What the studio calls this pass." },
          "price": { "type": ["number", "string", "null"], "description": "Price in the studio's currency." },
          "classes_left": { "type": ["integer", "null"], "description": "Number of classes the pass includes. `-5` means unlimited." },
          "limit": { "type": ["string", "null"], "description": "Usage cap, as `<period>-<count>`: `w-2` is two classes per week, `m-4` four per month, `d-1` one per day. Empty or null means uncapped.", "examples": ["w-2"] },
          "includes_video": { "type": ["boolean", "null"], "description": "Whether the pass also unlocks the studio's video library." }
        },
        "additionalProperties": true
      },
      "Experience": {
        "type": "object",
        "description": "A workshop, retreat, training or other special event.",
        "properties": {
          "id": { "type": "integer", "format": "int64" },
          "name": { "type": ["string", "null"], "description": "Event title." },
          "created_by": { "type": ["integer", "null"], "format": "int64", "description": "Studio id that runs the event — pass to getPublicStudio." },
          "date": { "type": "string", "format": "date", "description": "Start date." },
          "date_end": { "type": ["string", "null"], "format": "date", "description": "End date for a multi-day event." },
          "is_multi_day": { "type": ["boolean", "null"] },
          "start": { "type": ["string", "null"], "description": "Local start time, HH:MM." },
          "end": { "type": ["string", "null"], "description": "Local end time, HH:MM." },
          "description": { "type": ["string", "null"] },
          "experience_summary": { "type": ["string", "null"], "description": "Short summary line." },
          "experience_category": { "type": ["string", "null"], "description": "Category, e.g. workshop, retreat, training." },
          "capacity": { "type": ["integer", "null"] },
          "experience_spots_left": { "type": ["integer", "null"], "description": "Remaining spots, when the studio publishes availability." },
          "venue_name": { "type": ["string", "null"] },
          "venue_city": { "type": ["string", "null"] },
          "venue_country": { "type": ["string", "null"] },
          "cover_image_url": { "type": ["string", "null"], "format": "uri" },
          "registration_deadline": { "type": ["string", "null"], "format": "date" },
          "ticket_types": { "type": ["array", "null"], "description": "Prices for this event.", "items": { "$ref": "#/components/schemas/TicketType" } }
        },
        "additionalProperties": true
      },
      "TeacherPage": {
        "type": "object",
        "description": "A public teacher profile, aggregated across every studio the teacher works at.",
        "properties": {
          "slug": { "type": "string", "description": "The slug this page is served at." },
          "page_status": { "type": "string", "enum": ["claimed", "unclaimed"], "description": "`claimed` once the teacher has edited their own page." },
          "teacher": {
            "type": "object",
            "description": "The teacher's own details.",
            "properties": {
              "name": { "type": ["string", "null"] },
              "photo": { "type": ["string", "null"], "format": "uri" },
              "bio": { "type": ["string", "null"] }
            },
            "additionalProperties": true
          },
          "socials": { "type": ["object", "null"], "description": "Social links the teacher has published.", "additionalProperties": true },
          "studios": { "type": "array", "description": "Studios the teacher currently teaches at.", "items": { "type": "object", "additionalProperties": true } },
          "schedule": { "type": "array", "description": "Upcoming classes across all of those studios.", "items": { "$ref": "#/components/schemas/ScheduledClass" } },
          "video": { "type": ["object", "null"], "description": "A featured video, when the teacher has published one.", "additionalProperties": true }
        },
        "additionalProperties": true
      }
    }
  }
}
