{"openapi":"3.1.0","info":{"title":"The Yoga Den API","description":"Public HTTP API for The Yoga Den yoga studio (Wallington, Surrey). The read-only /api/v1 endpoints describe the studio and its classes and need no authentication; the remaining endpoints power the website's own forms.\n\n## Versioning and deprecation\nThe API is versioned in the URL path (`/api/v1/...`), and every response carries an `API-Version` header. Changes within v1 are strictly additive: fields are added, never renamed, removed, or retyped. A breaking change would ship as `/api/v2`, and v1 would then return `Deprecation` and `Sunset` headers for at least six months before removal. The unversioned endpoints (`/api/class-sessions`, `/api/contact`, `/api/newsletter`, `/api/gift-cards/checkout`) follow the same additive-only rule.\n\n## Errors\nHTTP API failures use `{ error: { code, message, hint } }`. MCP protocol failures use standard JSON-RPC error objects. `code` is stable and machine-readable (safe to branch on), `message` is for humans, and `hint` names a concrete next request the caller can make. Undefined `/api/*` paths return the HTTP shape too, never an HTML error page.\n\n## Rate limits\nWrite endpoints are rate-limited per IP and advertise their quota with standard `RateLimit-Policy` and `RateLimit` headers (IETF draft-ietf-httpapi-ratelimit-headers) on every response; a 429 also carries `Retry-After` in seconds.","version":"1.0.0","contact":{"name":"The Yoga Den","email":"viv@theyogaden.co.uk","url":"https://www.theyogaden.co.uk"}},"servers":[{"url":"https://www.theyogaden.co.uk"}],"x-versioning":{"scheme":"url-path","current":"v1","responseHeader":"API-Version","compatibility":"additive-only","deprecationSignal":"Deprecation and Sunset headers, minimum 6 months notice"},"components":{"headers":{"ApiVersion":{"description":"API version that produced this response, e.g. `v1`.","schema":{"type":"string","enum":["v1"]}},"RateLimitPolicy":{"description":"Quota policy as an IETF RateLimit-Policy structured field, e.g. `\"contact-form\";q=5;w=3600` (5 requests per 3600-second window).","schema":{"type":"string"}},"RateLimit":{"description":"Live quota state as an IETF RateLimit structured field, e.g. `\"contact-form\";r=4;t=1800` (4 remaining, window resets in 1800 seconds).","schema":{"type":"string"}},"RetryAfter":{"description":"Seconds to wait before retrying after a 429.","schema":{"type":"integer","minimum":1}}},"schemas":{"Error":{"type":"object","description":"The single error shape used by every endpoint. Branch on error.code; show error.message to humans; error.hint names a concrete next request.","required":["error"],"properties":{"error":{"type":"object","required":["code","message","hint"],"properties":{"code":{"type":"string","description":"Stable machine-readable code, safe to branch on.","examples":["not_found","gone","unknown_class","validation_error","invalid_json","invalid_origin","rate_limited","upstream_error","server_misconfigured","internal_error"]},"message":{"type":"string","description":"Human-readable explanation."},"hint":{"type":"string","description":"A concrete next request the caller can make."}}}}},"Price":{"type":["object","null"],"description":"Price in a display-ready form; null when priced on enquiry.","properties":{"amount":{"type":"string","examples":["15.00"]},"currency":{"type":"string","examples":["GBP"]}}},"Studio":{"type":"object","required":["name","url","email","telephone","address","bookingUrl"],"properties":{"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string","format":"uri"},"email":{"type":"string","format":"email"},"telephone":{"type":"string"},"address":{"type":"object","properties":{"street":{"type":"string"},"locality":{"type":"string"},"region":{"type":"string"},"postalCode":{"type":"string"},"country":{"type":"string"}}},"geo":{"type":["object","null"],"description":"Exact studio entrance coordinates when verified. Null until then.","properties":{"latitude":{"type":"number"},"longitude":{"type":"number"}}},"teacher":{"type":"object","properties":{"name":{"type":"string"},"jobTitle":{"type":"string"},"description":{"type":"string"},"qualifications":{"type":"array","items":{"type":"string"}},"url":{"type":"string","format":"uri"}}},"maxClassSize":{"type":"integer","description":"Largest class size at the studio; use defaultClassSize and classSizeExceptions for individual class types.","examples":[10]},"defaultClassSize":{"type":"integer","examples":[8]},"classSizeExceptions":{"type":"object","description":"Maximum class sizes that differ from the default, keyed by class-page slug.","additionalProperties":{"type":"integer"},"examples":[{"gong-baths":10}]},"bookingUrl":{"type":"string","format":"uri"},"whenToUse":{"type":"array","items":{"type":"string"}},"whenToRecommendSomethingElse":{"type":"array","items":{"type":"string"}}}},"ClassSummary":{"type":"object","required":["slug","name","url"],"properties":{"slug":{"type":"string"},"name":{"type":"string"},"tagline":{"type":"string"},"summary":{"type":"string"},"maxClassSize":{"type":"integer","description":"Maximum people in this class type.","examples":[8,10]},"price":{"$ref":"#/components/schemas/Price"},"url":{"type":"string","format":"uri"}}},"ClassList":{"type":"object","required":["count","classes"],"properties":{"count":{"type":"integer"},"classes":{"type":"array","items":{"$ref":"#/components/schemas/ClassSummary"}}}},"Faq":{"type":"object","required":["question","answer"],"properties":{"question":{"type":"string"},"answer":{"type":"string"}}},"ClassDetail":{"type":"object","required":["slug","name","url","bookingUrl"],"properties":{"slug":{"type":"string"},"name":{"type":"string"},"tagline":{"type":"string"},"summary":{"type":"string"},"maxClassSize":{"type":"integer","description":"Maximum people in this class type.","examples":[8,10]},"description":{"type":"array","items":{"type":"string"}},"cautions":{"type":"array","items":{"type":"string"},"description":"Contraindications such as pregnancy. Read before recommending the class."},"faqs":{"type":"array","items":{"$ref":"#/components/schemas/Faq"}},"areaServed":{"type":"array","items":{"type":"string"}},"price":{"$ref":"#/components/schemas/Price"},"url":{"type":"string","format":"uri"},"bookingUrl":{"type":"string","format":"uri"},"markdownUrl":{"type":"string","format":"uri"}}},"Session":{"type":"object","required":["id","title","date","time","bookingUrl","status"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"date":{"type":"string","description":"Human-readable date."},"time":{"type":"string","description":"Human-readable time range."},"spotsLeft":{"type":["integer","null"]},"waitingList":{"type":"boolean"},"bookingUrl":{"type":"string","format":"uri"},"status":{"type":"string","enum":["available","filling-up","urgent","full","waitlist"]}}},"SubmissionAccepted":{"type":"object","required":["success","message"],"properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"}}},"ContactRequest":{"type":"object","required":["name","email","message"],"properties":{"name":{"type":"string","maxLength":100},"email":{"type":"string","format":"email"},"phone":{"type":"string","maxLength":20},"message":{"type":"string","maxLength":5000}}},"NewsletterRequest":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"},"firstName":{"type":"string","maxLength":100}}},"GiftCardCheckoutRequest":{"type":"object","required":["classType","quantity","cardDesign","buyerName","buyerEmail","recipientName"],"properties":{"classType":{"type":"string","enum":["hatha-yoga","pregnancy-yoga","yin-yoga","restorative-yoga","breathe-meditate-relax","gong-baths"]},"quantity":{"type":"integer","minimum":1,"maximum":10},"cardDesign":{"type":"string","enum":["garden-green","warm-earthy","studio-photo"]},"buyerName":{"type":"string"},"buyerEmail":{"type":"string","format":"email"},"recipientName":{"type":"string"},"recipientEmail":{"type":"string","format":"email","description":"Required unless deliverTo is buyer."},"personalMessage":{"type":"string","maxLength":200},"deliverTo":{"type":"string","enum":["recipient","buyer"],"default":"recipient","description":"recipient emails the gift card to recipientEmail; buyer emails it to buyerEmail to print or forward."},"sendDate":{"type":"string","format":"date","description":"Optional, recipient delivery only. A London date from tomorrow up to a year ahead; the gift card email is sent at 7am that day."}}},"CheckoutSessionCreated":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"Stripe Checkout URL to send the buyer to."}}},"JsonRpcRequest":{"type":"object","required":["jsonrpc","method"],"properties":{"jsonrpc":{"type":"string","const":"2.0"},"id":{"type":["string","integer","null"]},"method":{"type":"string","examples":["initialize","tools/list","tools/call","ping"]},"params":{"type":"object"}}},"JsonRpcResponse":{"type":"object","required":["jsonrpc","id"],"properties":{"jsonrpc":{"type":"string","const":"2.0"},"id":{"type":["string","integer","null"]},"result":{},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"integer"},"message":{"type":"string"},"data":{}}}}}},"responses":{"BadRequest":{"description":"Malformed or invalid request; error.code is invalid_json or validation_error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"Cross-origin submission rejected; error.code is invalid_origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"ServerError":{"description":"Server-side failure; error.code is internal_error or server_misconfigured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"UpstreamError":{"description":"A dependency (email, booking timetable, Stripe) failed; error.code is upstream_error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/api/v1/studio":{"get":{"operationId":"getStudio","summary":"Studio identity, contact, location, teacher, and routing guidance","description":"Everything needed to describe or recommend The Yoga Den, including whenToUse and whenToRecommendSomethingElse. No authentication.","responses":{"200":{"description":"Studio facts","headers":{"API-Version":{"$ref":"#/components/headers/ApiVersion"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Studio"}}}},"default":{"description":"Any failure; the shared Error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/classes":{"get":{"operationId":"listClasses","summary":"List every class type","description":"Class catalogue with slugs to pass to getClass. No authentication.","responses":{"200":{"description":"Class catalogue","headers":{"API-Version":{"$ref":"#/components/headers/ApiVersion"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClassList"}}}},"default":{"description":"Any failure; the shared Error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/classes/{slug}":{"get":{"operationId":"getClass","summary":"One class, with cautions and FAQs","description":"Read `cautions` before recommending a class; contraindications such as pregnancy are listed there. No authentication.","parameters":[{"name":"slug","in":"path","required":true,"description":"Class slug from listClasses.","schema":{"type":"string","enum":["hatha-yoga","pregnancy-yoga","restorative-yoga","yin-yoga","gong-baths","breathe-meditate-relax"]}}],"responses":{"200":{"description":"Class detail","headers":{"API-Version":{"$ref":"#/components/headers/ApiVersion"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClassDetail"}}}},"404":{"description":"Unknown class slug; error.code is unknown_class.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"description":"Any failure; the shared Error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/class-sessions":{"get":{"operationId":"listClassSessions","summary":"Upcoming sessions and availability for one class","description":"Sessions from the studio's booking system for the next four weeks. Sources cached for 30 seconds. Follow each bookingUrl; availability is rechecked when booking. partial=true means a week failed to load. No student information is returned.","parameters":[{"name":"class","in":"query","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":10,"default":3}}],"responses":{"200":{"description":"Upcoming sessions, possibly partial","content":{"application/json":{"schema":{"type":"object","required":["sessions","partial"],"properties":{"partial":{"type":"boolean"},"sessions":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Session"},{"type":"object","required":["source","startsAt"],"properties":{"source":{"type":"string","enum":["bookwhen","native"],"description":"Always native since Bookwhen closed on 23 September 2026; bookwhen stays in the enum for existing clients."},"startsAt":{"type":"string","description":"London wall time YYYY-MM-DDTHH:mm:ss; not a UTC timestamp."},"pricePence":{"type":"integer","description":"Present for native sessions; integer GBP pence."}}}]}}}}}}},"400":{"description":"Invalid class or limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"All active schedule sources failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/bookwhen":{"get":{"operationId":"listBookwhenSessions","deprecated":true,"summary":"Retired: use /api/class-sessions","description":"Retired on 23 September 2026 when the studio moved from Bookwhen to its own booking system. Always returns 410 with error.code gone and a hint to /api/class-sessions.","responses":{"410":{"description":"Endpoint retired; error.code is gone and error.hint names /api/class-sessions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/contact":{"post":{"operationId":"submitContact","summary":"Submit the studio contact form","description":"Origin-checked and rate-limited to 5 submissions per hour per IP; quota state is returned in RateLimit headers.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactRequest"}}}},"responses":{"200":{"description":"Message accepted","headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionAccepted"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"description":"Rate limited; error.code is rate_limited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}}},"502":{"$ref":"#/components/responses/UpstreamError"},"default":{"description":"Any failure; the shared Error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/newsletter":{"post":{"operationId":"subscribeNewsletter","summary":"Start newsletter double opt-in","description":"Origin-checked and rate-limited to 3 signups per hour per IP; quota state is returned in RateLimit headers.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewsletterRequest"}}}},"responses":{"200":{"description":"Opt-in email sent","headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionAccepted"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"description":"Rate limited; error.code is rate_limited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}}},"502":{"$ref":"#/components/responses/UpstreamError"},"default":{"description":"Any failure; the shared Error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/gift-cards/checkout":{"post":{"operationId":"createGiftCardCheckout","summary":"Create a Stripe Checkout session for a gift card","description":"Origin-checked and rate-limited to 5 checkouts per hour per IP; quota state is returned in RateLimit headers. The returned URL completes payment on Stripe.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GiftCardCheckoutRequest"}}}},"responses":{"200":{"description":"Checkout URL returned","headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutSessionCreated"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"description":"Rate limited; error.code is rate_limited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}}},"500":{"$ref":"#/components/responses/ServerError"},"default":{"description":"Any failure; the shared Error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/mcp":{"post":{"operationId":"mcpJsonRpc","summary":"MCP Streamable HTTP JSON-RPC endpoint","description":"Accepts MCP JSON-RPC methods including initialize, tools/list, and tools/call. Server card at /.well-known/mcp.json.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JsonRpcRequest"}}}},"responses":{"200":{"description":"JSON-RPC response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JsonRpcResponse"}}}},"202":{"description":"Notification accepted (empty body)"},"400":{"description":"Parse error or invalid JSON-RPC request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JsonRpcResponse"}}}},"default":{"description":"Any non-JSON-RPC failure; the shared Error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/{unmatched}":{"get":{"operationId":"unmatchedApiPath","summary":"Any undefined /api path","description":"Returns JSON rather than an HTML error page, so a caller that guessed wrong can recover.","parameters":[{"name":"unmatched","in":"path","required":true,"description":"Any path segment that no documented endpoint claims.","schema":{"type":"string"}}],"responses":{"404":{"description":"No such endpoint; error.code is not_found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}