# Mindbody API Spectral Ruleset # # Generated by API Evangelist analysing the Mindbody Public API v6 and Webhooks API. # Mindbody's specs use lowercased single-word path segments (e.g. /appointment/addons), # PascalCase JSON property names (e.g. "ClientId", "SiteId"), camelCase query parameters # (e.g. "request.endTime"), header-based API key auth (`API-Key`), and PascalCase tags # tied to top-level business resources (Appointment, Class, Client, Sale, Site, Staff, # Enrollment, Payroll, Cross Site, User Token). Webhooks live under /api/v1. # # Many of these rules are opinionated: they encode the *intended* shape of a # next-generation, Spectral-compliant Mindbody contract — they are not a strict mirror # of every quirk in the published v6 spec, which has accumulated legacy patterns. extends: - spectral:oas rules: # ─────────────────────────── INFO / METADATA ──────────────────────────── info-title-mindbody-prefix: description: API title must begin with "Mindbody". message: '{{property}} must start with "Mindbody" (got "{{value}}").' given: "$.info.title" severity: error then: function: pattern functionOptions: match: "^Mindbody\\b" info-description-required: description: API description must be present and meaningful (>= 60 chars). given: "$.info" severity: error then: field: description function: truthy info-description-length: description: Description should be at least 60 characters. given: "$.info.description" severity: warn then: function: length functionOptions: min: 60 info-contact-required: description: Mindbody contact block must be present. given: "$.info" severity: warn then: field: contact function: truthy info-license-required: description: License must be declared. given: "$.info" severity: warn then: field: license function: truthy # ─────────────────────────── OPENAPI VERSION ──────────────────────────── oas-version-3: description: Use OpenAPI 3.0.x. given: "$.openapi" severity: error then: function: pattern functionOptions: match: "^3\\.0\\." # ─────────────────────────── SERVERS ──────────────────────────────────── servers-defined: description: At least one server URL must be defined. given: "$.servers" severity: error then: function: length functionOptions: min: 1 servers-https: description: All server URLs must use HTTPS. given: "$.servers[*].url" severity: error then: function: pattern functionOptions: match: "^https://" servers-mindbody-host: description: Server URLs must point at api.mindbodyonline.com or push-api.mindbodyonline.com. given: "$.servers[*].url" severity: warn then: function: pattern functionOptions: match: "^https://(api|push-api)\\.mindbodyonline\\.com" # ─────────────────── PATHS — NAMING CONVENTIONS ───────────────────────── paths-version-prefix: description: 'Public API paths must include the /public/v6 prefix; Webhooks paths use /api/v1.' given: "$.paths.*~" severity: warn then: function: pattern functionOptions: match: "^/(public/v6|api/v1)/" paths-no-trailing-slash: description: Paths must not end with a trailing slash. given: "$.paths.*~" severity: error then: function: pattern functionOptions: notMatch: ".+/$" paths-no-query-strings: description: Paths must not contain query strings. given: "$.paths.*~" severity: error then: function: pattern functionOptions: notMatch: "\\?" paths-lowercase-segments: description: Path segments must be lowercase (Mindbody convention). given: "$.paths.*~" severity: warn then: function: pattern functionOptions: match: "^[a-z0-9/{}\\-_.]+$" # ─────────────────────────── OPERATIONS ───────────────────────────────── operation-operationid-required: description: Every operation must have an operationId. given: "$.paths.*[get,post,put,patch,delete]" severity: error then: field: operationId function: truthy operation-operationid-camelcase: description: operationId should use camelCase. given: "$.paths.*[get,post,put,patch,delete].operationId" severity: warn then: function: pattern functionOptions: match: "^[a-z][a-zA-Z0-9]*$" operation-operationid-verb-prefix: description: 'operationId should start with a verb (get, list, add, update, delete, send, check, validate, search, upsert, return).' given: "$.paths.*[get,post,put,patch,delete].operationId" severity: warn then: function: pattern functionOptions: match: "^(get|list|add|update|delete|create|remove|send|check|validate|search|upsert|return|cancel|issue)" operation-summary-required: description: Every operation must have a summary. given: "$.paths.*[get,post,put,patch,delete]" severity: error then: field: summary function: truthy operation-summary-mindbody-prefix: description: Operation summaries must begin with "Mindbody". given: "$.paths.*[get,post,put,patch,delete].summary" severity: warn then: function: pattern functionOptions: match: "^Mindbody\\b" operation-description-required: description: Every operation must have a description. given: "$.paths.*[get,post,put,patch,delete]" severity: warn then: field: description function: truthy operation-tags-required: description: Every operation must have at least one tag. given: "$.paths.*[get,post,put,patch,delete]" severity: error then: field: tags function: schema functionOptions: schema: type: array minItems: 1 # ─────────────────────────── TAGS ─────────────────────────────────────── tags-defined-globally: description: Top-level tags array must be defined. given: "$" severity: warn then: field: tags function: truthy tags-name-titlecase: description: Tag names should use Title Case (e.g. "Cross Site", "User Token"). given: "$.tags[*].name" severity: warn then: function: pattern functionOptions: match: "^[A-Z][A-Za-z0-9]*( [A-Z][A-Za-z0-9]*)*$" tags-description-required: description: Every tag should have a description. given: "$.tags[*]" severity: warn then: field: description function: truthy # ─────────────────────────── PARAMETERS ───────────────────────────────── parameter-description-required: description: Every parameter should have a description. given: "$.paths.*[get,post,put,patch,delete].parameters[*]" severity: warn then: field: description function: truthy parameter-siteid-header: description: SiteId must travel in the header, not the query. message: SiteId must be a header parameter on Mindbody Public API operations. given: "$.paths.*[get,post,put,patch,delete].parameters[?(@.name=='SiteId' || @.name=='siteId')]" severity: warn then: field: in function: pattern functionOptions: match: "^header$" parameter-apikey-not-in-query: description: API-Key must never appear as a query parameter. given: "$.paths.*[get,post,put,patch,delete].parameters[?(@.name=='API-Key' || @.name=='ApiKey' || @.name=='api_key')]" severity: error then: field: in function: pattern functionOptions: notMatch: "^query$" parameter-pagination-naming: description: Pagination uses `request.limit` and `request.offset` (Mindbody convention). given: "$.paths.*[get].parameters[?(@.in=='query' && (@.name=='limit' || @.name=='offset'))]" severity: info message: 'Prefer `request.limit` / `request.offset` over bare `limit` / `offset` to match Mindbody Public API style.' then: function: falsy # ─────────────────────────── REQUEST BODIES ───────────────────────────── request-body-json: description: Request bodies must offer application/json. given: "$.paths.*[post,put,patch].requestBody.content" severity: error then: field: application/json function: truthy # ─────────────────────────── RESPONSES ────────────────────────────────── response-success-required: description: Every operation must define a 2xx response. given: "$.paths.*[get,post,put,patch,delete].responses" severity: error then: function: schema functionOptions: schema: type: object patternProperties: "^2[0-9]{2}$": {} minProperties: 1 additionalProperties: true response-401-defined: description: Operations should document 401 Unauthorized. given: "$.paths.*[get,post,put,patch,delete].responses" severity: warn then: field: "401" function: truthy response-404-defined: description: Operations should document 404 Not Found. given: "$.paths.*[get,post,put,patch,delete].responses" severity: info then: field: "404" function: truthy response-description-required: description: Every response must have a description. given: "$.paths.*[get,post,put,patch,delete].responses.*" severity: warn then: field: description function: truthy # ────────────────── SCHEMAS — PROPERTY NAMING ─────────────────────────── schema-property-pascalcase: description: Mindbody JSON property names are PascalCase (e.g. ClientId, SiteId, Email). given: "$.components.schemas[*].properties.*~" severity: warn then: function: pattern functionOptions: match: "^[A-Z][A-Za-z0-9]*$" schema-id-property-naming: description: Identifier properties should follow {Entity}Id PascalCase (ClientId, SiteId, StaffId). given: "$.components.schemas[*].properties[?(@property.match(/^[Ii]d$|^id_|^Id_/))]~" severity: info message: Prefer fully-qualified PascalCase identifiers like ClientId, SiteId rather than bare `Id` or snake_case. then: function: falsy schema-type-required: description: Schemas must declare `type`. given: "$.components.schemas.*" severity: warn then: field: type function: truthy # ─────────────────────────── SECURITY ─────────────────────────────────── security-schemes-defined: description: At least one security scheme must be defined. given: "$.components.securitySchemes" severity: error then: function: truthy security-apikey-header: description: Mindbody API key must travel in the `API-Key` request header. given: "$.components.securitySchemes[?(@.type=='apiKey')]" severity: error then: function: schema functionOptions: schema: type: object properties: in: { const: header } name: { const: API-Key } required: [in, name] security-oauth2-mindbody-urls: description: OAuth 2 flows must point at Mindbody Identity Service. given: "$.components.securitySchemes[?(@.type=='oauth2')].flows.authorizationCode" severity: warn then: function: schema functionOptions: schema: type: object properties: authorizationUrl: type: string pattern: "^https://signin\\.mindbodyonline\\.com/connect/authorize$" tokenUrl: type: string pattern: "^https://signin\\.mindbodyonline\\.com/connect/token$" global-security-defined: description: Global security requirement must be defined. given: "$" severity: warn then: field: security function: truthy # ─────────────────── HTTP METHOD CONVENTIONS ──────────────────────────── get-no-request-body: description: GET operations must not declare a request body. given: "$.paths.*.get" severity: error then: field: requestBody function: falsy delete-no-request-body: description: DELETE operations should not declare a request body. given: "$.paths.*.delete" severity: warn then: field: requestBody function: falsy post-has-body-or-explicit-query: description: POST operations should declare a request body when the operation mutates state. given: "$.paths.*.post" severity: info then: field: requestBody function: truthy # ────────────────────────── MICROCKS ──────────────────────────────────── operation-microcks-extension: description: Every operation should carry an x-microcks-operation extension for mock-server compatibility. given: "$.paths.*[get,post,put,patch,delete]" severity: info then: field: x-microcks-operation function: truthy # ────────────────────────── QUALITY ───────────────────────────────────── operation-deprecated-documented: description: Deprecated operations must explain the replacement in description. given: "$.paths.*[get,post,put,patch,delete][?(@.deprecated==true)]" severity: warn then: field: description function: pattern functionOptions: match: "(?i)(deprecated|replaced|migrate)"