{"openapi":"3.1.0","info":{"title":"Booking SaaS API","version":"0.2.0","description":"Tenant-scoped API for yoga studios and other booking businesses. Money values are integer minor units (øre for DKK), date-times are ISO 8601 UTC strings, and recurrence dates use the series timezone. Authenticate with a Bearer session token; the embedded demo additionally uses an HttpOnly demo session cookie."},"servers":[{"url":"/api","description":"Current Booking SaaS deployment"}],"tags":[{"name":"System","description":"Health, readiness, and API discovery."},{"name":"Authentication","description":"Passwordless and demo sessions."},{"name":"Account","description":"Tenant, users, memberships, and groups."},{"name":"Programme","description":"Locations, durations, events, recurrence, and instances."},{"name":"Commerce","description":"Products, MobilePay, entitlements, payments, and renewals."},{"name":"Bookings","description":"Attendee reservations and teacher rosters."},{"name":"Administration","description":"Aggregated administration data."}],"paths":{"/health":{"get":{"tags":["System"],"summary":"Check process health","description":"Returns immediately when the Express process is running. It does not check database connectivity.","responses":{"200":{"description":"Process is running.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"},"example":{"status":"ok"}}}}}}},"/health/ready":{"get":{"tags":["System"],"summary":"Check service readiness","description":"Reads the configured store and succeeds only when the application can serve API traffic.","responses":{"200":{"description":"Application and store are ready.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"},"example":{"status":"ready"}}}},"500":{"description":"The configured store cannot be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Database unavailable"}}}}}}},"/config":{"get":{"tags":["System"],"summary":"Read public client configuration","description":"Tells browser clients whether the resettable demo-persona login is enabled.","responses":{"200":{"description":"Public configuration.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Config"},"example":{"demoEnabled":true}}}}}}},"/public/accounts/{slug}":{"get":{"tags":["Account"],"summary":"Resolve a public tenant","description":"Resolves one active top-level account by its public slug without exposing memberships, users, payments, or other private tenant data.","parameters":[{"name":"slug","in":"path","required":true,"description":"Public tenant slug.","schema":{"type":"string"},"example":"demo-slug"}],"responses":{"200":{"description":"Public account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Account"},"example":{"id":"demo-harbour-yoga","slug":"harbour-yoga-demo","name":"Harbour Yoga","timezone":"Europe/Copenhagen","locale":"da-DK"}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}}}}},"/public/accounts/{slug}/events":{"get":{"tags":["Programme"],"summary":"List the public schedule","description":"Returns published events and their scheduled, publicly bookable instances for an active tenant. Cancelled and group-restricted instances are omitted.","parameters":[{"name":"slug","in":"path","required":true,"description":"Public tenant slug.","schema":{"type":"string"},"example":"demo-slug"}],"responses":{"200":{"description":"Published event catalogue and availability.","content":{"application/json":{"schema":{"type":"object"},"example":{"events":[{"id":"demo-morning-flow","accountId":"demo-harbour-yoga","title":"Morning Flow","description":"A steady, energising start to the day.","teacherIds":["demo-emil"],"location":"Harbour room","locationId":"demo-harbour-room","durationMinutes":60,"capacity":16,"status":"published"}],"instances":[{"id":"demo-morning-flow-instance-1","accountId":"demo-harbour-yoga","eventId":"demo-morning-flow","seriesId":"demo-morning-flow-series","startsAt":"2026-07-22T07:00:00.000Z","endsAt":"2026-07-22T08:00:00.000Z","capacity":16,"status":"scheduled","audience":"everyone","groupIds":[]}]}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}}}}},"/public/accounts/{slug}/products":{"get":{"tags":["Commerce"],"summary":"List public products","description":"Returns active drop-ins, finite passes, and subscriptions offered by the selected public tenant.","parameters":[{"name":"slug","in":"path","required":true,"description":"Public tenant slug.","schema":{"type":"string"},"example":"demo-slug"}],"responses":{"200":{"description":"Active public products.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Product"}},"example":[{"id":"demo-five-pass","accountId":"demo-harbour-yoga","type":"class_pass","name":"Five class pass","prices":[{"id":"demo-five-pass-price","amount":55000,"currency":"DKK"}],"classAllowance":5,"validityDays":90,"eligibleEventIds":[],"status":"active"}]}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}}}}},"/accounts/{accountId}/attendees":{"post":{"tags":["Authentication"],"summary":"Register an attendee for a tenant","description":"Creates or reuses a normalized email identity, adds an attendee membership in the selected active account, and sends a tenant-scoped single-use magic link.","parameters":[{"name":"accountId","in":"path","required":true,"description":"Active tenant accepting registration.","schema":{"type":"string"},"example":"demo-account"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","displayName"],"properties":{"email":{"type":"string","format":"email"},"displayName":{"type":"string","minLength":1}}},"example":{"email":"new-attendee@example.test","displayName":"New Attendee"}}}},"responses":{"201":{"description":"Attendee membership and magic link created.","content":{"application/json":{"schema":{"type":"object"},"example":{"account":{"id":"demo-harbour-yoga","slug":"harbour-yoga-demo","name":"Harbour Yoga","timezone":"Europe/Copenhagen","locale":"da-DK"},"user":{"id":"demo-maja","email":"maja@example.test","displayName":"Maja Nielsen","status":"active"}}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}}}}},"/openapi.json":{"get":{"tags":["System"],"summary":"Download this OpenAPI contract","description":"Returns the same OpenAPI 3.1 document rendered by `/api/docs/` for client generation and contract tooling.","responses":{"200":{"description":"OpenAPI 3.1 JSON document.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/demo-auth/session":{"post":{"tags":["Authentication"],"summary":"Start a demo-persona session","description":"When demo authentication is enabled, selects a seeded attendee, teacher, administrator, or owner and sets an HttpOnly cookie valid for one day.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DemoLogin"},"example":{"persona":"attendee"}}}},"responses":{"201":{"description":"Demo session cookie created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DemoLogin"},"example":{"persona":"attendee"}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"404":{"description":"Demo authentication is disabled or the persona is not seeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Demo authentication is disabled"}}}}}},"delete":{"tags":["Authentication"],"summary":"End the demo-persona session","description":"Expires the demo session cookie in the browser. This endpoint is safe to call when no demo session exists.","responses":{"204":{"description":"Demo cookie cleared."}}}},"/auth/signup":{"post":{"tags":["Authentication"],"summary":"Create an account and its owner","description":"Creates a new top-level tenant, an owner user and membership, then sends a single-use magic link through the configured email adapter.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupRequest"},"example":{"accountName":"Harbour Yoga","email":"owner@harbour.example","displayName":"Freja Larsen"}}}},"responses":{"201":{"description":"Account and owner created; magic link issued.","content":{"application/json":{"schema":{"type":"object"},"example":{"account":{"id":"demo-harbour-yoga","slug":"harbour-yoga-demo","name":"Harbour Yoga","timezone":"Europe/Copenhagen","locale":"da-DK"},"user":{"id":"demo-maja","email":"maja@example.test","displayName":"Maja Nielsen","status":"active"},"magicLink":{"expiresAt":"2026-07-20T08:15:00.000Z"}}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"409":{"description":"The email already exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Email already registered"}}}}}}},"/auth/magic-links":{"post":{"tags":["Authentication"],"summary":"Request a passwordless sign-in link","description":"Creates and emails a short-lived, single-use link for an existing user in the selected account.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MagicLinkRequest"},"example":{"email":"maja@example.test","accountId":"demo-harbour-yoga"}}}},"responses":{"202":{"description":"Request accepted and email adapter invoked.","content":{"application/json":{"schema":{"type":"object"},"example":{"expiresAt":"2026-07-20T08:15:00.000Z"}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}}}}},"/auth/magic-links/consume":{"post":{"tags":["Authentication"],"summary":"Exchange a magic link for a session","description":"Consumes the token exactly once and returns a 30-day Bearer session plus the authenticated user and tenant membership.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MagicLinkConsume"},"example":{"token":"single-use-token-from-email"}}}},"responses":{"200":{"description":"Authenticated session created.","content":{"application/json":{"schema":{"type":"object"},"example":{"session":{"id":"session-token","userId":"demo-maja","accountId":"demo-harbour-yoga","expiresAt":"2026-08-19T08:00:00.000Z"},"user":{"id":"demo-maja","email":"maja@example.test","displayName":"Maja Nielsen","status":"active"},"membership":{"accountId":"demo-harbour-yoga","userId":"demo-maja","roles":["attendee"]}}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"401":{"description":"The link is unknown, expired, or already consumed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Magic link is used or expired"}}}}}}},"/auth/logout":{"post":{"tags":["Authentication"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Invalidate the current session","description":"Deletes the presented Bearer or demo session from the store so it cannot be reused.","responses":{"204":{"description":"Session invalidated."},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}}}}},"/me":{"get":{"tags":["Account"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Read the current tenant context","description":"Returns the authenticated user, active top-level account and the roles held in that account.","responses":{"200":{"description":"Current identity and tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"},"example":{"user":{"id":"demo-maja","email":"maja@example.test","displayName":"Maja Nielsen","status":"active"},"account":{"id":"demo-harbour-yoga","slug":"harbour-yoga-demo","name":"Harbour Yoga","timezone":"Europe/Copenhagen","locale":"da-DK"},"roles":["attendee"]}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}}}}},"/me/accounts":{"get":{"tags":["Account"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"List the current user's accounts","description":"Lists every top-level account in which the authenticated identity participates, including roles and which account is active in the current session.","responses":{"200":{"description":"Available account contexts.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}},"example":[{"account":{"id":"demo-harbour-yoga","slug":"harbour-yoga-demo","name":"Harbour Yoga","timezone":"Europe/Copenhagen","locale":"da-DK"},"roles":["attendee"],"active":true}]}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}}}}},"/session/account":{"post":{"tags":["Authentication"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Select the active account","description":"Changes only the tenant context on the current session after verifying that the authenticated user has a membership in the requested account.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["accountId"],"properties":{"accountId":{"type":"string","minLength":1}}},"example":{"accountId":"demo-harbour-yoga"}}}},"responses":{"200":{"description":"Session tenant changed.","content":{"application/json":{"schema":{"type":"object"},"example":{"id":"session-token","userId":"demo-maja","accountId":"demo-harbour-yoga","expiresAt":"2026-08-19T08:00:00.000Z"}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}}}}},"/account":{"patch":{"tags":["Account"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Update top-level account settings","description":"Owner-only operation for changing the tenant name, IANA timezone, and locale used by administration and recurrence defaults.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountUpdate"},"example":{"name":"Harbour Yoga Copenhagen","timezone":"Europe/Copenhagen","locale":"da-DK"}}}},"responses":{"200":{"description":"Updated account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Account"},"example":{"id":"demo-harbour-yoga","slug":"harbour-yoga-demo","name":"Harbour Yoga","timezone":"Europe/Copenhagen","locale":"da-DK"}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}}}}},"/users":{"get":{"tags":["Account"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"List account users","description":"Lists user profiles that have a membership in the active tenant. Users from other tenants are never returned.","responses":{"200":{"description":"Tenant users.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/User"}},"example":[{"id":"demo-maja","email":"maja@example.test","displayName":"Maja Nielsen","status":"active"}]}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}}}}},"/users/invitations":{"post":{"tags":["Account"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Invite a user into the account","description":"Owner/administrator operation. Creates an invited user when needed and attaches the requested tenant roles; duplicate membership returns 409.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvitationRequest"},"example":{"email":"teacher@example.test","displayName":"Emil Madsen","roles":["teacher"]}}}},"responses":{"201":{"description":"Invitation and membership created.","content":{"application/json":{"schema":{"type":"object"},"example":{"user":{"id":"demo-maja","email":"maja@example.test","displayName":"Maja Nielsen","status":"active"},"membership":{"accountId":"demo-harbour-yoga","userId":"demo-maja","roles":["teacher"]}}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}},"409":{"description":"User already participates in this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"User already participates in this account"}}}}}}},"/memberships/{userId}/ownership-transfer":{"post":{"tags":["Account"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Transfer account ownership","description":"Owner-only operation. Promotes the target account member to owner and retains the previous owner as an administrator so the account always has exactly one continuing owner.","parameters":[{"name":"userId","in":"path","required":true,"description":"Existing member receiving ownership.","schema":{"type":"string"},"example":"demo-user"}],"responses":{"200":{"description":"Ownership transferred.","content":{"application/json":{"schema":{"type":"object"},"example":{"previousOwner":{"accountId":"demo-harbour-yoga","userId":"demo-sofie","roles":["administrator"]},"owner":{"accountId":"demo-harbour-yoga","userId":"demo-emil","roles":["owner"]}}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}}}}},"/memberships/{userId}":{"delete":{"tags":["Account"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Remove an account membership","description":"Owner-only operation. Removes a non-owner member from the active tenant. Ownership must be transferred before an owner can be removed.","parameters":[{"name":"userId","in":"path","required":true,"description":"Non-owner member to remove.","schema":{"type":"string"},"example":"demo-user"}],"responses":{"204":{"description":"Membership removed."},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}},"409":{"description":"The target remains an owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Transfer ownership before removal"}}}}}}},"/groups":{"get":{"tags":["Account"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"List booking groups","description":"Returns tenant groups used to restrict instance bookability to selected attendees.","responses":{"200":{"description":"Tenant groups.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Group"}},"example":[{"id":"demo-beginners","accountId":"demo-harbour-yoga","name":"Beginners","userIds":["demo-maja"]}]}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}}}},"post":{"tags":["Account"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Create a booking group","description":"Owner/administrator operation. Creates an empty tenant group; add members with the group-member endpoint.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NameRequest"},"example":{"name":"Beginners"}}}},"responses":{"201":{"description":"Group created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Group"},"example":{"id":"demo-beginners","accountId":"demo-harbour-yoga","name":"Beginners","userIds":["demo-maja"]}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}}}}},"/groups/{groupId}":{"patch":{"tags":["Account"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Rename a booking group","description":"Owner/administrator operation scoped to the active tenant.","parameters":[{"name":"groupId","in":"path","required":true,"description":"Group to rename.","schema":{"type":"string"},"example":"demo-group"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NameRequest"},"example":{"name":"Intermediate members"}}}},"responses":{"200":{"description":"Renamed group.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Group"},"example":{"id":"demo-beginners","accountId":"demo-harbour-yoga","name":"Beginners","userIds":["demo-maja"]}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}}}},"delete":{"tags":["Account"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Delete a booking group","description":"Owner/administrator operation. Removes the group; existing instance IDs referring to it are not expanded to another audience.","parameters":[{"name":"groupId","in":"path","required":true,"description":"Group to delete.","schema":{"type":"string"},"example":"demo-group"}],"responses":{"204":{"description":"Group deleted."},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}}}}},"/groups/{groupId}/users/{userId}":{"post":{"tags":["Account"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Add an account user to a group","description":"Owner/administrator operation. The user must already participate in the active tenant; adding the same user twice is idempotent.","parameters":[{"name":"groupId","in":"path","required":true,"description":"Target group.","schema":{"type":"string"},"example":"demo-group"},{"name":"userId","in":"path","required":true,"description":"Existing tenant user to add.","schema":{"type":"string"},"example":"demo-user"}],"responses":{"200":{"description":"Updated group membership.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Group"},"example":{"id":"demo-beginners","accountId":"demo-harbour-yoga","name":"Beginners","userIds":["demo-maja"]}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}}}}},"/locations":{"get":{"tags":["Programme"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"List reusable locations","description":"Lists active and archived tenant locations with their default capacities.","responses":{"200":{"description":"Tenant locations.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Location"}},"example":[{"id":"demo-harbour-room","accountId":"demo-harbour-yoga","name":"Harbour room","address":"Havnevej 1, Copenhagen","capacity":16,"status":"active"}]}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}}}},"post":{"tags":["Programme"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Create a reusable location","description":"Owner/administrator operation. Event creation can inherit the location name and capacity by passing its ID.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocationWrite"},"example":{"name":"Harbour room","address":"Havnevej 1","capacity":16,"status":"active"}}}},"responses":{"201":{"description":"Location created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Location"},"example":{"id":"demo-harbour-room","accountId":"demo-harbour-yoga","name":"Harbour room","address":"Havnevej 1, Copenhagen","capacity":16,"status":"active"}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}}}}},"/locations/{locationId}":{"patch":{"tags":["Programme"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Edit or archive a location","description":"Owner/administrator operation. Updates defaults for future event edits; it does not rewrite historical event instances.","parameters":[{"name":"locationId","in":"path","required":true,"description":"Location to update.","schema":{"type":"string"},"example":"demo-location"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocationWrite"},"example":{"capacity":18}}}},"responses":{"200":{"description":"Updated location.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Location"},"example":{"id":"demo-harbour-room","accountId":"demo-harbour-yoga","name":"Harbour room","address":"Havnevej 1, Copenhagen","capacity":18,"status":"active"}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}}}}},"/duration-presets":{"get":{"tags":["Programme"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"List reusable class lengths","description":"Returns tenant duration presets used by the administration event editor.","responses":{"200":{"description":"Duration presets.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DurationPreset"}},"example":[{"id":"demo-duration-60","accountId":"demo-harbour-yoga","name":"1 hour","minutes":60,"status":"active"}]}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}}}},"post":{"tags":["Programme"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Create a reusable class length","description":"Owner/administrator operation. Minutes must be a positive integer.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DurationWrite"},"example":{"name":"1.5 hours","minutes":90,"status":"active"}}}},"responses":{"201":{"description":"Duration preset created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DurationPreset"},"example":{"id":"demo-duration-60","accountId":"demo-harbour-yoga","name":"1 hour","minutes":60,"status":"active"}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}}}}},"/duration-presets/{presetId}":{"patch":{"tags":["Programme"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Edit or archive a class length","description":"Owner/administrator operation. Existing events retain their stored duration until edited.","parameters":[{"name":"presetId","in":"path","required":true,"description":"Duration preset to update.","schema":{"type":"string"},"example":"demo-preset"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DurationWrite"},"example":{"name":"75 minutes","minutes":75}}}},"responses":{"200":{"description":"Updated duration preset.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DurationPreset"},"example":{"id":"demo-duration-60","accountId":"demo-harbour-yoga","name":"1 hour","minutes":60,"status":"active"}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}}}}},"/events":{"get":{"tags":["Programme"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"List event definitions","description":"Returns tenant event templates. Concrete scheduled occurrences are returned by `/instances`.","responses":{"200":{"description":"Tenant events.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Event"}},"example":[{"id":"demo-morning-flow","accountId":"demo-harbour-yoga","title":"Morning Flow","description":"A steady, energising start to the day.","teacherIds":["demo-emil"],"location":"Harbour room","locationId":"demo-harbour-room","durationMinutes":60,"capacity":16,"status":"published"}]}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}}}},"post":{"tags":["Programme"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Create an event definition","description":"Owner, administrator, or teacher operation. Supplying `startsAt` also creates one concrete instance; omit it when a recurrence series will be created separately.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventWrite"},"example":{"title":"Morning Flow","description":"Energising vinyasa","teacherIds":["demo-emil"],"locationId":"demo-harbour-room","durationMinutes":60,"status":"published","startsAt":"2026-07-22T07:00:00.000Z"}}}},"responses":{"201":{"description":"Event created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Event"},"example":{"id":"demo-morning-flow","accountId":"demo-harbour-yoga","title":"Morning Flow","description":"A steady, energising start to the day.","teacherIds":["demo-emil"],"location":"Harbour room","locationId":"demo-harbour-room","durationMinutes":60,"capacity":16,"status":"published"}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}}}}},"/events/{eventId}":{"patch":{"tags":["Programme"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Edit an event definition","description":"Owner, administrator, or teacher operation. A selected location supplies its name and, unless capacity is explicit, its default capacity.","parameters":[{"name":"eventId","in":"path","required":true,"description":"Event definition to update.","schema":{"type":"string"},"example":"demo-event"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventWrite"},"example":{"title":"Morning Flow – summer","locationId":"demo-harbour-room","capacity":18}}}},"responses":{"200":{"description":"Updated event.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Event"},"example":{"id":"demo-morning-flow","accountId":"demo-harbour-yoga","title":"Morning Flow","description":"A steady, energising start to the day.","teacherIds":["demo-emil"],"location":"Harbour room","locationId":"demo-harbour-room","durationMinutes":60,"capacity":16,"status":"published"}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}}}}},"/events/{eventId}/series":{"post":{"tags":["Programme"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Create a bounded recurrence series","description":"Creates daily, weekly, or monthly occurrences in an IANA timezone. Supply either `endsOn` or `count`; weekdays use 0=Sunday through 6=Saturday.","parameters":[{"name":"eventId","in":"path","required":true,"description":"Event definition that owns the series.","schema":{"type":"string"},"example":"demo-event"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesWrite"},"example":{"frequency":"weekly","interval":1,"weekdays":[1,3],"localStartTime":"09:00","durationMinutes":60,"startsOn":"2026-07-22","endsOn":"2026-10-21","timezone":"Europe/Copenhagen"}}}},"responses":{"201":{"description":"Series and generated instances.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesResult"},"example":{"series":{"id":"demo-morning-flow-series","accountId":"demo-harbour-yoga","eventId":"demo-morning-flow","frequency":"weekly","interval":1,"weekdays":[3],"localStartTime":"09:00","durationMinutes":60,"timezone":"Europe/Copenhagen","startsOn":"2026-07-22","endsOn":"2026-10-21","status":"active"},"instances":[{"id":"demo-morning-flow-instance-1","accountId":"demo-harbour-yoga","eventId":"demo-morning-flow","seriesId":"demo-morning-flow-series","startsAt":"2026-07-22T07:00:00.000Z","endsAt":"2026-07-22T08:00:00.000Z","capacity":16,"status":"scheduled","audience":"everyone","groupIds":[]}]}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}}}}},"/series":{"get":{"tags":["Programme"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"List recurrence series","description":"Returns every recurrence rule belonging to the active tenant, including paused and ended series.","responses":{"200":{"description":"Tenant recurrence series.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Series"}},"example":[{"id":"demo-morning-flow-series","accountId":"demo-harbour-yoga","eventId":"demo-morning-flow","frequency":"weekly","interval":1,"weekdays":[3],"localStartTime":"09:00","durationMinutes":60,"timezone":"Europe/Copenhagen","startsOn":"2026-07-22","endsOn":"2026-10-21","status":"active"}]}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}}}}},"/series/{seriesId}":{"patch":{"tags":["Programme"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Edit, pause, resume, or end a series","description":"Status-only updates preserve occurrence IDs and bookings. Rule changes regenerate future, non-exception occurrences while retaining moved/cancelled exceptions and past instances.","parameters":[{"name":"seriesId","in":"path","required":true,"description":"Recurrence series to update.","schema":{"type":"string"},"example":"demo-series"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesPatch"},"example":{"interval":2,"localStartTime":"10:30","endsOn":"2026-11-30"}}}},"responses":{"200":{"description":"Updated series and resulting instances.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesResult"},"example":{"series":{"id":"demo-morning-flow-series","accountId":"demo-harbour-yoga","eventId":"demo-morning-flow","frequency":"weekly","interval":1,"weekdays":[3],"localStartTime":"09:00","durationMinutes":60,"timezone":"Europe/Copenhagen","startsOn":"2026-07-22","endsOn":"2026-10-21","status":"active"},"instances":[{"id":"demo-morning-flow-instance-1","accountId":"demo-harbour-yoga","eventId":"demo-morning-flow","seriesId":"demo-morning-flow-series","startsAt":"2026-07-22T07:00:00.000Z","endsAt":"2026-07-22T08:00:00.000Z","capacity":16,"status":"scheduled","audience":"everyone","groupIds":[]}]}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}}}}},"/instances":{"get":{"tags":["Programme"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"List scheduled event instances","description":"Returns concrete occurrences chronologically. Optional ISO date-time bounds use an inclusive `from` and exclusive `to`; `seriesId` narrows the result.","parameters":[{"name":"from","in":"query","required":false,"description":"Earliest start, inclusive.","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"description":"Latest start, exclusive.","schema":{"type":"string","format":"date-time"}},{"name":"seriesId","in":"query","required":false,"description":"Only occurrences from this recurrence series.","schema":{"type":"string"}}],"responses":{"200":{"description":"Chronological instances.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Instance"}},"example":[{"id":"demo-morning-flow-instance-1","accountId":"demo-harbour-yoga","eventId":"demo-morning-flow","seriesId":"demo-morning-flow-series","startsAt":"2026-07-22T07:00:00.000Z","endsAt":"2026-07-22T08:00:00.000Z","capacity":16,"status":"scheduled","audience":"everyone","groupIds":[]}]}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}}}}},"/instances/{instanceId}":{"patch":{"tags":["Programme"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Apply an occurrence exception or bookability policy","description":"Owner, administrator, or teacher operation. Cancel, move, or restore one occurrence without changing siblings, or make it bookable by everyone, selected groups, or nobody.","parameters":[{"name":"instanceId","in":"path","required":true,"description":"Concrete occurrence to change.","schema":{"type":"string"},"example":"demo-instance"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstanceAction"},"example":{"action":"move","startsAt":"2026-07-22T08:00:00.000Z"}}}},"responses":{"200":{"description":"Updated instance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Instance"},"example":{"id":"demo-morning-flow-instance-1","accountId":"demo-harbour-yoga","eventId":"demo-morning-flow","seriesId":"demo-morning-flow-series","startsAt":"2026-07-22T08:00:00.000Z","endsAt":"2026-07-22T08:00:00.000Z","capacity":16,"status":"moved","audience":"everyone","groupIds":[]}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}}}}},"/instances/{instanceId}/roster":{"get":{"tags":["Bookings"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Read a class roster","description":"Owner, administrator, or teacher operation. Returns confirmed bookings joined with attendee profiles for one tenant instance.","parameters":[{"name":"instanceId","in":"path","required":true,"description":"Occurrence whose roster should be opened.","schema":{"type":"string"},"example":"demo-instance"}],"responses":{"200":{"description":"Confirmed roster entries.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RosterEntry"}},"example":[{"booking":{"id":"demo-booking-1","accountId":"demo-harbour-yoga","instanceId":"demo-morning-flow-instance-1","attendeeId":"demo-maja","status":"confirmed","entitlementId":"demo-pass-entitlement","createdAt":"2026-07-20T08:00:00.000Z"},"attendee":{"id":"demo-maja","email":"maja@example.test","displayName":"Maja Nielsen","status":"active"}}]}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}}}}},"/instance-exceptions":{"get":{"tags":["Programme"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"List recurrence exceptions","description":"Returns the tenant audit records created by moving or cancelling individual recurring occurrences.","responses":{"200":{"description":"Move and cancellation exceptions.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InstanceException"}},"example":[{"id":"exception-1","accountId":"demo-harbour-yoga","instanceId":"demo-morning-flow-instance-1","type":"move","originalStartsAt":"2026-07-22T07:00:00.000Z","replacementStartsAt":"2026-07-22T08:00:00.000Z","createdAt":"2026-07-20T08:00:00.000Z"}]}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}}}}},"/products":{"get":{"tags":["Commerce"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"List booking products","description":"Returns draft, active, and archived products for the tenant. Prices are integer minor units.","responses":{"200":{"description":"Tenant products.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Product"}},"example":[{"id":"demo-five-pass","accountId":"demo-harbour-yoga","type":"class_pass","name":"Five class pass","prices":[{"id":"demo-five-pass-price","amount":55000,"currency":"DKK"}],"classAllowance":5,"validityDays":90,"eligibleEventIds":[],"status":"active"}]}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}}}},"post":{"tags":["Commerce"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Create a booking product","description":"Owner/administrator operation. Supports drop-ins, subscriptions with one or more billing cadences, finite class passes, workshops, teacher training, and custom products.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductWrite"},"example":{"type":"class_pass","name":"Five class pass","prices":[{"amount":55000,"currency":"DKK"}],"classAllowance":5,"validityDays":90,"eligibleEventIds":[],"status":"active"}}}},"responses":{"201":{"description":"Product created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Product"},"example":{"id":"demo-five-pass","accountId":"demo-harbour-yoga","type":"class_pass","name":"Five class pass","prices":[{"id":"demo-five-pass-price","amount":55000,"currency":"DKK"}],"classAllowance":5,"validityDays":90,"eligibleEventIds":[],"status":"active"}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}}}}},"/products/{productId}":{"patch":{"tags":["Commerce"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Edit a booking product","description":"Owner/administrator operation. Changes future purchases; existing payments and entitlements retain their recorded values.","parameters":[{"name":"productId","in":"path","required":true,"description":"Product to update.","schema":{"type":"string"},"example":"demo-product"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductPatch"},"example":{"prices":[{"id":"demo-five-pass-price","amount":57500,"currency":"DKK"}],"classAllowance":5,"validityDays":90,"status":"active"}}}},"responses":{"200":{"description":"Updated product.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Product"},"example":{"id":"demo-five-pass","accountId":"demo-harbour-yoga","type":"class_pass","name":"Five class pass","prices":[{"id":"demo-five-pass-price","amount":57500,"currency":"DKK"}],"classAllowance":5,"validityDays":90,"eligibleEventIds":[],"status":"active"}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}}}}},"/payment-connections":{"post":{"tags":["Commerce"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Connect MobilePay Business","description":"Owner-only operation. Replaces any existing tenant payment connection with a test or live MobilePay merchant reference.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentConnectionWrite"},"example":{"merchantReference":"harbour-yoga-test","environment":"test"}}}},"responses":{"201":{"description":"Payment connection active.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentConnection"},"example":{"id":"connection-1","accountId":"demo-harbour-yoga","provider":"mobilepay","environment":"test","merchantReference":"harbour-yoga-test","status":"connected"}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}}}}},"/products/{productId}/purchases":{"post":{"tags":["Commerce"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Purchase an active product with MobilePay","description":"Charges the selected product price through the tenant MobilePay connection. Supply priceId when a product has multiple prices. A paid charge creates an entitlement whose price and subscription cadence are snapshotted for future renewals.","parameters":[{"name":"productId","in":"path","required":true,"description":"Active product to purchase.","schema":{"type":"string"},"example":"demo-product"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PurchaseRequest"},"example":{"priceId":"demo-membership-monthly"}}}},"responses":{"201":{"description":"Payment attempted and entitlement created when paid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PurchaseResult"},"example":{"payment":{"id":"demo-payment-1","accountId":"demo-harbour-yoga","purchaserId":"demo-maja","productId":"demo-membership","priceId":"demo-membership-monthly","amount":69500,"currency":"DKK","status":"paid","providerReference":"mobilepay-test-123","createdAt":"2026-07-20T08:00:00.000Z"},"entitlement":{"id":"demo-membership-entitlement","accountId":"demo-harbour-yoga","userId":"demo-maja","productId":"demo-membership","price":{"id":"demo-membership-monthly","amount":69500,"currency":"DKK","cadence":{"unit":"month","count":1}},"renewsAt":"2026-08-20T08:00:00.000Z","status":"active"}}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}},"409":{"description":"MobilePay is not connected or the product is inactive.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"MobilePay connection not configured"}}}}}}},"/payments/{paymentId}/refund":{"post":{"tags":["Commerce"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Refund a paid MobilePay payment","description":"Owner/administrator operation. Omitting amount refunds the remaining balance. A partial refund keeps the entitlement active; a full refund revokes related entitlements and marks payment-backed bookings refunded. Every refund is audited.","parameters":[{"name":"paymentId","in":"path","required":true,"description":"Paid payment to refund.","schema":{"type":"string"},"example":"demo-payment"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"amount":{"type":"integer","minimum":1,"description":"Optional minor-unit amount. Defaults to the remaining paid balance."}}},"example":{"amount":25000}}}},"responses":{"200":{"description":"Refunded payment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Payment"},"example":{"id":"demo-payment-1","accountId":"demo-harbour-yoga","purchaserId":"demo-maja","productId":"demo-membership","priceId":"demo-membership-monthly","amount":69500,"currency":"DKK","status":"refunded","providerReference":"mobilepay-test-123","createdAt":"2026-07-20T08:00:00.000Z"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}},"409":{"description":"The requested refund exceeds the remaining paid amount.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Refund amount exceeds the remaining paid amount"}}}}}}},"/bookings":{"get":{"tags":["Bookings"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"List visible bookings","description":"Attendees see only their own bookings; owners, administrators, and teachers see all tenant bookings.","responses":{"200":{"description":"Visible tenant bookings.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Booking"}},"example":[{"id":"demo-booking-1","accountId":"demo-harbour-yoga","instanceId":"demo-morning-flow-instance-1","attendeeId":"demo-maja","status":"confirmed","entitlementId":"demo-pass-entitlement","createdAt":"2026-07-20T08:00:00.000Z"}]}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}}}},"post":{"tags":["Bookings"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Book an event instance","description":"Checks audience, group membership, capacity, expiry and credits. Supply an entitlement or a drop-in product. Unlimited active access is preferred over consuming a finite pass.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingRequest"},"example":{"instanceId":"demo-morning-flow-instance-1","entitlementId":"demo-pass-entitlement"}}}},"responses":{"201":{"description":"Confirmed booking.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"},"example":{"id":"demo-booking-1","accountId":"demo-harbour-yoga","instanceId":"demo-morning-flow-instance-1","attendeeId":"demo-maja","status":"confirmed","entitlementId":"demo-pass-entitlement","createdAt":"2026-07-20T08:00:00.000Z"}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"402":{"description":"Drop-in payment failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Payment failed"}}}},"403":{"description":"Attendee is outside allowed groups.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Attendee is outside the allowed groups"}}}},"409":{"description":"Instance is full/not bookable or access is exhausted/expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Instance is full"}}}}}}},"/bookings/{bookingId}":{"delete":{"tags":["Bookings"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Cancel a booking","description":"An attendee may cancel their own booking; owners/administrators may cancel any tenant booking. A consumed finite class-pass credit is restored.","parameters":[{"name":"bookingId","in":"path","required":true,"description":"Booking to cancel.","schema":{"type":"string"},"example":"demo-booking"}],"responses":{"200":{"description":"Cancelled booking.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"},"example":{"id":"demo-booking-1","accountId":"demo-harbour-yoga","instanceId":"demo-morning-flow-instance-1","attendeeId":"demo-maja","status":"cancelled","entitlementId":"demo-pass-entitlement","createdAt":"2026-07-20T08:00:00.000Z"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}}}}},"/entitlements":{"get":{"tags":["Commerce"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"List passes and memberships","description":"Attendees see their own access; owners/administrators see every tenant entitlement, including revoked and expired access.","responses":{"200":{"description":"Visible entitlements.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Entitlement"}},"example":[{"id":"demo-membership-entitlement","accountId":"demo-harbour-yoga","userId":"demo-maja","productId":"demo-membership","price":{"id":"demo-membership-monthly","amount":69500,"currency":"DKK","cadence":{"unit":"month","count":1}},"renewsAt":"2026-08-20T08:00:00.000Z","status":"active"}]}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}}}}},"/entitlements/{entitlementId}":{"patch":{"tags":["Commerce"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Cancel or reactivate a subscription","description":"Attendees may change their own subscription; owners/administrators may change tenant subscriptions. Cancellation revokes access and removes its renewal date; reactivation schedules the next charge using the purchased billing cadence.","parameters":[{"name":"entitlementId","in":"path","required":true,"description":"Subscription entitlement to change.","schema":{"type":"string"},"example":"demo-entitlement"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntitlementAction"},"example":{"action":"cancel"}}}},"responses":{"200":{"description":"Updated subscription entitlement.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Entitlement"},"example":{"id":"demo-membership-entitlement","accountId":"demo-harbour-yoga","userId":"demo-maja","productId":"demo-membership","price":{"id":"demo-membership-monthly","amount":69500,"currency":"DKK","cadence":{"unit":"month","count":1}},"renewsAt":null,"status":"revoked"}}}},"400":{"description":"The JSON body or query parameters are invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Validation failed"}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}},"404":{"description":"The tenant-scoped resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Resource not found"}}}},"409":{"description":"The entitlement is not a subscription.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Only subscriptions can be cancelled"}}}}}}},"/payments":{"get":{"tags":["Commerce"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"List visible payments","description":"Purchasers see their own payments; owners/administrators see all tenant payment and renewal records.","responses":{"200":{"description":"Visible payments.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Payment"}},"example":[{"id":"demo-payment-1","accountId":"demo-harbour-yoga","purchaserId":"demo-maja","productId":"demo-membership","priceId":"demo-membership-monthly","amount":69500,"currency":"DKK","status":"paid","providerReference":"mobilepay-test-123","createdAt":"2026-07-20T08:00:00.000Z"}]}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}}}}},"/subscriptions/renewals/run":{"post":{"tags":["Commerce"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Process due subscription renewals now","description":"Owner/administrator operation. Charges every due active subscription through MobilePay, records each payment, advances paid renewals by their snapshotted billing cadence, and revokes access after failed charges. Hosted deployments also run this hourly.","responses":{"200":{"description":"IDs grouped by renewal outcome.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenewalResult"},"example":{"renewed":["demo-membership-entitlement"],"failed":[]}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}}}}},"/admin/audit":{"get":{"tags":["Administration"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Search authoritative audit history","description":"Owner/administrator operation. Returns tenant-scoped state-transition entries and optionally searches action, entity, actor, or provider reference.","parameters":[{"name":"search","in":"query","required":false,"description":"Optional action, entity, actor, or provider-reference fragment.","schema":{"type":"string"}}],"responses":{"200":{"description":"Newest audit entries first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AuditEntry"}},"example":[{"id":"audit-1","accountId":"demo-harbour-yoga","actorId":"demo-sofie","action":"payment.partially_refunded","entityType":"payment","entityId":"demo-payment-1","providerReference":"mobilepay-test-123","createdAt":"2026-07-20T08:30:00.000Z"}]}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}}}}},"/admin/dashboard":{"get":{"tags":["Administration"],"security":[{"bearerAuth":[]},{"demoSession":[]}],"summary":"Read administration dashboard totals","description":"Owner, administrator, or teacher operation. Returns current tenant counts and the next five non-cancelled instances in chronological order.","responses":{"200":{"description":"Dashboard aggregate.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Dashboard"},"example":{"counts":{"users":7,"events":2,"bookings":4,"products":3},"upcoming":[{"id":"demo-morning-flow-instance-1","accountId":"demo-harbour-yoga","eventId":"demo-morning-flow","seriesId":"demo-morning-flow-series","startsAt":"2026-07-22T07:00:00.000Z","endsAt":"2026-07-22T08:00:00.000Z","capacity":16,"status":"scheduled","audience":"everyone","groupIds":[]}]}}}},"401":{"description":"Authentication is missing, expired, or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Authentication required"}}}},"403":{"description":"The authenticated account member lacks the required role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Insufficient permission"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"opaque session token","description":"Session ID returned by magic-link consumption."},"demoSession":{"type":"apiKey","in":"cookie","name":"booking_demo_session","description":"HttpOnly cookie created by the enabled demo-persona endpoint."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string"}}},"Health":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["ok","ready"]}}},"Config":{"type":"object","required":["demoEnabled"],"properties":{"demoEnabled":{"type":"boolean"}}},"DemoLogin":{"type":"object","required":["persona"],"properties":{"persona":{"type":"string","enum":["attendee","teacher","administrator","owner"]}}},"SignupRequest":{"type":"object","required":["accountName","email"],"properties":{"accountName":{"type":"string","minLength":2},"email":{"type":"string","format":"email"},"displayName":{"type":"string"}}},"MagicLinkRequest":{"type":"object","required":["email","accountId"],"properties":{"email":{"type":"string","format":"email"},"accountId":{"type":"string"}}},"MagicLinkConsume":{"type":"object","required":["token"],"properties":{"token":{"type":"string"}}},"Account":{"type":"object","required":["id","slug","name","timezone","locale"],"properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"timezone":{"type":"string","example":"Europe/Copenhagen"},"locale":{"type":"string","example":"da-DK"}}},"AccountUpdate":{"type":"object","properties":{"name":{"type":"string","minLength":2},"timezone":{"type":"string","description":"IANA timezone."},"locale":{"type":"string","minLength":2,"maxLength":16}},"minProperties":1},"User":{"type":"object","required":["id","email","displayName","status"],"properties":{"id":{"type":"string"},"email":{"type":"string","format":"email"},"displayName":{"type":"string"},"status":{"type":"string","enum":["invited","active","disabled"]}}},"Me":{"type":"object","required":["user","account","roles"],"properties":{"user":{"$ref":"#/components/schemas/User"},"account":{"$ref":"#/components/schemas/Account"},"roles":{"type":"array","items":{"type":"string","enum":["owner","administrator","teacher","attendee"]}}}},"InvitationRequest":{"type":"object","required":["email","displayName","roles"],"properties":{"email":{"type":"string","format":"email"},"displayName":{"type":"string"},"roles":{"type":"array","minItems":1,"items":{"type":"string","enum":["administrator","teacher","attendee"]}}}},"NameRequest":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1}}},"Group":{"type":"object","required":["id","accountId","name","userIds"],"properties":{"id":{"type":"string"},"accountId":{"type":"string"},"name":{"type":"string"},"userIds":{"type":"array","items":{"type":"string"}}}},"Location":{"type":"object","required":["id","accountId","name","capacity","status"],"properties":{"id":{"type":"string"},"accountId":{"type":"string"},"name":{"type":"string"},"address":{"type":"string"},"capacity":{"type":"integer","minimum":1},"status":{"type":"string","enum":["active","archived"]}}},"LocationWrite":{"type":"object","properties":{"name":{"type":"string","minLength":1},"address":{"type":"string"},"capacity":{"type":"integer","minimum":1},"status":{"type":"string","enum":["active","archived"]}}},"DurationPreset":{"type":"object","required":["id","accountId","name","minutes","status"],"properties":{"id":{"type":"string"},"accountId":{"type":"string"},"name":{"type":"string"},"minutes":{"type":"integer","minimum":1},"status":{"type":"string","enum":["active","archived"]}}},"DurationWrite":{"type":"object","properties":{"name":{"type":"string","minLength":1},"minutes":{"type":"integer","minimum":1},"status":{"type":"string","enum":["active","archived"]}}},"Event":{"type":"object","required":["id","accountId","title","teacherIds","durationMinutes","status"],"properties":{"id":{"type":"string"},"accountId":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"teacherIds":{"type":"array","items":{"type":"string"}},"location":{"type":"string"},"locationId":{"type":"string"},"capacity":{"type":"integer","minimum":1},"durationMinutes":{"type":"integer","minimum":1},"status":{"type":"string","enum":["draft","published","archived"]}}},"EventWrite":{"type":"object","properties":{"title":{"type":"string","minLength":1},"description":{"type":"string"},"teacherIds":{"type":"array","items":{"type":"string"}},"location":{"type":"string"},"locationId":{"type":"string"},"capacity":{"type":"integer","minimum":1},"durationMinutes":{"type":"integer","minimum":1},"status":{"type":"string","enum":["draft","published","archived"]},"startsAt":{"type":"string","format":"date-time"}}},"Series":{"type":"object","required":["id","accountId","eventId","frequency","interval","localStartTime","durationMinutes","timezone","startsOn","status"],"properties":{"id":{"type":"string"},"accountId":{"type":"string"},"eventId":{"type":"string"},"frequency":{"type":"string","enum":["daily","weekly","monthly"]},"interval":{"type":"integer","minimum":1,"maximum":52},"weekdays":{"type":"array","items":{"type":"integer","minimum":0,"maximum":6}},"monthDay":{"type":"integer","minimum":1,"maximum":31},"localStartTime":{"type":"string","pattern":"^\\d\\d:\\d\\d$"},"durationMinutes":{"type":"integer","minimum":1},"timezone":{"type":"string"},"startsOn":{"type":"string","format":"date"},"endsOn":{"type":["string","null"],"format":"date"},"count":{"type":["integer","null"],"minimum":1,"maximum":500},"status":{"type":"string","enum":["active","paused","ended"]}}},"SeriesWrite":{"allOf":[{"$ref":"#/components/schemas/SeriesPatch"},{"type":"object","required":["localStartTime","durationMinutes","startsOn"],"properties":{"timezone":{"type":"string","default":"UTC"}}}]},"SeriesPatch":{"type":"object","properties":{"frequency":{"type":"string","enum":["daily","weekly","monthly"]},"interval":{"type":"integer","minimum":1,"maximum":52},"weekdays":{"type":"array","items":{"type":"integer","minimum":0,"maximum":6}},"weekday":{"type":"integer","minimum":0,"maximum":6,"deprecated":true},"monthDay":{"type":"integer","minimum":1,"maximum":31},"localStartTime":{"type":"string","pattern":"^\\d\\d:\\d\\d$"},"durationMinutes":{"type":"integer","minimum":1},"startsOn":{"type":"string","format":"date"},"endsOn":{"type":["string","null"],"format":"date"},"count":{"type":["integer","null"],"minimum":1,"maximum":500},"timezone":{"type":"string"},"status":{"type":"string","enum":["active","paused","ended"]}}},"SeriesResult":{"type":"object","required":["series","instances"],"properties":{"series":{"$ref":"#/components/schemas/Series"},"instances":{"type":"array","items":{"$ref":"#/components/schemas/Instance"}}}},"Instance":{"type":"object","required":["id","accountId","eventId","startsAt","endsAt","status","audience","groupIds"],"properties":{"id":{"type":"string"},"accountId":{"type":"string"},"eventId":{"type":"string"},"seriesId":{"type":"string"},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"},"capacity":{"type":"integer","minimum":1},"status":{"type":"string","enum":["scheduled","cancelled","moved"]},"audience":{"type":"string","enum":["everyone","groups","nobody"]},"groupIds":{"type":"array","items":{"type":"string"}}}},"InstanceAction":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["cancel","move","restore","bookability"]},"startsAt":{"type":"string","format":"date-time","description":"Required for move."},"audience":{"type":"string","enum":["everyone","groups","nobody"],"description":"Used by bookability."},"groupIds":{"type":"array","items":{"type":"string"},"description":"Allowed group IDs when audience is groups."}}},"InstanceException":{"type":"object","additionalProperties":true,"required":["id","accountId","instanceId","type","createdAt"],"properties":{"id":{"type":"string"},"accountId":{"type":"string"},"instanceId":{"type":"string"},"type":{"type":"string","enum":["cancel","move"]},"originalStartsAt":{"type":"string","format":"date-time"},"replacementStartsAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"}}},"RosterEntry":{"type":"object","required":["booking","attendee"],"properties":{"booking":{"$ref":"#/components/schemas/Booking"},"attendee":{"$ref":"#/components/schemas/User"}}},"Product":{"type":"object","required":["id","accountId","type","name","prices","eligibleEventIds","status"],"properties":{"id":{"type":"string"},"accountId":{"type":"string"},"type":{"type":"string","enum":["drop_in","subscription","class_pass","workshop","teacher_training","other"]},"name":{"type":"string"},"prices":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/ProductPrice"}},"eligibleEventIds":{"type":"array","items":{"type":"string"},"description":"Empty means all events."},"classAllowance":{"type":"integer","minimum":1},"validityDays":{"type":"integer","minimum":1},"status":{"type":"string","enum":["draft","active","archived"]}}},"ProductWrite":{"allOf":[{"$ref":"#/components/schemas/ProductPatch"},{"type":"object","required":["type","name","prices"],"properties":{"type":{"type":"string","enum":["drop_in","subscription","class_pass","workshop","teacher_training","other"]}}}]},"ProductPatch":{"type":"object","properties":{"name":{"type":"string","minLength":1},"prices":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/ProductPriceWrite"}},"eligibleEventIds":{"type":"array","items":{"type":"string"}},"classAllowance":{"type":"integer","minimum":1},"validityDays":{"type":"integer","minimum":1},"status":{"type":"string","enum":["draft","active","archived"]}}},"BillingCadence":{"type":"object","required":["unit","count"],"properties":{"unit":{"type":"string","enum":["week","month","year"]},"count":{"type":"integer","minimum":1}}},"ProductPrice":{"type":"object","required":["id","amount","currency"],"properties":{"id":{"type":"string"},"amount":{"type":"integer","minimum":0,"description":"Minor currency units."},"currency":{"type":"string","minLength":3,"maxLength":3},"cadence":{"$ref":"#/components/schemas/BillingCadence"}}},"ProductPriceWrite":{"type":"object","required":["amount"],"properties":{"id":{"type":"string"},"amount":{"type":"integer","minimum":0},"currency":{"type":"string","minLength":3,"maxLength":3,"default":"DKK"},"cadence":{"$ref":"#/components/schemas/BillingCadence"}}},"PurchaseRequest":{"type":"object","properties":{"priceId":{"type":"string","description":"Required when the product offers more than one price option."}}},"PaymentConnectionWrite":{"type":"object","required":["merchantReference"],"properties":{"merchantReference":{"type":"string","minLength":1},"environment":{"type":"string","enum":["test","live"],"default":"test"}}},"PaymentConnection":{"type":"object","required":["id","accountId","provider","environment","merchantReference","status"],"properties":{"id":{"type":"string"},"accountId":{"type":"string"},"provider":{"const":"mobilepay"},"environment":{"type":"string","enum":["test","live"]},"merchantReference":{"type":"string"},"status":{"const":"connected"}}},"Payment":{"type":"object","required":["id","accountId","purchaserId","productId","priceId","amount","currency","status","createdAt"],"properties":{"id":{"type":"string"},"accountId":{"type":"string"},"purchaserId":{"type":"string"},"productId":{"type":"string"},"priceId":{"type":"string"},"amount":{"type":"integer"},"currency":{"type":"string"},"status":{"type":"string","enum":["pending","paid","failed","partially_refunded","refunded"]},"refundedAmount":{"type":"integer","minimum":0},"providerReference":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}},"Entitlement":{"type":"object","required":["id","accountId","userId","productId","price","status"],"properties":{"id":{"type":"string"},"accountId":{"type":"string"},"userId":{"type":"string"},"productId":{"type":"string"},"price":{"$ref":"#/components/schemas/ProductPrice"},"remainingClasses":{"type":"integer","minimum":0},"expiresAt":{"type":"string","format":"date-time"},"renewsAt":{"type":["string","null"],"format":"date-time"},"paidThroughAt":{"type":["string","null"],"format":"date-time"},"graceEndsAt":{"type":["string","null"],"format":"date-time"},"renewalAttempts":{"type":"integer","minimum":0,"maximum":3},"cancelAtPeriodEnd":{"type":"boolean"},"status":{"type":"string","enum":["active","grace","cancelling","suspended","expired","revoked"]}}},"PurchaseResult":{"type":"object","required":["payment"],"properties":{"payment":{"$ref":"#/components/schemas/Payment"},"entitlement":{"$ref":"#/components/schemas/Entitlement"}}},"BookingRequest":{"type":"object","required":["instanceId"],"properties":{"instanceId":{"type":"string"},"entitlementId":{"type":"string","description":"Use existing pass or membership."},"productId":{"type":"string","description":"Use an active drop-in product when no entitlement is supplied."},"priceId":{"type":"string","description":"Select a drop-in price when the product has multiple options."}},"oneOf":[{"required":["instanceId","entitlementId"]},{"required":["instanceId","productId"]}]},"Booking":{"type":"object","required":["id","accountId","instanceId","attendeeId","status","createdAt"],"properties":{"id":{"type":"string"},"accountId":{"type":"string"},"instanceId":{"type":"string"},"attendeeId":{"type":"string"},"status":{"type":"string","enum":["confirmed","cancelled","cancelled_late","cancelled_by_studio","refunded"]},"entitlementId":{"type":"string"},"paymentId":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}},"EntitlementAction":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["cancel","reactivate"]}}},"RenewalResult":{"type":"object","required":["renewed","failed"],"properties":{"renewed":{"type":"array","items":{"type":"string"}},"failed":{"type":"array","items":{"type":"string"}}}},"AuditEntry":{"type":"object","required":["id","accountId","actorId","action","entityType","entityId","before","after","createdAt"],"properties":{"id":{"type":"string"},"accountId":{"type":"string"},"actorId":{"type":"string"},"action":{"type":"string"},"entityType":{"type":"string"},"entityId":{"type":"string"},"before":{},"after":{},"providerReference":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}},"Dashboard":{"type":"object","required":["counts","upcoming"],"properties":{"counts":{"type":"object","required":["users","events","bookings","products"],"properties":{"users":{"type":"integer"},"events":{"type":"integer"},"bookings":{"type":"integer"},"products":{"type":"integer"}}},"upcoming":{"type":"array","maxItems":5,"items":{"$ref":"#/components/schemas/Instance"}}}}}}}